From 63ae5ed6c9904aeb36139110a3ac25f237d6083f Mon Sep 17 00:00:00 2001 From: shindw Date: Thu, 26 Feb 2026 11:10:00 +0900 Subject: [PATCH] =?UTF-8?q?[api]=20swagger=20=EC=85=8B=ED=8C=85=EC=A4=91?= =?UTF-8?q?=20-=20cors=20=ED=99=94=EC=9D=B4=ED=8A=B8=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=97=88=EC=9A=A9=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 5 +- .../java/com/alist/api/config/CorsConfig.java | 34 ----- .../api/config/filter/DynamicCorsFilter.java | 117 ++++++++++++++++++ .../api/config/jwt/JwtTokenProvider.java | 1 + .../api/config/properties/CorsProperties.java | 19 --- .../modules/auth/mapper/TestCorsMapper.java | 12 ++ src/main/resources/application-local.yaml | 10 -- src/main/resources/application-pjt.yaml | 17 --- .../resources/mapper/auth/TestCorsMapper.xml | 20 +++ 9 files changed, 153 insertions(+), 82 deletions(-) delete mode 100644 src/main/java/com/alist/api/config/CorsConfig.java create mode 100644 src/main/java/com/alist/api/config/filter/DynamicCorsFilter.java delete mode 100644 src/main/java/com/alist/api/config/properties/CorsProperties.java create mode 100644 src/main/java/com/alist/api/modules/auth/mapper/TestCorsMapper.java create mode 100644 src/main/resources/mapper/auth/TestCorsMapper.xml diff --git a/CLAUDE.md b/CLAUDE.md index a2d1382..eb3c3bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,8 +5,9 @@ ### 코드 수정 시 반드시 따를 것 1. **절대로 바로 파일을 수정하지 말 것** 2. **먼저 수정 방향과 계획을 설명** -3. **수정할 코드를 먼저 보여주기** (사용자가 직접 수정할 수도 있도록) -4. **사용자 확인 후 자동 작성 진행** 또는 사용자가 요청 시에만 작성 +3. **수정할 코드를 보여줄지 말지 물어보고 보여주기** +4. **수정할 코드를 먼저 보여주기** (사용자가 직접 수정할 수도 있도록) +5. **사용자 확인 후 자동 작성 진행** 또는 사용자가 요청 시에만 작성 ### 작업 순서 예시 ``` diff --git a/src/main/java/com/alist/api/config/CorsConfig.java b/src/main/java/com/alist/api/config/CorsConfig.java deleted file mode 100644 index dd08e33..0000000 --- a/src/main/java/com/alist/api/config/CorsConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.alist.api.config; - -import com.alist.api.config.properties.CorsProperties; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -@Configuration -@EnableConfigurationProperties(CorsProperties.class) -@RequiredArgsConstructor -public class CorsConfig { - - private final CorsProperties corsProperties; - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(corsProperties.getAllowedOrigins()); - configuration.setAllowCredentials(corsProperties.getAllowCredentials()); - configuration.setAllowedMethods(corsProperties.getAllowedMethods()); - configuration.setAllowedHeaders(corsProperties.getAllowedHeaders()); - configuration.setExposedHeaders(corsProperties.getExposedHeaders()); - configuration.setMaxAge(corsProperties.getMaxAge()); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } -} \ No newline at end of file diff --git a/src/main/java/com/alist/api/config/filter/DynamicCorsFilter.java b/src/main/java/com/alist/api/config/filter/DynamicCorsFilter.java new file mode 100644 index 0000000..e9f36a9 --- /dev/null +++ b/src/main/java/com/alist/api/config/filter/DynamicCorsFilter.java @@ -0,0 +1,117 @@ +package com.alist.api.config.filter; +import com.alist.api.config.jwt.JwtTokenProvider; +import com.alist.api.modules.auth.mapper.TestCorsMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Slf4j +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class DynamicCorsFilter extends OncePerRequestFilter { + private final TestCorsMapper testCorsMapper; + private final JwtTokenProvider jwtTokenProvider; + + public DynamicCorsFilter(TestCorsMapper testCorsMapper, JwtTokenProvider jwtTokenProvider) { + this.testCorsMapper = testCorsMapper; + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String origin = request.getHeader("Origin"); + Integer userTokenIdx = extractUserTokenIdx(request); + + // DB에서 허용 도메인 조회 + List allowedOrigins = testCorsMapper.selectTestCorsAllowedList(userTokenIdx); + + // CORS 체크 결과 로깅 + if (origin != null) { + boolean isAllowed = allowedOrigins.contains("*") || allowedOrigins.contains(origin); + + if (isAllowed && log.isDebugEnabled()) { + // 허용된 경우 DEBUG 레벨 + log.debug("CORS allowed - Origin: {}, UserTokenIdx: {}", origin, userTokenIdx); + } else if (!isAllowed) { + // 차단된 경우 WARN 레벨 (보안 모니터링) + log.warn("CORS blocked - Origin: {}, UserTokenIdx: {}, AllowedOrigins: {}", + origin, userTokenIdx, allowedOrigins); + } + } + + // Origin이 허용 목록에 있거나 "*"가 있으면 CORS 헤더 설정 + if (origin != null && (allowedOrigins.contains("*") || allowedOrigins.contains(origin))) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", "*"); + response.setHeader("Access-Control-Expose-Headers", "Authorization, Set-Cookie"); + response.setHeader("Access-Control-Max-Age", "3600"); + } + + // Preflight 요청(OPTIONS) 처리 + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + response.setStatus(HttpServletResponse.SC_OK); + return; + } + + filterChain.doFilter(request, response); + } + + /** + * Request에서 user_token_idx 추출 + * JWT 토큰 또는 API Key를 통해 추출 + */ + private Integer extractUserTokenIdx(HttpServletRequest request) { + try { + String token = null; + + // 1. Authorization Bearer 토큰 + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + } + + // 2. Cookie에서 accessToken + if (token == null && request.getCookies() != null) { + for (var cookie : request.getCookies()) { + if ("accessToken".equals(cookie.getName())) { + token = cookie.getValue(); + break; + } + } + } + + // 토큰이 있으면 검증 후 userTokenIdx 추출 + if (token != null && jwtTokenProvider.validateToken(token)) { + String subject = jwtTokenProvider.getUserId(token); + + // subject를 Integer로 변환 시도 + try { + return Integer.parseInt(subject); + } catch (NumberFormatException e) { + // 숫자가 아닌 문자열이면 null 반환 (기본값 0으로 조회) + log.debug("Subject is not a number: {}", subject); + return null; + } + } + + } catch (Exception e) { + log.debug("Failed to extract userTokenIdx: {}", e.getMessage()); + } + + return null; // 기본값 0만 조회 + } +} diff --git a/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java b/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java index a75701b..ed4113f 100644 --- a/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java +++ b/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java @@ -89,4 +89,5 @@ public class JwtTokenProvider { .parseClaimsJws(token) .getBody(); } + } diff --git a/src/main/java/com/alist/api/config/properties/CorsProperties.java b/src/main/java/com/alist/api/config/properties/CorsProperties.java deleted file mode 100644 index 41050f5..0000000 --- a/src/main/java/com/alist/api/config/properties/CorsProperties.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.alist.api.config.properties; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.List; - -@Getter -@Setter -@ConfigurationProperties(prefix = "cors") -public class CorsProperties { - List allowedOrigins; - Boolean allowCredentials = true; - List allowedMethods = List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"); - List allowedHeaders = List.of("*"); - List exposedHeaders = List.of("Authorization", "Set-Cookie"); - Long maxAge = 3600L; -} diff --git a/src/main/java/com/alist/api/modules/auth/mapper/TestCorsMapper.java b/src/main/java/com/alist/api/modules/auth/mapper/TestCorsMapper.java new file mode 100644 index 0000000..783047c --- /dev/null +++ b/src/main/java/com/alist/api/modules/auth/mapper/TestCorsMapper.java @@ -0,0 +1,12 @@ +package com.alist.api.modules.auth.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.springframework.cache.annotation.Cacheable; + +import java.util.List; + +@Mapper +public interface TestCorsMapper { + @Cacheable(value = "corsAllowedOrigins", key = "#userTokenIdx") + List selectTestCorsAllowedList(Integer userTokenIdx); +} diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index d1ceba5..c2f10e5 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -18,16 +18,6 @@ jwt: cookie: secure: false # 로컬 개발 환경 (HTTP) -cors: - allowed-origins: - - "http://localhost:8100" - - "http://localhost:8101" - - "http://localhost:8102" - - "http://localhost:8103" - - "http://localhost:8104" - - "http://localhost:8105" - - "http://localhost:8108" - swagger: login: id: alist diff --git a/src/main/resources/application-pjt.yaml b/src/main/resources/application-pjt.yaml index e602b5c..31aa0a8 100644 --- a/src/main/resources/application-pjt.yaml +++ b/src/main/resources/application-pjt.yaml @@ -20,23 +20,6 @@ jwt: cookie: secure: true # 프로젝트 환경 (HTTPS) -cors: - allowed-origins: - - "http://localhost:8100" - - "http://localhost:8101" - - "http://localhost:8102" - - "http://localhost:8103" - - "http://localhost:8104" - - "http://localhost:8105" - - "http://localhost:8108" - - "https://wwwl-alist.pjt.kr" - - "https://engl-alist.pjt.kr" - - "https://admin-alist.pjt.kr" - - "https://wwwc-alist.pjt.kr" - - "https://class-alist.pjt.kr" - - "https://student-alist.pjt.kr" - - "https://storybook-alist.pjt.kr" - swagger: login: id: ${SWAGGER_ID} diff --git a/src/main/resources/mapper/auth/TestCorsMapper.xml b/src/main/resources/mapper/auth/TestCorsMapper.xml new file mode 100644 index 0000000..fd48cfb --- /dev/null +++ b/src/main/resources/mapper/auth/TestCorsMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file