Skip to content

Commit

Permalink
Merge pull request #46 from Tico-Corp/feature-be/TICO-224-reissue-token
Browse files Browse the repository at this point in the history
[FEAT] 토큰 재발급 기능 구현 (TICO-224)
  • Loading branch information
bu119 authored Jul 23, 2024
2 parents 8d20813 + 6922570 commit d1181ca
Show file tree
Hide file tree
Showing 16 changed files with 447 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "admin: 관리자", description = "백엔드를 테스트를 위한 API")
@Tag(name = "Admin: 관리자", description = "백엔드를 테스트를 위한 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import com.tico.pomoro_do.domain.user.dto.request.GoogleJoinDTO;
import com.tico.pomoro_do.domain.user.dto.response.JwtDTO;
import com.tico.pomoro_do.domain.user.dto.response.TokenDTO;
import com.tico.pomoro_do.domain.user.service.AuthService;
import com.tico.pomoro_do.domain.user.service.TokenService;
import com.tico.pomoro_do.global.auth.jwt.JWTUtil;
import com.tico.pomoro_do.global.code.SuccessCode;
import com.tico.pomoro_do.global.enums.TokenType;
import com.tico.pomoro_do.global.response.SuccessResponseDTO;
import com.tico.pomoro_do.global.code.ErrorCode;
import com.tico.pomoro_do.global.exception.CustomException;
Expand All @@ -16,6 +20,8 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -38,19 +44,21 @@ public class AuthController {
// 토큰 갱신

private final AuthService authService;
private final TokenService tokenService;
//jwt관리 및 검증 utill
private final JWTUtil jwtUtil;

/**
* 구글 로그인 API
*
* @param googleIdTokenHeader Google-ID-Token 헤더에는 구글 ID 토큰이 포함됩니다.
* @param googleIdTokenHeader Google-ID-Token 헤더에 포함된 구글 ID 토큰
* @return 성공 시 JwtDTO를 포함하는 SuccessResponseDTO
* @throws CustomException 구글 ID 토큰 검증에 실패한 경우 예외를 던집니다.
*/
@Operation(
summary = "로그인",
description = "구글 소셜 로그인을 통해 로그인을 수행합니다. <br>"
+ "Google-ID-Token 헤더에는 구글 ID 토큰을 입력해야 합니다. 예시: Bearer 차돌이짱귀여워 <br>"
+ "로그인 성공 시에는 JwtDTO를 포함하는 SuccessResponseDTO를 반환합니다.",
summary = "구글 로그인",
description = "구글 소셜 로그인을 통해 사용자를 인증하고 JWT 토큰을 발급합니다. <br>"
+ "Google-ID-Token 헤더에 구글 ID 토큰을 입력해야 합니다. 예시: Bearer <id_token>",
parameters = @Parameter(
name = "Google-ID-Token",
description = "Google ID Token",
Expand All @@ -68,7 +76,9 @@ public class AuthController {
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
})
@PostMapping("/google/login")
public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleLogin(@RequestHeader("Google-ID-Token") String googleIdTokenHeader) {
public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleLogin(
@RequestHeader("Google-ID-Token") String googleIdTokenHeader
) {
try {
JwtDTO jwtResponse = authService.googleLogin(googleIdTokenHeader);
SuccessResponseDTO<JwtDTO> response = SuccessResponseDTO.<JwtDTO>builder()
Expand All @@ -78,24 +88,23 @@ public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleLogin(@RequestHeader("Go
.build();
return ResponseEntity.ok(response);
} catch (GeneralSecurityException | IOException | IllegalArgumentException e) {
log.error("Google ID Token verification failed: {}", e.getMessage(), e);
log.error("구글 ID 토큰 검증 실패: {}", e.getMessage(), e);
throw new CustomException(ErrorCode.GOOGLE_TOKEN_VERIFICATION_FAILED);
}
}

/**
* 구글 회원가입 API
*
* @param googleIdTokenHeader Google-ID-Token 헤더에는 구글 ID 토큰이 포함됩니다.
* @param googleIdTokenHeader Google-ID-Token 헤더에 포함된 구글 ID 토큰
* @param requestUserInfo 회원가입 요청 정보가 포함된 DTO
* @return 성공 시 JwtDTO를 포함하는 SuccessResponseDTO
* @throws CustomException 구글 ID 토큰 검증에 실패한 경우 예외를 던집니다.
*/
@Operation(
summary = "회원 가입",
description = "구글 소셜 로그인을 통해 회원가입을 수행합니다. <br>"
+ "Google-ID-Token 헤더에는 구글 ID 토큰을 입력하고, 요청 본문에는 닉네임 등의 추가 정보를 포함해야 합니다. <br>"
+ "회원가입 성공 시에는 JwtDTO를 포함하는 SuccessResponseDTO를 반환합니다.",
summary = "구글 회원가입",
description = "구글 소셜 로그인을 통해 사용자를 회원가입하고 JWT 토큰을 발급합니다. <br>"
+ "Google-ID-Token 헤더에 구글 ID 토큰을 입력하고, 요청 본문에는 추가 정보를 포함해야 합니다.",
parameters = @Parameter(
name = "Google-ID-Token",
description = "Google ID Token",
Expand All @@ -113,8 +122,10 @@ public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleLogin(@RequestHeader("Go
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
})
@PostMapping("/google/join")
public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleJoin(@RequestHeader("Google-ID-Token") String googleIdTokenHeader,
@Valid @RequestBody GoogleJoinDTO requestUserInfo) {
public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleJoin(
@RequestHeader("Google-ID-Token") String googleIdTokenHeader,
@Valid @RequestBody GoogleJoinDTO requestUserInfo
) {
try {
JwtDTO jwtResponse = authService.googleJoin(googleIdTokenHeader, requestUserInfo);
SuccessResponseDTO<JwtDTO> response = SuccessResponseDTO.<JwtDTO>builder()
Expand All @@ -124,8 +135,116 @@ public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleJoin(@RequestHeader("Goo
.build();
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (GeneralSecurityException | IOException | IllegalArgumentException e) {
log.error("Google ID Token verification failed: {}", e.getMessage(), e);
log.error("구글 ID 토큰 검증 실패: {}", e.getMessage(), e);
throw new CustomException(ErrorCode.GOOGLE_TOKEN_VERIFICATION_FAILED);
}
}

/**
* 토큰 재발급 API
*
* @param request HTTP 요청 객체
* @param response HTTP 응답 객체
* @return 재발급된 토큰을 포함하는 SuccessResponseDTO
*/
@Operation(
summary = "토큰 재발급",
description = "리프레시 토큰을 사용하여 새로운 액세스 토큰 및 리프레시 토큰을 재발급합니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "토큰 재발급 성공"),
@ApiResponse(responseCode = "401", description = "리프레시 토큰이 유효하지 않음",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
@ApiResponse(responseCode = "400", description = "리프레시 토큰 헤더가 없음",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
})
@PostMapping("/token/reissue")
public ResponseEntity<SuccessResponseDTO<TokenDTO>> reissueToken(
HttpServletRequest request,
HttpServletResponse response
) {
// 엑세스 토큰으로 현재 Redis 정보 삭제
// AuthService의 reissueToken 메서드 호출하여 결과 받기
TokenDTO tokenDTO = authService.reissueToken(request, response);

SuccessResponseDTO<TokenDTO> successResponse = SuccessResponseDTO.<TokenDTO>builder()
.status(SuccessCode.ACCESS_TOKEN_REISSUED.getHttpStatus().value())
.message(SuccessCode.ACCESS_TOKEN_REISSUED.getMessage())
.data(tokenDTO)
.build();

// 재발행 성공 시, HTTP 상태 코드 200(OK)와 함께 결과 반환
return ResponseEntity.ok(successResponse);
}

// /**
// * 액세스 토큰 검증 API
// * 이 엔드포인트는 JWT 인증이 필요하지 않습니다.
// *
// * @param tokenHeader X-Auth-Token 헤더로 전달된 액세스 토큰
// * @return 토큰 검증 결과를 반환합니다.
// */
// @Operation(
// summary = "액세스 토큰 검증",
// description = "전달된 액세스 토큰이 유효한지 검증합니다."
// )
// @ApiResponses(value = {
// @ApiResponse(responseCode = "200", description = "토큰 검증 성공"),
// @ApiResponse(responseCode = "401", description = "토큰이 유효하지 않음",
// content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
// @ApiResponse(responseCode = "400", description = "토큰 헤더가 없음",
// content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
// })
// @GetMapping("/token/validate")
// public ResponseEntity<SuccessResponseDTO<String>> validateAccessToken(
// @RequestHeader("X-Auth-Token") String tokenHeader
// ) {
// String token = authService.extractToken(tokenHeader, TokenType.JWT);
// tokenService.validateToken(token, "access");
// SuccessResponseDTO<String> successResponse = SuccessResponseDTO.<String>builder()
// .status(SuccessCode.ACCESS_TOKEN_VALIDATED.getHttpStatus().value())
// .message(SuccessCode.ACCESS_TOKEN_VALIDATED.getMessage())
// .data(SuccessCode.ACCESS_TOKEN_VALIDATED.name()) // data가 없을 때는 null로 설정
// .build();
//
// // 검증 성공 시, HTTP 상태 코드 200(OK)와 함께 결과 반환
// return ResponseEntity.ok(successResponse);
// }

/**
* 로그아웃 API
* 로그아웃하여 Refresh 토큰을 삭제합니다.
*
* @param request HTTP 요청 객체
* @param response HTTP 응답 객체
* @return 로그아웃 및 토큰 삭제 결과를 반환합니다.
*/
@Operation(
summary = "로그아웃 및 토큰 만료",
description = "사용자가 로그아웃할 때, 쿠키의 Refresh 토큰을 만료시켜 삭제합니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = SuccessResponseDTO.class))),
@ApiResponse(responseCode = "400", description = "로그아웃 요청이 잘못됨",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
})
@DeleteMapping("/logout")
public ResponseEntity<SuccessResponseDTO<String>> removeToken(
HttpServletRequest request,
HttpServletResponse response
) {
// 액세스 토큰으로 현재 Redis 정보 삭제
tokenService.removeRefreshToken(request, response);

SuccessResponseDTO<String> successResponse = SuccessResponseDTO.<String>builder()
.status(SuccessCode.LOGOUT_SUCCESS.getHttpStatus().value())
.message(SuccessCode.LOGOUT_SUCCESS.getMessage())
.data(SuccessCode.LOGOUT_SUCCESS.name()) // data가 없을 때는 null로 설정
.build();

// 토큰 삭제 성공 응답 처리
return ResponseEntity.ok(successResponse);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.tico.pomoro_do.domain.user.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Schema(description = "JWT Response")
public class TokenDTO {

@Schema(description = "Access Token", nullable = false)
private String accessToken;

@Builder
public TokenDTO(String accessToken){
this.accessToken = accessToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,26 @@
import lombok.NoArgsConstructor;

@Entity
@Table(name = "account")
@Getter
@Table(name = "refresh")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account {
public class Refresh {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "account_id")
@Column(name = "refresh_id")
private Long id;

private String username;
private String password;
private String role;
@Column(name = "refresh_token")
private String refreshToken;
private String expiration;

// 생성자
@Builder
public Account(String username, String password, String role) {
public Refresh (String username, String refreshToken, String expiration){
this.username = username;
this.password = password;
this.role = role;
this.refreshToken = refreshToken;
this.expiration = expiration;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.tico.pomoro_do.domain.user.repository;

import com.tico.pomoro_do.domain.user.entity.Refresh;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;

public interface RefreshRepository extends JpaRepository<Refresh, Long> {

// 해당 리프레쉬 토큰의 존재 여부를 판단하는 메소드
Boolean existsByRefreshToken(String refreshToken);

// 해당 리프레쉬 토큰을 삭제하는 메소드
@Transactional
void deleteByRefreshToken(String refreshToken);

// 사용자 이름을 기준으로 모든 리프레쉬 토큰을 삭제하는 메소드
@Transactional
void deleteByUsername(String username);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import com.tico.pomoro_do.domain.user.dto.GoogleUserInfoDTO;
import com.tico.pomoro_do.domain.user.dto.request.GoogleJoinDTO;
import com.tico.pomoro_do.domain.user.dto.response.JwtDTO;
import com.tico.pomoro_do.domain.user.dto.response.TokenDTO;
import com.tico.pomoro_do.domain.user.entity.User;
import com.tico.pomoro_do.global.enums.TokenType;
import com.tico.pomoro_do.global.enums.UserRole;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.security.GeneralSecurityException;
Expand All @@ -32,5 +35,6 @@ public interface AuthService {
JwtDTO createJwtTokens(String email, String role);

// Refresh 토큰으로 Access토큰 발급
TokenDTO reissueToken(HttpServletRequest request, HttpServletResponse response);

}
Loading

0 comments on commit d1181ca

Please sign in to comment.