mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2026-06-12 19:49:10 +00:00
회선번호 처리 개선 및 다양한 API 기능 강화
- user-service: 회원등록 API를 upsert 방식으로 변경 (기존 사용자 업데이트 지원) - user-service: userName 필드 응답 누락 문제 해결 (DB 데이터 업데이트) - kos-mock: Mock 데이터 생성 기간을 3개월에서 6개월로 확장 - product-service: 회선번호 대시 처리 지원 (010-1234-5678, 01012345678 모두 허용) - bill-service: 회선번호 대시 선택적 처리 지원 (유연한 입력 형식) - api-gateway: CORS 중복 헤더 제거 필터 추가 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,6 @@ import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.reactive.CorsWebFilter;
|
||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Spring Cloud Gateway 라우팅 및 CORS 설정
|
||||
@@ -32,8 +27,6 @@ public class GatewayConfig {
|
||||
|
||||
private final JwtAuthenticationGatewayFilterFactory jwtAuthFilter;
|
||||
|
||||
@Value("${cors.allowed-origins}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Value("${services.user-service.url}")
|
||||
private String userServiceUrl;
|
||||
@@ -123,7 +116,7 @@ public class GatewayConfig {
|
||||
.setBackoff(java.time.Duration.ofSeconds(1), java.time.Duration.ofSeconds(5), 2, true))
|
||||
)
|
||||
.uri(kosMockUrl))
|
||||
|
||||
|
||||
// 주의: Gateway 자체 엔드포인트는 라우팅하지 않음
|
||||
// Health Check와 Swagger UI는 Spring Boot에서 직접 제공
|
||||
|
||||
@@ -131,51 +124,8 @@ public class GatewayConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS 설정
|
||||
*
|
||||
* 프론트엔드에서 API Gateway로의 크로스 오리진 요청을 허용합니다.
|
||||
* 개발/운영 환경에 따라 허용 오리진을 다르게 설정합니다.
|
||||
*
|
||||
* @return CorsWebFilter
|
||||
* CORS 설정은 application.yml의 globalcors에서 관리
|
||||
* add-to-simple-url-handler-mapping: true 설정으로
|
||||
* 라우트 predicate와 매치되지 않는 OPTIONS 요청도 처리
|
||||
*/
|
||||
@Bean
|
||||
public CorsWebFilter corsWebFilter() {
|
||||
CorsConfiguration corsConfig = new CorsConfiguration();
|
||||
|
||||
// 환경변수에서 허용할 Origin 패턴 설정
|
||||
String[] origins = allowedOrigins.split(",");
|
||||
corsConfig.setAllowedOriginPatterns(Arrays.asList(origins));
|
||||
|
||||
// 허용할 HTTP 메서드
|
||||
corsConfig.setAllowedMethods(Arrays.asList(
|
||||
"GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"
|
||||
));
|
||||
|
||||
// 허용할 헤더
|
||||
corsConfig.setAllowedHeaders(Arrays.asList(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"X-Request-ID",
|
||||
"X-User-Agent"
|
||||
));
|
||||
|
||||
// 노출할 헤더 (클라이언트가 접근 가능한 헤더)
|
||||
corsConfig.setExposedHeaders(Arrays.asList(
|
||||
"X-Request-ID",
|
||||
"X-Response-Time",
|
||||
"X-Rate-Limit-Remaining"
|
||||
));
|
||||
|
||||
// 자격 증명 허용 (쿠키, Authorization 헤더 등)
|
||||
corsConfig.setAllowCredentials(true);
|
||||
|
||||
// Preflight 요청 캐시 시간 (초)
|
||||
corsConfig.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", corsConfig);
|
||||
|
||||
return new CorsWebFilter(source);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.unicorn.phonebill.gateway.filter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Global Filter for removing duplicate CORS headers
|
||||
*
|
||||
* 이 필터는 백엔드 서비스에서 오는 CORS 헤더를 제거하여
|
||||
* API Gateway의 GlobalCors 설정만 사용하도록 합니다.
|
||||
*
|
||||
* @author 이개발(백엔더)
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-08
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CorsDedupeGlobalFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private static final List<String> CORS_HEADERS = Arrays.asList(
|
||||
"Access-Control-Allow-Origin",
|
||||
"Access-Control-Allow-Methods",
|
||||
"Access-Control-Allow-Headers",
|
||||
"Access-Control-Allow-Credentials",
|
||||
"Access-Control-Expose-Headers",
|
||||
"Access-Control-Max-Age"
|
||||
);
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
String path = exchange.getRequest().getPath().value();
|
||||
log.info("=== CorsDedupeGlobalFilter 시작 - Path: {}", path);
|
||||
|
||||
// Response를 감싸서 헤더 수정 가능하게 만들기
|
||||
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(exchange.getResponse()) {
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
HttpHeaders originalHeaders = super.getHeaders();
|
||||
HttpHeaders filteredHeaders = new HttpHeaders();
|
||||
|
||||
// 원본 헤더를 안전하게 복사하면서 CORS 헤더는 제외
|
||||
final int[] removedCount = {0};
|
||||
originalHeaders.forEach((key, values) -> {
|
||||
if (CORS_HEADERS.contains(key)) {
|
||||
removedCount[0]++;
|
||||
log.info("CORS 헤더 제거: {} = {}", key, values);
|
||||
} else {
|
||||
try {
|
||||
filteredHeaders.addAll(key, values);
|
||||
} catch (Exception e) {
|
||||
log.warn("헤더 추가 실패: {} = {}, 에러: {}", key, values, e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (removedCount[0] > 0) {
|
||||
log.info("=== CorsDedupeGlobalFilter 완료 - 제거된 헤더 수: {}", removedCount[0]);
|
||||
}
|
||||
|
||||
return filteredHeaders;
|
||||
}
|
||||
};
|
||||
|
||||
// 수정된 response로 exchange 생성
|
||||
ServerWebExchange mutatedExchange = exchange.mutate().response(decoratedResponse).build();
|
||||
return chain.filter(mutatedExchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return -1; // 높은 우선순위로 설정
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,28 @@ spring:
|
||||
response-timeout: 60s
|
||||
connect-timeout: 5000
|
||||
|
||||
# Global CORS 설정
|
||||
globalcors:
|
||||
cors-configurations:
|
||||
'[/**]':
|
||||
allowedOrigins: "${CORS_ALLOWED_ORIGINS:http://localhost:3000}"
|
||||
allowedMethods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
allowedHeaders: "*"
|
||||
allow-credentials: true
|
||||
max-age: 3600
|
||||
|
||||
# Discovery 설정 비활성화 (직접 라우팅 사용)
|
||||
discovery:
|
||||
locator:
|
||||
enabled: false
|
||||
|
||||
# Default filters
|
||||
default-filters:
|
||||
- name: AddRequestHeader
|
||||
args:
|
||||
@@ -39,27 +61,6 @@ spring:
|
||||
args:
|
||||
name: X-Gateway-Response
|
||||
value: API-Gateway
|
||||
|
||||
# Global CORS 설정
|
||||
globalcors:
|
||||
cors-configurations:
|
||||
'[/**]':
|
||||
allowed-origin-patterns: "*"
|
||||
allowed-methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
allowed-headers: "*"
|
||||
allow-credentials: true
|
||||
max-age: 3600
|
||||
|
||||
# Discovery 설정 비활성화 (직접 라우팅 사용)
|
||||
discovery:
|
||||
locator:
|
||||
enabled: false
|
||||
|
||||
# JSON 설정
|
||||
jackson:
|
||||
@@ -69,10 +70,6 @@ spring:
|
||||
deserialization:
|
||||
fail-on-unknown-properties: false
|
||||
|
||||
# CORS
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000}
|
||||
|
||||
# JWT 토큰 설정
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
|
||||
Reference in New Issue
Block a user