Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.moongeul.backend.api.member.dto.LoginResponseDTO;
import com.moongeul.backend.api.member.dto.LoginRequestDTO;
import com.moongeul.backend.api.member.dto.UserInfoDTO;
import com.moongeul.backend.api.member.jwt.dto.JwtTokenDTO;
import com.moongeul.backend.api.member.service.MemberService;
import com.moongeul.backend.common.response.ApiResponse;
import com.moongeul.backend.common.response.SuccessStatus;
Expand Down Expand Up @@ -71,5 +72,20 @@ public ResponseEntity<ApiResponse<UserInfoDTO>> getUserInfo(@AuthenticationPrinc
UserInfoDTO response = memberService.getUserInfo(userDetails.getUsername());
return ApiResponse.success(SuccessStatus.GET_USERINFO_SUCCESS, response);
}

@Operation(
summary = "토큰 재발급 API",
description = "엑세스 토큰 만료 시, 유효한 리프레시 토큰을 통해 엑세스 토큰을 재발급 받습니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "토큰 재발급 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "토큰이 만료되었거나 유효하지 않은 토큰입니다."),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 사용자를 찾을 수 없습니다.")
})
@GetMapping("/reissue-token")
public ResponseEntity<ApiResponse<JwtTokenDTO>> reissueAccessToken(@RequestHeader(value = "Authorization-Refresh") String refreshToken){
JwtTokenDTO response = memberService.reissueToken(refreshToken);
return ApiResponse.success(SuccessStatus.REISSUE_TOKEN_SUCCESS, response);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);

Optional<Member> findBySocialId(String socialId);

Optional<Member> findByRefreshToken(String refreshToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.moongeul.backend.api.member.repository.MemberRepository;
import com.moongeul.backend.common.config.jwt.JwtTokenProvider;
import com.moongeul.backend.common.exception.NotFoundException;
import com.moongeul.backend.common.exception.UnauthorizedException;
import com.moongeul.backend.common.response.ErrorStatus;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -117,4 +118,27 @@ private Member getMemberByEmail(String email) {
return memberRepository.findByEmail(email)
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));
}

@Transactional
public JwtTokenDTO reissueToken(String refreshToken){

// 1. Refresh Token 유효성 검증
if(!jwtTokenProvider.validateToken(refreshToken)){
throw new UnauthorizedException(ErrorStatus.TOKEN_UNAUTHORIZED.getMessage());
}

// 2. DB에서 해당 Refresh Token을 가진 회원 찾기
Member member = memberRepository.findByRefreshToken(refreshToken)
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));

// 3. 토큰 재발급 (Access/Refresh)
JwtTokenDTO jwtToken = jwtTokenProvider.generateToken(member);

// 4. DB에 새로 발급한 Refresh Token 저장
member.updateRefreshToken(jwtToken.getRefreshToken());
memberRepository.save(member);

// 5. 재발급 토큰 반환
return jwtToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,25 @@ public JwtTokenDTO generateToken(Member member) {
}

// 토큰 정보를 검증하는 메서드
// 3. 신분증 검사 : 제출하는 액세스 토큰이 진짜인지 확인
// 3. 신분증 검사 : 제출하는 토큰이 진짜인지 확인
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(key)
.verifyWith(key) // 서명(signature) 검증 : Header + Payload를 key로 다시 서명하여 생성한 값과 토큰의 Signature를 비교
.build()
.parseSignedClaims(token);
.parseSignedClaims(token); // 만료 시간 확인 : Payload의 exp (expiration) claim과 현재 시간 비교
return true;
} catch (SecurityException | MalformedJwtException e) {
// 서명이 잘못되었거나 JWT 형식이 올바르지 않음
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
// 토큰이 만료됨 (exp < 현재시간)
log.info("Expired JWT Token", e);
} catch (UnsupportedJwtException e) {
// 지원하지 않는 서명 알고리즘 또는 JWT 타입
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
// 토큰이 null이거나 빈 문자열
log.info("JWT claims string is empty.", e);
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 무상태 설정
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable())) //h2-console 화면 깨짐 방지(iframe 렌더링 오류)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/api/v2/member/google/login", "/api/v2/member/kakao/login", "/h2-console/**").permitAll()
.requestMatchers("/", "/h2-console/**").permitAll()
.requestMatchers("/v3/api-docs/**", "/api-doc/**", "/swagger-ui/**").permitAll()
.requestMatchers("/api/v2/member/google/login", "/api/v2/member/kakao/login", "api/v2/member/reissue-token").permitAll()
.anyRequest().authenticated()
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public enum ErrorStatus {
* 401 UNAUTHORIZED
*/
AUTH_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "유효하지 않은 인가코드 입니다."),
TOKEN_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었거나 유효하지 않은 토큰입니다."),

/**
* 404 NOT_FOUND
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public enum SuccessStatus {
*/
SEND_HEALTH_CHECK_SUCCESS(HttpStatus.OK,"서버 상태 체크 성공"),
SEND_LOGIN_SUCCESS(HttpStatus.OK, "로그인 성공"),
GET_USERINFO_SUCCESS(HttpStatus.OK, "사용자 정보 조회 성공")
GET_USERINFO_SUCCESS(HttpStatus.OK, "사용자 정보 조회 성공"),
REISSUE_TOKEN_SUCCESS(HttpStatus.OK, "토큰 재발급 성공")

/**
* 201
Expand Down