Skip to content

Commit

Permalink
Merge pull request #98 from Central-MakeUs/dev
Browse files Browse the repository at this point in the history
[Refactor] refresh token 재발급 api 분리
  • Loading branch information
dainnida authored Feb 12, 2025
2 parents 1255b12 + 8771a1c commit df8bfaa
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers("/login/**", "/oauth2/**").permitAll()
// 도서 검색, 사용자 api, health check
.requestMatchers("/api/books/search", "/api/users/**", "/api/health").permitAll()
// access token 없어도 호출 가능해야 함
.requestMatchers("/api/auth/refresh").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.cmc.mercury.global.controller;

import com.cmc.mercury.domain.user.entity.User;
import com.cmc.mercury.global.exception.CustomException;
import com.cmc.mercury.global.exception.ErrorCode;
import com.cmc.mercury.global.jwt.JwtProvider;
import com.cmc.mercury.global.response.SuccessResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Tag(name = "AuthController", description = "토큰 발급 API")
@Slf4j
public class AuthController {

private final JwtProvider jwtProvider;

@Value("${jwt.refresh-token-validity}")
private long refreshTokenValidity;

@PostMapping("/refresh")
@Operation(summary = "refresh token 재발급", description = "access token 만료 시 refresh token을 통해 재발급을 요청합니다.")
public SuccessResponse<?> refreshAccessToken(
@CookieValue(value = "refresh_token", required = false) String refreshToken, HttpServletResponse response) {

log.info("Refresh Token을 이용한 Access Token 갱신 요청");

// Refresh Token 갱신
String newRefreshToken = jwtProvider.refreshToken(refreshToken);

// 새로운 Access Token 생성
User user = jwtProvider.getUserFromToken(refreshToken);
String newAccessToken = jwtProvider.createAccessToken(user.getId(), user.getEmail());

// 새로운 Access Token을 헤더에 추가
response.setHeader("Authorization", "Bearer " + newAccessToken);

// 새로운 Refresh Token을 쿠키에 설정
Cookie refreshTokenCookie = new Cookie("refresh_token", newRefreshToken);
refreshTokenCookie.setHttpOnly(true); // JavaScript에서 접근 방지
// refreshTokenCookie.setSecure(true); // HTTPS만 허용
refreshTokenCookie.setPath("/"); // 모든 경로에서 접근 가능
// refreshTokenCookie.setDomain("mercuryplanet.co.kr"); // 도메인 간 쿠키 공유
refreshTokenCookie.setMaxAge((int) refreshTokenValidity / 1000); // ms를 초 단위로 변환
response.addCookie(refreshTokenCookie);

return SuccessResponse.ok(new HashMap<>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public enum ErrorCode {
EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Jwt401", "만료된 access 토큰입니다."),
EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "Jwt401", "만료된 refresh 토큰입니다."),
TOKEN_TYPE_MISMATCH(HttpStatus.UNAUTHORIZED, "Jwt401", "토큰 타입이 일치하지 않습니다."),
EMPTY_TOKEN(HttpStatus.UNAUTHORIZED, "Jwt401", "토큰이 없습니다.");
EMPTY_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Jwt401", "access 토큰이 없습니다."),
EMPTY_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "Jwt401", "refresh 토큰이 없습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
Expand Down Expand Up @@ -47,7 +48,8 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
path.startsWith("/api-docs") ||
path.startsWith("/api/books/search") ||
path.startsWith("/api/users") ||
path.startsWith("/api/health");
path.startsWith("/api/health") ||
path.startsWith("/api/auth/refresh");
}

@Override
Expand All @@ -60,7 +62,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

// Access Token이 없음
if (!StringUtils.hasText(accessToken)) {
throw new CustomException(ErrorCode.EMPTY_TOKEN);
throw new CustomException(ErrorCode.EMPTY_ACCESS_TOKEN);
}

try {
Expand All @@ -76,48 +78,27 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

filterChain.doFilter(request, response);

return;

} catch (CustomException e) {
// Access Token이 만료된 경우, Refresh Token 확인
// Access Token이 만료된 경우, 바로 에러 반환
if (e.getErrorCode() == ErrorCode.EXPIRED_ACCESS_TOKEN) {
String refreshToken = extractRefreshToken(request);

if (StringUtils.hasText(refreshToken)) {
// Refresh Token이 유효한지 검증
jwtProvider.validateToken(refreshToken, "RefreshToken");
// Refresh Token이 DB에 저장된 것과 일치하는지 검증
jwtProvider.checkRefreshToken(refreshToken);

// Refresh Token이 유효하면 새로운 Access Token과 Refresh Token 발급
User user = jwtProvider.getUserFromToken(refreshToken);
String newAccessToken = jwtProvider.createAccessToken(user.getId(), user.getEmail());
String newRefreshToken = jwtProvider.createRefreshToken(user.getId(), user.getEmail());

// 새로운 Access Token을 헤더에 추가
response.setHeader("Authorization", "Bearer " + newAccessToken);

// 새로운 Refresh Token을 쿠키에 설정
Cookie refreshTokenCookie = new Cookie("refresh_token", newRefreshToken);
refreshTokenCookie.setHttpOnly(true); // JavaScript에서 접근 방지
// refreshTokenCookie.setSecure(true); // HTTPS만 허용
refreshTokenCookie.setPath("/"); // 모든 경로에서 접근 가능
// refreshTokenCookie.setDomain("mercuryplanet.co.kr"); // 도메인 간 쿠키 공유
refreshTokenCookie.setMaxAge((int) refreshTokenValidity / 1000); // ms를 초 단위로 변환
response.addCookie(refreshTokenCookie);

// Authentication 객체 생성해서 SecurityContext에 저장
Authentication authentication = createAuthentication(user);
SecurityContextHolder.getContext().setAuthentication(authentication);

filterChain.doFilter(request, response);

return;
}

response.setStatus(e.getErrorCode().getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");

ErrorResponse errorResponse = new ErrorResponse(e.getErrorCode());

response.getWriter().write(objectMapper.writeValueAsString(errorResponse));

return;
}
// 그 외 토큰 관련 에러
throw new CustomException(e.getErrorCode());
// 그 외 토큰 관련 에러 그대로 던짐
throw e;
}
} catch (CustomException e) {
SecurityContextHolder.clearContext();
SecurityContextHolder.clearContext(); // 최종적으로 인증이 실패한 경우 초기화

response.setStatus(e.getErrorCode().getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
Expand Down Expand Up @@ -163,4 +144,4 @@ private Authentication createAuthentication(User user) {
userDetails.getAuthorities()
);
}
}
}
33 changes: 29 additions & 4 deletions src/main/java/com/cmc/mercury/global/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Date;

Expand Down Expand Up @@ -121,16 +122,40 @@ public void checkRefreshToken(String token) {
User user = getUserFromToken(token);

String storedRefreshToken = user.getRefreshToken();
if (storedRefreshToken == null) {
// DB에 Refresh Token이 없는 경우 (이미 무효화된 경우)
throw new CustomException(ErrorCode.EXPIRED_REFRESH_TOKEN);
if (storedRefreshToken == null) {
// 사용자가 로그아웃한 경우
throw new CustomException(ErrorCode.EMPTY_REFRESH_TOKEN);
}

if (!token.equals(user.getRefreshToken())) {
/* if (!token.equals(user.getRefreshToken())) {
// DB의 Refresh Token과 일치하지 않으면 재사용 시도로 간주
user.updateRefreshToken(null); // DB의 Refresh Token 무효화
userRepository.save(user);
throw new CustomException(ErrorCode.EXPIRED_REFRESH_TOKEN);
}*/
if (!token.equals(storedRefreshToken)) {
throw new CustomException(ErrorCode.EXPIRED_REFRESH_TOKEN);
}
}

public String refreshToken(String refreshToken) {

// Refresh Token이 없음
if (!StringUtils.hasText(refreshToken)) {
throw new CustomException(ErrorCode.EMPTY_REFRESH_TOKEN);
}

// Refresh Token 검증
validateToken(refreshToken, "RefreshToken");
// DB에 저장된 Refresh Token과 비교하여 유효성 확인
checkRefreshToken(refreshToken);

// Refresh Token이 유효하면 새로운 Refresh Token 발급
User user = getUserFromToken(refreshToken);
String newRefreshToken = createRefreshToken(user.getId(), user.getEmail());
// 새 Refresh Token을 DB에 저장
updateRefreshToken(user.getId(), newRefreshToken);

return newRefreshToken;
}
}

0 comments on commit df8bfaa

Please sign in to comment.