From 8b7aab494a358a996606de92aecb344f5a8925e0 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 7 Aug 2025 03:20:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20cors=EC=84=A4=EC=A0=95=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Authorization=20=ED=97=A4=EB=8D=94=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/seeat/server/security/config/CorsConfig.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/seeat/server/security/config/CorsConfig.java b/src/main/java/com/seeat/server/security/config/CorsConfig.java index 4c1398e7..c00fa3c1 100644 --- a/src/main/java/com/seeat/server/security/config/CorsConfig.java +++ b/src/main/java/com/seeat/server/security/config/CorsConfig.java @@ -35,6 +35,8 @@ public CorsConfigurationSource corsConfigurationSource() { configuration.addAllowedMethod("*"); configuration.setAllowCredentials(true); + configuration.addExposedHeader("Authorization"); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; From 5b216c0efae13677ca480281c66e6f584bcfd727 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 7 Aug 2025 04:07:29 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20refreshToken=EC=9D=84=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20accessToken=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/TokenResponse.java | 11 ++++ .../auth/presentation/AuthController.java | 60 +++++++++++++++++++ .../swagger/AuthControllerSpec.java | 16 +++++ .../server/global/service/RedisService.java | 13 ++++ .../security/config/RequestMatcherHolder.java | 3 + .../security/jwt/service/TokenService.java | 21 +++++++ 6 files changed, 124 insertions(+) create mode 100644 src/main/java/com/seeat/server/domain/auth/application/dto/response/TokenResponse.java create mode 100644 src/main/java/com/seeat/server/domain/auth/presentation/AuthController.java create mode 100644 src/main/java/com/seeat/server/domain/auth/presentation/swagger/AuthControllerSpec.java diff --git a/src/main/java/com/seeat/server/domain/auth/application/dto/response/TokenResponse.java b/src/main/java/com/seeat/server/domain/auth/application/dto/response/TokenResponse.java new file mode 100644 index 00000000..f908008c --- /dev/null +++ b/src/main/java/com/seeat/server/domain/auth/application/dto/response/TokenResponse.java @@ -0,0 +1,11 @@ +package com.seeat.server.domain.auth.application.dto.response; + +/** + * accessToken 응답입니다. + * + * @param accessToken accessToken + */ +public record TokenResponse( + String accessToken +) { +} diff --git a/src/main/java/com/seeat/server/domain/auth/presentation/AuthController.java b/src/main/java/com/seeat/server/domain/auth/presentation/AuthController.java new file mode 100644 index 00000000..1e85f4ee --- /dev/null +++ b/src/main/java/com/seeat/server/domain/auth/presentation/AuthController.java @@ -0,0 +1,60 @@ +package com.seeat.server.domain.auth.presentation; + +import com.seeat.server.domain.auth.application.dto.response.TokenResponse; +import com.seeat.server.domain.auth.presentation.swagger.AuthControllerSpec; +import com.seeat.server.domain.user.domain.entity.User; +import com.seeat.server.global.response.ApiResponse; +import com.seeat.server.global.response.CustomException; +import com.seeat.server.global.response.ErrorCode; +import com.seeat.server.global.util.JwtConstants; +import com.seeat.server.security.jwt.service.TokenService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +public class AuthController implements AuthControllerSpec { + + private final TokenService tokenService; + + @GetMapping + public ApiResponse getAccessToken(HttpServletRequest request){ + + // 클라이언트 쿠키에서 refreshToken 추출 + String refreshToken = extractRefreshTokenFromCookies(request.getCookies()); + + // 사용자 정보 조회 + Optional user = tokenService.getUserFromRefreshToken(refreshToken); + if (user.isEmpty()) { + + return ApiResponse.fail(new CustomException(ErrorCode.NOT_USER, null)); + } + + // 새로운 accessToken 생성 + String newAccessToken = tokenService.generateAccessToken(user.get()); + + // accessToken 응답 + return ApiResponse.ok(new TokenResponse(newAccessToken)); + } + + private String extractRefreshTokenFromCookies(Cookie[] cookies) { + // 쿠키에서 refreshToken 추출 + if (cookies == null) return null; + + for (Cookie cookie : cookies) { + if (JwtConstants.REFRESH_TOKEN_COOKIE.equals(cookie.getName())) { + // 쿠키 값 반환 + return cookie.getValue(); + } + } + // 없으면 null + return null; + } +} diff --git a/src/main/java/com/seeat/server/domain/auth/presentation/swagger/AuthControllerSpec.java b/src/main/java/com/seeat/server/domain/auth/presentation/swagger/AuthControllerSpec.java new file mode 100644 index 00000000..bd9433cc --- /dev/null +++ b/src/main/java/com/seeat/server/domain/auth/presentation/swagger/AuthControllerSpec.java @@ -0,0 +1,16 @@ +package com.seeat.server.domain.auth.presentation.swagger; + +import com.seeat.server.domain.auth.application.dto.response.TokenResponse; +import com.seeat.server.global.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.GetMapping; + +@Tag(name = "토큰 조회 API", description = "토큰 조회하는 API 입니다.") +public interface AuthControllerSpec { + @Operation(summary = "AccessToken 조회", description = "RefreshToken 기반 AccessToken 재발급") + @GetMapping + ApiResponse getAccessToken(HttpServletRequest request); + +} diff --git a/src/main/java/com/seeat/server/global/service/RedisService.java b/src/main/java/com/seeat/server/global/service/RedisService.java index 46891323..44dacb70 100644 --- a/src/main/java/com/seeat/server/global/service/RedisService.java +++ b/src/main/java/com/seeat/server/global/service/RedisService.java @@ -10,6 +10,8 @@ import java.time.Duration; import java.util.Map; +import java.util.Set; + /** * Redis와의 상호작용을 담당하는 서비스 클래스 * @@ -44,6 +46,17 @@ public T getValues(String key, Class clazz) { return objectMapper.convertValue(value, clazz); } + public boolean existsRefreshToken(String token) { + Set keys = redisTemplate.keys(REFRESH_TOKEN_PREFIX + "*"); + if (keys == null || keys.isEmpty()) return false; + + ValueOperations values = redisTemplate.opsForValue(); + + return keys.stream() + .map(values::get) + .anyMatch(value -> token.equals(value)); + } + public void deleteValues(String key) { redisTemplate.delete(key); } diff --git a/src/main/java/com/seeat/server/security/config/RequestMatcherHolder.java b/src/main/java/com/seeat/server/security/config/RequestMatcherHolder.java index a68c7cb3..c3f408b2 100644 --- a/src/main/java/com/seeat/server/security/config/RequestMatcherHolder.java +++ b/src/main/java/com/seeat/server/security/config/RequestMatcherHolder.java @@ -74,6 +74,9 @@ public class RequestMatcherHolder { new RequestInfo(DELETE, "/api/v1/search", null), new RequestInfo(GET, "/api/v1/search/**", null), + // 토큰 조회 관련 + new RequestInfo(GET, "/api/v1/auth", null), + // static resources new RequestInfo(GET, "/docs/**", null), new RequestInfo(GET, "/*.ico", null), diff --git a/src/main/java/com/seeat/server/security/jwt/service/TokenService.java b/src/main/java/com/seeat/server/security/jwt/service/TokenService.java index 29414f5a..746cb064 100644 --- a/src/main/java/com/seeat/server/security/jwt/service/TokenService.java +++ b/src/main/java/com/seeat/server/security/jwt/service/TokenService.java @@ -10,12 +10,14 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.Arrays; +import java.util.Optional; /** * 토큰 발급 서비스 @@ -80,4 +82,23 @@ public void generateDevTokensAndSetHeaders(Long userId, String username, UserRol response.addCookie(refreshTokenCookie); } + + public Optional getUserFromRefreshToken(String refreshToken) { + if (refreshToken == null || !redisService.existsRefreshToken(refreshToken)) { + return Optional.empty(); + } + + if (!jwtProvider.validateToken(refreshToken)) { + return Optional.empty(); + } + + Authentication authentication = jwtProvider.getAuthentication(refreshToken); + User user = (User) authentication.getPrincipal(); + + return Optional.ofNullable(user); + } + + public String generateAccessToken(User user) { + return jwtProvider.generateAccessToken(user); + } }