From 80bea58953dba8da3106f2de09863c52ecabb60f Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:38:46 +0900 Subject: [PATCH 01/11] =?UTF-8?q?[refactor]=20argument=5Fresover=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20->=20resolver=EB=A1=9C=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthTokenArgumentResolver.java | 2 +- .../Oauth2IdArgumentResolver.java | 2 +- .../{argument_resolver => resolver}/UserIdArgumentResolver.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/konkuk/thip/common/security/{argument_resolver => resolver}/AuthTokenArgumentResolver.java (96%) rename src/main/java/konkuk/thip/common/security/{argument_resolver => resolver}/Oauth2IdArgumentResolver.java (96%) rename src/main/java/konkuk/thip/common/security/{argument_resolver => resolver}/UserIdArgumentResolver.java (96%) diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java b/src/main/java/konkuk/thip/common/security/resolver/AuthTokenArgumentResolver.java similarity index 96% rename from src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java rename to src/main/java/konkuk/thip/common/security/resolver/AuthTokenArgumentResolver.java index 672b12bf3..c8fa81803 100644 --- a/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/AuthTokenArgumentResolver.java @@ -1,4 +1,4 @@ -package konkuk.thip.common.security.argument_resolver; +package konkuk.thip.common.security.resolver; import jakarta.servlet.http.HttpServletRequest; import konkuk.thip.common.exception.AuthException; diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java b/src/main/java/konkuk/thip/common/security/resolver/Oauth2IdArgumentResolver.java similarity index 96% rename from src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java rename to src/main/java/konkuk/thip/common/security/resolver/Oauth2IdArgumentResolver.java index 34d590e14..00bd17d23 100644 --- a/src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/Oauth2IdArgumentResolver.java @@ -1,4 +1,4 @@ -package konkuk.thip.common.security.argument_resolver; +package konkuk.thip.common.security.resolver; import jakarta.servlet.http.HttpServletRequest; import konkuk.thip.common.exception.AuthException; diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java b/src/main/java/konkuk/thip/common/security/resolver/UserIdArgumentResolver.java similarity index 96% rename from src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java rename to src/main/java/konkuk/thip/common/security/resolver/UserIdArgumentResolver.java index f2b8a7d06..b82954ee9 100644 --- a/src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/UserIdArgumentResolver.java @@ -1,4 +1,4 @@ -package konkuk.thip.common.security.argument_resolver; +package konkuk.thip.common.security.resolver; import jakarta.servlet.http.HttpServletRequest; import konkuk.thip.common.exception.AuthException; From 2e3b64e42f19bc01da5be6549d16a7b6aaf30727 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:52:48 +0900 Subject: [PATCH 02/11] =?UTF-8?q?[refactor]=20redirect=5Furl=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=A1=B8=EB=B2=84=20=EA=B5=AC=ED=98=84=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomAuthorizationRequestResolver.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java diff --git a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java new file mode 100644 index 000000000..8e2974e64 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java @@ -0,0 +1,58 @@ +package konkuk.thip.common.security.resolver; + +import jakarta.servlet.http.HttpServletRequest; +import konkuk.thip.config.properties.WebDomainProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_SESSION_KEY; +import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_URL_KEY; + +@Slf4j +@RequiredArgsConstructor +public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + + private final OAuth2AuthorizationRequestResolver delegate; + private final WebDomainProperties webDomainProperties; + + public CustomAuthorizationRequestResolver(ClientRegistrationRepository repo, + String authorizationRequestBaseUri, + WebDomainProperties props) { + this.delegate = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri); + this.webDomainProperties = props; + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + OAuth2AuthorizationRequest base = delegate.resolve(request); + return customize(request, base); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { + OAuth2AuthorizationRequest base = delegate.resolve(request, clientRegistrationId); + return customize(request, base); + } + + private OAuth2AuthorizationRequest customize(HttpServletRequest request, OAuth2AuthorizationRequest base) { + if (base == null) return null; + + String redirectUrl = request.getParameter(REDIRECT_URL_KEY.getValue()); + Map additional = new HashMap<>(base.getAdditionalParameters()); + if (StringUtils.hasText(redirectUrl) && webDomainProperties.isAllowed(redirectUrl)) { + request.getSession(true).setAttribute(REDIRECT_SESSION_KEY.getValue(), redirectUrl); + } + + return OAuth2AuthorizationRequest.from(base) + .additionalParameters(additional) + .build(); + } +} \ No newline at end of file From 9d428ff94c32a17611124bba03a045ad6eb9fb85 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:52:59 +0900 Subject: [PATCH 03/11] =?UTF-8?q?[refactor]=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20properties=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=84=A0=EC=96=B8=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../properties/WebDomainProperties.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/konkuk/thip/config/properties/WebDomainProperties.java diff --git a/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java b/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java new file mode 100644 index 000000000..cf45ec711 --- /dev/null +++ b/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java @@ -0,0 +1,29 @@ +package konkuk.thip.config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "server") +public class WebDomainProperties { + + private List webDomainUrls = new ArrayList<>(); + + public boolean isAllowed(String target) { + try { + URI uri = URI.create(target); + String origin = uri.getScheme() + "://" + uri.getHost() + (uri.getPort() > 0 ? ":" + uri.getPort() : ""); + return webDomainUrls.stream().anyMatch(o -> o.equalsIgnoreCase(origin)); + } catch (Exception e) { + return false; + } + } +} From fe121b3ef77be14266cddf0980ca99a68526cee5 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:53:25 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[refactor]=20Security=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=97=90=EC=84=9C=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20resolver=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/config/SecurityConfig.java | 39 ++++++++++++------- .../java/konkuk/thip/config/WebMvcConfig.java | 6 +-- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/main/java/konkuk/thip/config/SecurityConfig.java b/src/main/java/konkuk/thip/config/SecurityConfig.java index 9ee8472fe..4bfac31bc 100644 --- a/src/main/java/konkuk/thip/config/SecurityConfig.java +++ b/src/main/java/konkuk/thip/config/SecurityConfig.java @@ -5,6 +5,8 @@ import konkuk.thip.common.security.filter.JwtAuthenticationFilter; import konkuk.thip.common.security.oauth2.CustomOAuth2UserService; import konkuk.thip.common.security.oauth2.CustomSuccessHandler; +import konkuk.thip.common.security.resolver.CustomAuthorizationRequestResolver; +import konkuk.thip.config.properties.WebDomainProperties; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -16,6 +18,8 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -32,12 +36,6 @@ @RequiredArgsConstructor public class SecurityConfig { - @Value("${server.web-url}") - private String webUrl; - - @Value("${server.web-domain-url}") - private String webDomainUrl; - @Value("${server.https-url}") private String prodServerUrl; @@ -49,9 +47,22 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final CustomSuccessHandler customSuccessHandler; + private final ClientRegistrationRepository clientRegistrationRepository; + private final WebDomainProperties webDomainProperties; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // redirect_url(origin) → additionalParameters.return_to 저장 + var resolver = new CustomAuthorizationRequestResolver( + clientRegistrationRepository, + "/oauth2/authorization", + webDomainProperties + ); + + // 세션 저장소 + var authReqRepo = new HttpSessionOAuth2AuthorizationRequestRepository(); + http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) @@ -59,6 +70,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .httpBasic(AbstractHttpConfigurer::disable) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .oauth2Login((oauth2) -> oauth2 + .authorizationEndpoint(authorizationEndpointConfig -> authorizationEndpointConfig + .authorizationRequestResolver(resolver) + .authorizationRequestRepository(authReqRepo) + ) .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig .userService(customOAuth2UserService) ) @@ -89,13 +104,11 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(List.of( - "http://localhost:5173", - webUrl, - webDomainUrl, - prodServerUrl, - devServerUrl - )); + + List allowedOrigins = webDomainProperties.getWebDomainUrls(); + allowedOrigins.addAll(List.of(prodServerUrl, devServerUrl)); + + config.setAllowedOrigins(allowedOrigins); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(Collections.singletonList("*")); config.setAllowCredentials(true); diff --git a/src/main/java/konkuk/thip/config/WebMvcConfig.java b/src/main/java/konkuk/thip/config/WebMvcConfig.java index 44a410d9e..b4b8a3256 100644 --- a/src/main/java/konkuk/thip/config/WebMvcConfig.java +++ b/src/main/java/konkuk/thip/config/WebMvcConfig.java @@ -1,8 +1,8 @@ package konkuk.thip.config; -import konkuk.thip.common.security.argument_resolver.AuthTokenArgumentResolver; -import konkuk.thip.common.security.argument_resolver.Oauth2IdArgumentResolver; -import konkuk.thip.common.security.argument_resolver.UserIdArgumentResolver; +import konkuk.thip.common.security.resolver.AuthTokenArgumentResolver; +import konkuk.thip.common.security.resolver.Oauth2IdArgumentResolver; +import konkuk.thip.common.security.resolver.UserIdArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; From c5b31fc895d63d35d991c47eae0233b9a76a3940 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:53:43 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[refactor]=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B1=EA=B3=B5=EC=8B=9C=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=EC=A0=80=EC=9E=A5=ED=95=B4=EB=91=94=20red?= =?UTF-8?q?irect=5Furl=20=EB=B3=B5=EC=9B=90=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/exception/code/ErrorCode.java | 1 + .../security/constant/AuthParameters.java | 3 ++ .../security/oauth2/CustomSuccessHandler.java | 33 ++++++++++++++----- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 0628d8a6d..1a8ea98f7 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -27,6 +27,7 @@ public enum ErrorCode implements ResponseCode { JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50100, "JSON 직렬화/역직렬화에 실패했습니다."), AWS_BUCKET_BASE_URL_NOT_CONFIGURED(HttpStatus.INTERNAL_SERVER_ERROR, 50101, "aws s3 bucket base url 설정이 누락되었습니다."), + WEB_DOMAIN_ORIGIN_EMPTY(HttpStatus.INTERNAL_SERVER_ERROR, 50102, "허용된 웹 도메인 설정이 비어있습니다."), PERSISTENCE_TRANSACTION_REQUIRED(HttpStatus.INTERNAL_SERVER_ERROR, 50110, "@Transactional 컨텍스트가 필요합니다. 트랜잭션 범위 내에서만 사용할 수 있습니다."), diff --git a/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java b/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java index 688986eef..70c51d441 100644 --- a/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java +++ b/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java @@ -19,6 +19,9 @@ public enum AuthParameters { COOKIE_ACCESS_TOKEN("access_token"), COOKIE_TEMP_TOKEN("temp_token"), + REDIRECT_URL_KEY("redirect_url"), + REDIRECT_SESSION_KEY("oauth2_return_to"), + ; private final String value; diff --git a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java index 74e2faf98..f91341f14 100644 --- a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java +++ b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java @@ -3,31 +3,32 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.config.properties.WebDomainProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; import java.time.Duration; +import java.util.List; +import java.util.Objects; import java.util.UUID; -import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_HOME_URL; -import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_SIGNUP_URL; +import static konkuk.thip.common.security.constant.AuthParameters.*; @Slf4j @Component @RequiredArgsConstructor public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private static final int COOKIE_MAX_AGE = 60 * 60 * 24; // 1일 private final LoginTokenStorage loginTokenStorage; - @Value("${server.web-redirect-url}") - private String webRedirectUrl; + private final WebDomainProperties webDomainProperties; private final JwtUtil jwtUtil; @@ -38,6 +39,22 @@ public void onAuthenticationSuccess( Authentication authentication ) throws IOException, ServletException { + // Resolver에서 세션에 저장한 origin을 복원 + String webRedirectDomain = null; + if (request.getSession(false) != null) { + webRedirectDomain = (String) request.getSession(false).getAttribute(REDIRECT_SESSION_KEY.getValue()); + request.getSession(false).removeAttribute(REDIRECT_SESSION_KEY.getValue()); // 사용했으면 제거(일회성) + } + + // 허용 오리진 검증 및 폴백 + if (!webDomainProperties.isAllowed(Objects.toString(webRedirectDomain, ""))) { + List origins = webDomainProperties.getWebDomainUrls(); + if (origins == null || origins.isEmpty()) { + throw new AuthException(ErrorCode.WEB_DOMAIN_ORIGIN_EMPTY); + } + webRedirectDomain = origins.get(0); + } + CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); LoginUser loginUser = oAuth2User.getLoginUser(); @@ -48,7 +65,7 @@ public void onAuthenticationSuccess( String loginTokenKey = UUID.randomUUID().toString(); loginTokenStorage.put(loginTokenKey, TokenType.TEMP, tempToken, Duration.ofMinutes(5)); // ttl 5분 - getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL.getValue() + "?loginTokenKey=" + loginTokenKey); + getRedirectStrategy().sendRedirect(request, response, webRedirectDomain + REDIRECT_SIGNUP_URL.getValue() + "?loginTokenKey=" + loginTokenKey); } else { // 기존 유저 - 로그인용 액세스 토큰 String accessToken = jwtUtil.createAccessToken(loginUser.userId()); @@ -56,7 +73,7 @@ public void onAuthenticationSuccess( String loginTokenKey = UUID.randomUUID().toString(); loginTokenStorage.put(loginTokenKey, TokenType.ACCESS, accessToken, Duration.ofMinutes(5)); // ttl 5분 - getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL.getValue() + "?loginTokenKey=" + loginTokenKey); + getRedirectStrategy().sendRedirect(request, response, webRedirectDomain + REDIRECT_HOME_URL.getValue() + "?loginTokenKey=" + loginTokenKey); } } } From 3f9698143f71ba45422b4e536664f9c74742dcb2 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 23:24:37 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[refactor]=20LoginTokenStorage=EB=A5=BC?= =?UTF-8?q?=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EA=B8=B0=EB=B0=98=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Redis=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/oauth2/CustomSuccessHandler.java | 1 + .../security/oauth2/LoginTokenStorage.java | 48 ------------------- .../security/oauth2/auth/AuthController.java | 14 +++--- .../tokenstorage/LoginTokenStorage.java | 19 ++++++++ .../tokenstorage/MemoryLoginTokenStorage.java | 44 +++++++++++++++++ .../tokenstorage/RedisLoginTokenStorage.java | 48 +++++++++++++++++++ 6 files changed, 119 insertions(+), 55 deletions(-) delete mode 100644 src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java create mode 100644 src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/LoginTokenStorage.java create mode 100644 src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/MemoryLoginTokenStorage.java create mode 100644 src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java diff --git a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java index f91341f14..b0008a60e 100644 --- a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java +++ b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletResponse; import konkuk.thip.common.exception.AuthException; import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.common.security.oauth2.tokenstorage.LoginTokenStorage; import konkuk.thip.common.security.util.JwtUtil; import konkuk.thip.config.properties.WebDomainProperties; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java deleted file mode 100644 index fe664f313..000000000 --- a/src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java +++ /dev/null @@ -1,48 +0,0 @@ -package konkuk.thip.common.security.oauth2; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.time.Duration; -import java.time.Instant; -import java.util.concurrent.ConcurrentHashMap; - -@Component -@RequiredArgsConstructor -public class LoginTokenStorage { - - @Getter - public static final class Entry { - private final TokenType type; - private final String token; - private final long expireAtEpochMillis; - - private Entry(TokenType type, String token, long expireAtEpochMillis) { - this.type = type; - this.token = token; - this.expireAtEpochMillis = expireAtEpochMillis; - } - } - - private final ConcurrentHashMap store = new ConcurrentHashMap<>(); - - /** - * 토큰을 메모리에 저장 (TTL 적용) - */ - public void put(String key, TokenType type, String token, Duration ttl) { - long expiredAt = Instant.now().plus(ttl).toEpochMilli(); - store.put(key, new Entry(type, token, expiredAt)); - } - - /** - * 토큰을 일회성으로 조회 후 제거 (만료 시 null 반환) - */ - public Entry consume(String key) { - Entry entry = store.remove(key); - - if (entry == null) return null; - if (entry.expireAtEpochMillis < Instant.now().toEpochMilli()) return null; - return entry; - } -} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java b/src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java index 05155cc80..3c0c43029 100644 --- a/src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java +++ b/src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java @@ -10,7 +10,7 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.common.security.annotation.Oauth2Id; -import konkuk.thip.common.security.oauth2.LoginTokenStorage; +import konkuk.thip.common.security.oauth2.tokenstorage.LoginTokenStorage; import konkuk.thip.common.security.oauth2.auth.dto.AuthSetCookieRequest; import konkuk.thip.common.security.oauth2.auth.dto.AuthSetCookieResponse; import konkuk.thip.common.security.oauth2.auth.dto.AuthTokenRequest; @@ -92,11 +92,11 @@ public BaseResponse getToken( String token; boolean isNewUser; - if (entry.getType() == ACCESS) { - token = entry.getToken(); + if (entry.type() == ACCESS) { + token = entry.token(); isNewUser = false; } else { - token = entry.getToken(); + token = entry.token(); isNewUser = true; } @@ -127,8 +127,8 @@ public BaseResponse setCookie( ResponseCookie cookie; String type; - if (entry.getType() == ACCESS) { - cookie = ResponseCookie.from(COOKIE_ACCESS_TOKEN.getValue(), entry.getToken()) + if (entry.type() == ACCESS) { + cookie = ResponseCookie.from(COOKIE_ACCESS_TOKEN.getValue(), entry.token()) .httpOnly(true) .secure(true) .sameSite("None") @@ -137,7 +137,7 @@ public BaseResponse setCookie( .build(); type = ACCESS.getValue(); } else { - cookie = ResponseCookie.from(COOKIE_TEMP_TOKEN.getValue(), entry.getToken()) + cookie = ResponseCookie.from(COOKIE_TEMP_TOKEN.getValue(), entry.token()) .httpOnly(true) .secure(true) .sameSite("None") diff --git a/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/LoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/LoginTokenStorage.java new file mode 100644 index 000000000..247ee1e31 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/LoginTokenStorage.java @@ -0,0 +1,19 @@ +package konkuk.thip.common.security.oauth2.tokenstorage; + +import konkuk.thip.common.security.oauth2.TokenType; + +import java.time.Duration; + +public interface LoginTokenStorage { + + void put(String key, TokenType type, String token, Duration ttl); + + /** + * 저장된 토큰을 1회용으로 소비 후 삭제한다. + * 존재하지 않으면 null 반환. + */ + Entry consume(String key); + + record Entry(TokenType type, String token) { + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/MemoryLoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/MemoryLoginTokenStorage.java new file mode 100644 index 000000000..575987f82 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/MemoryLoginTokenStorage.java @@ -0,0 +1,44 @@ +package konkuk.thip.common.security.oauth2.tokenstorage; + +import konkuk.thip.common.security.oauth2.TokenType; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; + +@Profile("test") +@Component +@RequiredArgsConstructor +public class MemoryLoginTokenStorage implements LoginTokenStorage{ + private record InternalEntry(TokenType type, String token, long expireAtEpochMillis) { } + + private final ConcurrentHashMap store = new ConcurrentHashMap<>(); + + /** + * 토큰을 메모리에 저장 (TTL 적용) + */ + @Override + public void put(String key, TokenType type, String token, Duration ttl) { + long expiredAt = Instant.now().plus(ttl).toEpochMilli(); + store.put(key, new InternalEntry(type, token, expiredAt)); + } + + /** + * 토큰을 일회성으로 조회 후 제거 (만료 시 null 반환) + */ + @Override + public Entry consume(String key) { + InternalEntry entry = store.remove(key); + if (entry == null) return null; + + if (entry.expireAtEpochMillis() < Instant.now().toEpochMilli()) { + return null; // 만료 + } + + // 외부에는 최소 DTO만 반환 (내부 정보 캡슐화) + return new Entry(entry.type(), entry.token()); + } +} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java new file mode 100644 index 000000000..689dd2133 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java @@ -0,0 +1,48 @@ +package konkuk.thip.common.security.oauth2.tokenstorage; + +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.common.security.oauth2.TokenType; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Profile({"!test"}) +@Component +@RequiredArgsConstructor +public class RedisLoginTokenStorage implements LoginTokenStorage { + + private final RedisTemplate redisTemplate; + + private static final String PREFIX = "auth:login-token:"; + + @Override + public void put(String key, TokenType type, String token, Duration ttl) { + String redisKey = toRedisKey(key); + Entry entry = new Entry(type, token); + + redisTemplate.opsForValue().set(redisKey, entry, ttl); + } + + @Override + public Entry consume(String key) { + String redisKey = toRedisKey(key); + Object value = redisTemplate.opsForValue().getAndDelete(redisKey); + if (value == null) { + return null; + } + + if (value instanceof Entry entry) { + return entry; + } + + throw new AuthException(ErrorCode.JSON_PROCESSING_ERROR); + } + + private String toRedisKey(String key) { + return PREFIX + key; + } +} \ No newline at end of file From 7accbaf3634ce066bf6f9b46f292e652acc3f538 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 19 Sep 2025 00:51:48 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[refactor]=20Setter=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=20=ED=9B=84=20=EB=B6=88=EB=B3=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/config/properties/WebDomainProperties.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java b/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java index cf45ec711..9a51754f9 100644 --- a/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java +++ b/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java @@ -1,7 +1,6 @@ package konkuk.thip.config.properties; import lombok.Getter; -import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -10,12 +9,11 @@ import java.util.List; @Getter -@Setter @Component @ConfigurationProperties(prefix = "server") public class WebDomainProperties { - private List webDomainUrls = new ArrayList<>(); + private final List webDomainUrls = new ArrayList<>(); public boolean isAllowed(String target) { try { From ef217a47ab766556e2ee89f366b90297e36c861b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 19 Sep 2025 00:52:08 +0900 Subject: [PATCH 08/11] =?UTF-8?q?[refactor]=20Slf4j=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/resolver/CustomAuthorizationRequestResolver.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java index 8e2974e64..76f1f26b0 100644 --- a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java @@ -3,7 +3,6 @@ import jakarta.servlet.http.HttpServletRequest; import konkuk.thip.config.properties.WebDomainProperties; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; @@ -16,7 +15,6 @@ import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_SESSION_KEY; import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_URL_KEY; -@Slf4j @RequiredArgsConstructor public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { From d300c6070022c5b5d820661d6ed89dd8c0b41c33 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sun, 21 Sep 2025 01:26:09 +0900 Subject: [PATCH 09/11] =?UTF-8?q?[refactor]=20=EC=BF=BC=EB=A6=AC=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=AF=B8=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=EC=8B=9C=20=EC=84=B8=EC=85=98=20=EC=A0=9C=EA=B1=B0=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resolver/CustomAuthorizationRequestResolver.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java index 76f1f26b0..e202f22bf 100644 --- a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java @@ -45,10 +45,12 @@ private OAuth2AuthorizationRequest customize(HttpServletRequest request, OAuth2A String redirectUrl = request.getParameter(REDIRECT_URL_KEY.getValue()); Map additional = new HashMap<>(base.getAdditionalParameters()); + var session = request.getSession(true); if (StringUtils.hasText(redirectUrl) && webDomainProperties.isAllowed(redirectUrl)) { - request.getSession(true).setAttribute(REDIRECT_SESSION_KEY.getValue(), redirectUrl); + session.setAttribute(REDIRECT_SESSION_KEY.getValue(), redirectUrl); + } else { + session.removeAttribute(REDIRECT_SESSION_KEY.getValue()); } - return OAuth2AuthorizationRequest.from(base) .additionalParameters(additional) .build(); From a78016821c42afa8d183c5e066aa7bd8c17eb39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sun, 21 Sep 2025 01:52:24 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[refactor]=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=EA=B2=80=EC=A6=9D=20=ED=83=88=ED=87=B4?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=EC=9D=80=20=EC=82=AC=EC=9A=A9=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20-->=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/adapter/in/web/UserVerifyNicknameControllerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java index a6042d8f6..82e2d2f62 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java @@ -147,7 +147,7 @@ void nickname_too_long() throws Exception { } @Test - @DisplayName("회원 탈퇴한(= soft delete 처리된) 유저의 닉네임 정보를 포함해서 중복 검증을 수행한다.") + @DisplayName("회원 탈퇴한(= soft delete 처리된) 유저의 닉네임 정보를 포함하지않고 중복 검증을 수행한다.") void verify_nickname_with_soft_delete_users() throws Exception { //given UserJpaEntity deleteUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "노성준")); @@ -170,6 +170,6 @@ void verify_nickname_with_soft_delete_users() throws Exception { JsonNode jsonNode = objectMapper.readTree(json); boolean isVerified = jsonNode.path("data").path("isVerified").asBoolean(); - assertThat(isVerified).isFalse(); // 닉네임 중복으로 인해 isVerified == false + assertThat(isVerified).isTrue(); } } From 2052fcf2ae65b74e197c39132e0ca9022b323978 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sun, 21 Sep 2025 02:38:29 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[refactor]=20entrypoint=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=84=98=EC=96=B4=EC=98=A4=EB=8A=94=20=EC=98=88=EC=83=81?= =?UTF-8?q?=EC=B9=98=20=EB=AA=BB=ED=95=9C=20=EC=97=90=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?AuthException=EC=9C=BC=EB=A1=9C=20=EA=B0=90=EC=8B=B8=EC=84=9C?= =?UTF-8?q?=20handler=EC=97=90=20=EC=A0=84=EB=8B=AC=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/exception/code/ErrorCode.java | 1 + .../filter/JwtAuthenticationEntryPoint.java | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 7b88726e2..9790bb1be 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -24,6 +24,7 @@ public enum ErrorCode implements ResponseCode { AUTH_UNSUPPORTED_SOCIAL_LOGIN(HttpStatus.UNAUTHORIZED, 40105, "지원하지 않는 소셜 로그인입니다."), AUTH_INVALID_LOGIN_TOKEN_KEY(HttpStatus.UNAUTHORIZED, 40106, "유효하지 않은 로그인 토큰 키입니다."), AUTH_BLACKLIST_TOKEN(HttpStatus.UNAUTHORIZED, 40107, "블랙리스트에 등록된 토큰입니다."), + AUTH_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 40108, "인증 처리 중 서버 오류가 발생했습니다."), JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50100, "JSON 직렬화/역직렬화에 실패했습니다."), AWS_BUCKET_BASE_URL_NOT_CONFIGURED(HttpStatus.INTERNAL_SERVER_ERROR, 50101, "aws s3 bucket base url 설정이 누락되었습니다."), diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java index b52dfa329..0a2fe3c5f 100644 --- a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java @@ -3,6 +3,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.exception.code.ErrorCode; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; @@ -15,16 +17,32 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { private final HandlerExceptionResolver resolver; - public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver){ + public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") + HandlerExceptionResolver resolver) { this.resolver = resolver; } @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - Exception e = (Exception) request.getAttribute("exception"); - if(e == null){ - e = authException; + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + // 필터에서 set한 예외 우선 + Exception original = (Exception) request.getAttribute("exception"); + if (original == null) { + original = authException; + } + + Exception mapped = wrapAsAuthException(original); + + resolver.resolveException(request, response, null, mapped); + } + + // 모든 예외를 AuthException(401)으로 감싸는 메서드 + private Exception wrapAsAuthException(Exception e) { + if (e instanceof AuthException) { + return e; } - resolver.resolveException(request, response, null, e); + return new AuthException(ErrorCode.AUTH_INTERNAL_SERVER_ERROR, e); } -} +} \ No newline at end of file