From 085a1dba70d755e88be57d31cb52519a12b91afe Mon Sep 17 00:00:00 2001 From: CYY1007 Date: Sun, 6 Jul 2025 21:04:21 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/user/business/UserService.java | 39 +++++++++++++++++++ .../implementation/UserCommandAdapter.java | 5 +++ .../persistence/RefreshTokenRepository.java | 2 + .../api/user/presentation/UserApi.java | 11 ++++++ .../api/user/presentation/dto/ReAuthDto.java | 35 +++++++++++++++++ .../common/exception/base/UserException.java | 8 ++++ .../global/config/SwaggerConfig.java | 16 ++++---- .../security/config/SecurityConfig.java | 2 +- .../global/security/filter/JwtAuthFilter.java | 38 ++++++++++++------ .../security/provider/TokenProvider.java | 24 +++++++++++- src/main/resources/application.yml | 4 +- 11 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 src/main/java/rootbox/rootboxApp/api/user/presentation/dto/ReAuthDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/exception/base/UserException.java diff --git a/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java b/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java index cfba60b..2339948 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java +++ b/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java @@ -10,7 +10,10 @@ import rootbox.rootboxApp.api.user.implementation.UserCommandAdapter; import rootbox.rootboxApp.api.user.implementation.UserQueryAdapter; import rootbox.rootboxApp.api.user.presentation.dto.JoinDto; +import rootbox.rootboxApp.api.user.presentation.dto.ReAuthDto; import rootbox.rootboxApp.api.user.presentation.dto.SocialLoginDto; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; +import rootbox.rootboxApp.global.common.exception.base.UserException; import rootbox.rootboxApp.global.entity.RefreshToken; import rootbox.rootboxApp.global.entity.User; import rootbox.rootboxApp.global.entity.enums.user.SocialType; @@ -112,6 +115,42 @@ public JoinDto.JoinResponseDto join(JoinDto.JoinRequestDto request, User user) { return UserMapper.toJoinResponseDto(joinedUser); } + public ReAuthDto.ReGenerateAccessTokenDto reGenerateAccessToken(String socialId) { + + Optional refreshTokenByUserId = userQueryAdapter.findRefreshTokenByUserId(socialId); + Optional userBySocialId = userQueryAdapter.findUserBySocialId(socialId); + + if (userBySocialId.isEmpty()) + throw new UserException(GlobalErrorCode.USER_NOT_FOUND); + else { + String accessToken = tokenProvider.createAccessToken(userBySocialId.get(), List.of(new SimpleGrantedAuthority(UserRole.USER.name()))); + return ReAuthDto.ReGenerateAccessTokenDto.builder() + .accessToken(accessToken) + .refreshToken(refreshTokenByUserId.get().getRefreshToken()) + .build(); + + } + } + + @Transactional + public ReAuthDto.ReGenerateRefreshTokenDto reGenerateRefreshToken(String socialId) { + Optional userBySocialId = userQueryAdapter.findUserBySocialId(socialId); + + if (userBySocialId.isEmpty()) + throw new UserException(GlobalErrorCode.USER_NOT_FOUND); + else { + userCommandAdapter.deleteRefreshToken(socialId); + + String accessToken = tokenProvider.createAccessToken(userBySocialId.get(), List.of(new SimpleGrantedAuthority(UserRole.USER.name()))); + return ReAuthDto.ReGenerateRefreshTokenDto + .builder() + .accessToken(accessToken) + .refreshToken(userCommandAdapter.saveRefreshToken(tokenProvider.createRefreshToken(), + socialId).getRefreshToken()) + .build(); + } + } + private String generateUniqueNickname() { String name = ""; do { diff --git a/src/main/java/rootbox/rootboxApp/api/user/implementation/UserCommandAdapter.java b/src/main/java/rootbox/rootboxApp/api/user/implementation/UserCommandAdapter.java index 653c365..32055c9 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/implementation/UserCommandAdapter.java +++ b/src/main/java/rootbox/rootboxApp/api/user/implementation/UserCommandAdapter.java @@ -43,4 +43,9 @@ public RefreshToken saveRefreshToken(String refreshToken, String userSocialId){ public User joinUser(JoinDto.JoinRequestDto requestDto, User user){ return user.joinUser(requestDto); } + + public Void deleteRefreshToken(String socialId){ + refreshTokenRepository.deleteByUserSocialId(socialId); + return null; + } } diff --git a/src/main/java/rootbox/rootboxApp/api/user/persistence/RefreshTokenRepository.java b/src/main/java/rootbox/rootboxApp/api/user/persistence/RefreshTokenRepository.java index 517d3f3..8756328 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/persistence/RefreshTokenRepository.java +++ b/src/main/java/rootbox/rootboxApp/api/user/persistence/RefreshTokenRepository.java @@ -8,4 +8,6 @@ public interface RefreshTokenRepository extends JpaRepository { Optional findByUserSocialId(String userId); + + void deleteByUserSocialId(String userId); } diff --git a/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java b/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java index cbef33a..a94349d 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java +++ b/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*; import rootbox.rootboxApp.api.user.business.UserService; import rootbox.rootboxApp.api.user.presentation.dto.JoinDto; +import rootbox.rootboxApp.api.user.presentation.dto.ReAuthDto; import rootbox.rootboxApp.api.user.presentation.dto.SocialLoginDto; import rootbox.rootboxApp.global.common.CommonResponse; import rootbox.rootboxApp.global.entity.User; @@ -50,6 +51,16 @@ public CommonResponse checkNickName(@Reque JoinDto.JoinNickNameCheckResponseDto.builder().useYn(!userService.checkNickname(nickname)).build()); } + @GetMapping("/accessToken") + public CommonResponse reGenerateAccessToken(@RequestParam(name = "userSocialId") String userSocialId) { + return CommonResponse.onSuccess(userService.reGenerateAccessToken(userSocialId)); + } + + @GetMapping("/auth/refreshToken") + public CommonResponse reGenerateRefreshToken(@RequestParam(name = "userSocialId") String userSocialId) { + return CommonResponse.onSuccess(userService.reGenerateRefreshToken(userSocialId)); + } + @PatchMapping("/") public CommonResponse joinUser(@RequestBody @Valid JoinDto.JoinRequestDto requestDto, @AuthMember @Parameter(hidden = true) User user) { return CommonResponse.onSuccess(userService.join(requestDto, user)); diff --git a/src/main/java/rootbox/rootboxApp/api/user/presentation/dto/ReAuthDto.java b/src/main/java/rootbox/rootboxApp/api/user/presentation/dto/ReAuthDto.java new file mode 100644 index 0000000..f069c7f --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/api/user/presentation/dto/ReAuthDto.java @@ -0,0 +1,35 @@ +package rootbox.rootboxApp.api.user.presentation.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.*; + +public class ReAuthDto { + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ReGenerateAccessTokenDto { + + @NotNull + String accessToken; + + @NotNull + String refreshToken; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ReGenerateRefreshTokenDto { + + @NotNull + String accessToken; + + @NotNull + String refreshToken; + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/base/UserException.java b/src/main/java/rootbox/rootboxApp/global/common/exception/base/UserException.java new file mode 100644 index 0000000..ce7d68c --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/base/UserException.java @@ -0,0 +1,8 @@ +package rootbox.rootboxApp.global.common.exception.base; + +public class UserException extends GeneralException{ + + public UserException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/config/SwaggerConfig.java b/src/main/java/rootbox/rootboxApp/global/config/SwaggerConfig.java index 51dbf2b..a7b99bf 100644 --- a/src/main/java/rootbox/rootboxApp/global/config/SwaggerConfig.java +++ b/src/main/java/rootbox/rootboxApp/global/config/SwaggerConfig.java @@ -22,26 +22,28 @@ public OpenAPI SpringCodeBaseAPI() { final String REFRESH_SCHEME_NAME = "Refresh Token"; Components components = new Components() + // Authorization 헤더용 Access Token (bearer auth) .addSecuritySchemes(ACCESS_SCHEME_NAME, new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) .name("Authorization") - .in(SecurityScheme.In.HEADER)) + ) + // Refresh 헤더용 Refresh Token (apikey 방식) .addSecuritySchemes(REFRESH_SCHEME_NAME, new SecurityScheme() - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .name("Refresh") // 예: 헤더 키를 다르게 설정할 수 있음 - .in(SecurityScheme.In.HEADER)); + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name("Refresh") + ); return new OpenAPI() .info(info) .components(components) .addServersItem(new Server().url("/")) - // 여기 두 개 모두 SecurityRequirement에 등록 + // 원하는 경우 둘 다 글로벌로 적용 가능. 필요 시 각 API에 개별 지정 가능 .addSecurityItem(new SecurityRequirement().addList(ACCESS_SCHEME_NAME)) .addSecurityItem(new SecurityRequirement().addList(REFRESH_SCHEME_NAME)); } diff --git a/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java b/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java index 5214d46..51d40fe 100644 --- a/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java +++ b/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java @@ -36,7 +36,7 @@ public class SecurityConfig { new JwtAuthenticationExceptionHandler(); private static final String[] whiteList = { - "/users/auth/nickname", "/users/auth/kakao/test", "/users/auth/kakao/code", "/users/auth/kakao", "/users/auth/health" + "/users/auth/nickname", "/users/auth/kakao/test", "/users/auth/kakao/code", "/users/auth/kakao", "/users/auth/health", "/users/auth/refreshToken" }; /** diff --git a/src/main/java/rootbox/rootboxApp/global/security/filter/JwtAuthFilter.java b/src/main/java/rootbox/rootboxApp/global/security/filter/JwtAuthFilter.java index 35944b3..8da5b05 100644 --- a/src/main/java/rootbox/rootboxApp/global/security/filter/JwtAuthFilter.java +++ b/src/main/java/rootbox/rootboxApp/global/security/filter/JwtAuthFilter.java @@ -35,9 +35,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse throws ServletException, IOException { log.info("jwt 인증 시작, access token 인증 헤더 정보 : {}, refresh 토큰 인증 헤더 정보 : {}", request.getHeader("Authorization"), request.getHeader("Refresh")); +// Enumeration headerNames = request.getHeaderNames(); +// while (headerNames.hasMoreElements()) { +// String headerName = headerNames.nextElement(); +// String headerValue = request.getHeader(headerName); +// log.info("인증 헤더 모두 null이기 때문에 헤더 정보 다 출력 => {} : {}", headerName, headerValue); +// } if (request.getHeader("Authorization") == null && request.getHeader("Refresh") == null) { - Enumeration headerNames = request.getHeaderNames(); +// Enumeration headerNames = request.getHeaderNames(); // while (headerNames.hasMoreElements()) { // String headerName = headerNames.nextElement(); @@ -51,21 +57,31 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (accessToken == null) { accessToken = tokenProvider.resolveToken(request, "Refresh"); - } + if (StringUtils.hasText(accessToken) && tokenProvider.validateRefreshToken(accessToken)){ + Authentication authentication = tokenProvider.getRefreshAuthentication(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + }else{ + SecurityContextHolder.getContext().setAuthentication(null); + } + // 다음 단계 실행 -> 다른 필터 및 컨트롤러 실행 + filterChain.doFilter(request,response); - // 토큰이 있다면 진행 - if(StringUtils.hasText(accessToken) && tokenProvider.validateToken(accessToken)) { + }else { - Authentication authentication = tokenProvider.getAuthentication(accessToken); - SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 SecurityContext에 설정 + // 토큰이 있다면 진행 + if (StringUtils.hasText(accessToken) && tokenProvider.validateToken(accessToken)) { + + Authentication authentication = tokenProvider.getAuthentication(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 SecurityContext에 설정 + + } else { + SecurityContextHolder.getContext().setAuthentication(null); + } + // 다음 단계 실행 -> 다른 필터 및 컨트롤러 실행 + filterChain.doFilter(request, response); } - else{ - SecurityContextHolder.getContext().setAuthentication(null); - } - // 다음 단계 실행 -> 다른 필터 및 컨트롤러 실행 - filterChain.doFilter(request,response); } diff --git a/src/main/java/rootbox/rootboxApp/global/security/provider/TokenProvider.java b/src/main/java/rootbox/rootboxApp/global/security/provider/TokenProvider.java index 437a2f8..d73266e 100644 --- a/src/main/java/rootbox/rootboxApp/global/security/provider/TokenProvider.java +++ b/src/main/java/rootbox/rootboxApp/global/security/provider/TokenProvider.java @@ -18,6 +18,7 @@ import rootbox.rootboxApp.global.entity.User; import java.security.Key; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Base64; import java.util.Collection; @@ -69,6 +70,9 @@ public String resolveToken(HttpServletRequest request, String tokenType) { } String token = request.getHeader(headerName); + + if (tokenType.equals("Refresh")) + return token; if (StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX)) { return token.substring(7); } @@ -117,6 +121,11 @@ public boolean validateToken(String token) { } catch (SecurityException | MalformedJwtException e) { log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); } catch (ExpiredJwtException e) { + Claims expiredClaims = e.getClaims(); + Date exp = expiredClaims.getExpiration(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + log.info("access 토큰 만료일자 : {}", sdf.format(exp)); + log.info("Expired JWT token, 만료된 JWT token 입니다."); throw new JwtAuthenticationException(GlobalErrorCode.TOKEN_EXPIRED); @@ -131,13 +140,19 @@ public boolean validateToken(String token) { return false; } - public void validateRefreshToken(String refreshToken){ + public boolean validateRefreshToken(String refreshToken){ try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken); + return true; } catch (SecurityException | MalformedJwtException e) { log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); throw new JwtAuthenticationException(GlobalErrorCode.INVALID_TOKEN); } catch (ExpiredJwtException e) { + Claims expiredClaims = e.getClaims(); + Date issuedAt = expiredClaims.getIssuedAt(); + Date exp = expiredClaims.getExpiration(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + log.info("리프레시 토큰 생성일자 : {}, 리프레시 토큰 만료일자 : {}", sdf.format(issuedAt),sdf.format(exp)); log.info("Expired JWT token, 만료된 JWT 리프레시 token 입니다."); throw new JwtAuthenticationException(GlobalErrorCode.REFRESH_TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { @@ -175,4 +190,11 @@ public Authentication getAuthentication(String token){ return new UsernamePasswordAuthenticationToken(principal, token, authorities); } + public Authentication getRefreshAuthentication(String token){ + Claims claims = + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); + + return new UsernamePasswordAuthenticationToken(null, token, null); + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 30d2a7e..d92c236 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -142,8 +142,8 @@ jwt: key: secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret # secret : ${JWT_SECRET} authorities-key: authoritiesKey - access-token-validity-in-seconds: 300000 # 15 days - refresh-token-validity-in-seconds: 5184000000 # 60 days + access-token-validity-in-seconds: 3000 # 15 days + refresh-token-validity-in-seconds: 1200000 # 60 days oauth: kakao: baseUrl: ${KAKAO_BASE_URL}