From b568dce92b63753f852387998743005d60c90676 Mon Sep 17 00:00:00 2001 From: kanghana1 Date: Fri, 10 Jul 2026 17:20:25 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EA=B0=92=20SSOT=20=EC=99=B8=EB=B6=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 6 +- .../prod.conf.template} | 2 +- .../staging.conf.template} | 2 +- .../member/controller/MemberController.java | 40 ++++++----- .../demo/global/config/SecurityConfig.java | 5 +- .../demo/global/config/WebProperties.java | 27 +++++++ src/main/resources/application.yml | 5 ++ .../demo/global/config/WebPropertiesTest.java | 71 +++++++++++++++++++ .../resources/application-integrationtest.yml | 3 + src/test/resources/application.yml | 3 + 10 files changed, 143 insertions(+), 21 deletions(-) rename nginx/{conf.d/prod.conf => templates/prod.conf.template} (95%) rename nginx/{conf.d/staging.conf => templates/staging.conf.template} (95%) create mode 100644 src/main/java/umc/cockple/demo/global/config/WebProperties.java create mode 100644 src/test/java/umc/cockple/demo/global/config/WebPropertiesTest.java diff --git a/docker-compose.yml b/docker-compose.yml index 637838ed0..8784f6a7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -113,9 +113,13 @@ services: restart: always ports: - "80:80" + environment: + PROD_SERVER_NAME: ${NGINX_PROD_SERVER_NAME:-api.cockple.store} + STAGING_SERVER_NAME: ${NGINX_STAGING_SERVER_NAME:-staging.cockple.store} + NGINX_ENVSUBST_FILTER: SERVER_NAME volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./nginx/templates:/etc/nginx/templates:ro mem_limit: 64m memswap_limit: 128m diff --git a/nginx/conf.d/prod.conf b/nginx/templates/prod.conf.template similarity index 95% rename from nginx/conf.d/prod.conf rename to nginx/templates/prod.conf.template index 635b2a3cf..867822e36 100644 --- a/nginx/conf.d/prod.conf +++ b/nginx/templates/prod.conf.template @@ -1,6 +1,6 @@ server { listen 80; - server_name api.cockple.store; + server_name ${PROD_SERVER_NAME}; location /ws/ { proxy_pass http://cockple-app:8080; diff --git a/nginx/conf.d/staging.conf b/nginx/templates/staging.conf.template similarity index 95% rename from nginx/conf.d/staging.conf rename to nginx/templates/staging.conf.template index dc016b844..0935e2557 100644 --- a/nginx/conf.d/staging.conf +++ b/nginx/templates/staging.conf.template @@ -1,6 +1,6 @@ server { listen 80; - server_name staging.cockple.store; + server_name ${STAGING_SERVER_NAME}; location /ws/ { proxy_pass http://cockple-app-staging:8080; diff --git a/src/main/java/umc/cockple/demo/domain/member/controller/MemberController.java b/src/main/java/umc/cockple/demo/domain/member/controller/MemberController.java index 2c9162f54..ea373651c 100644 --- a/src/main/java/umc/cockple/demo/domain/member/controller/MemberController.java +++ b/src/main/java/umc/cockple/demo/domain/member/controller/MemberController.java @@ -16,6 +16,7 @@ import umc.cockple.demo.domain.member.service.MemberCommandService; import umc.cockple.demo.domain.member.service.MemberProfileUpdateExecutor; import umc.cockple.demo.domain.member.service.MemberQueryService; +import umc.cockple.demo.global.config.WebProperties; import umc.cockple.demo.global.jwt.domain.TokenRefreshResponse; import umc.cockple.demo.global.oauth2.service.KakaoOauthService; import umc.cockple.demo.global.response.BaseResponse; @@ -41,6 +42,8 @@ public class MemberController { private final MemberProfileUpdateExecutor memberProfileUpdateExecutor; private final MemberQueryService memberQueryService; private final KakaoOauthService kakaoOauthService; + private final WebProperties webProperties; + @PostMapping("/oauth/login") @Operation(summary = "카카오 소셜 로그인 API", @@ -49,15 +52,7 @@ public ResponseEntity login(@RequestBody @Valid KakaoLogi KakaoLoginResponseDTO response = kakaoOauthService.signup(requestDTO.code()); - ResponseCookie cookie = ResponseCookie.from("refreshToken", response.refreshToken()) - .httpOnly(true) - .secure(true) - .path("/") - .maxAge(Duration.ofDays(7)) - .sameSite("None") - .domain(".cockple.store") - .build() - ; + ResponseCookie cookie = buildRefreshTokenCookie(response.refreshToken()); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookie.toString()) @@ -116,14 +111,7 @@ public ResponseEntity refresh(@CookieValue("refreshToken") // 리프레시 토큰 유효성 검사 TokenRefreshResponse response = kakaoOauthService.validateMember(refreshToken); - ResponseCookie cookie = ResponseCookie.from("refreshToken", response.refreshToken()) - .httpOnly(true) - .secure(true) - .path("/") - .maxAge(Duration.ofDays(7)) - .sameSite("None") - .domain(".cockple.store") - .build(); + ResponseCookie cookie = buildRefreshTokenCookie(response.refreshToken()); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookie.toString()) @@ -244,4 +232,22 @@ public BaseResponse> getAllAddress() { List addresses = memberQueryService.getAllAddress(memberId); return BaseResponse.success(CommonSuccessCode.OK, addresses); } + + /** + * refreshToken 쿠키 생성(로그인/재발급 공통) + */ + private ResponseCookie buildRefreshTokenCookie(String refreshToken) { + ResponseCookie.ResponseCookieBuilder builder = ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(Duration.ofDays(7)) + .sameSite("None"); + + String cookieDomain = webProperties.getCookieDomain(); + if (cookieDomain != null && !cookieDomain.isBlank()) { + builder.domain(cookieDomain); + } + return builder.build(); + } } diff --git a/src/main/java/umc/cockple/demo/global/config/SecurityConfig.java b/src/main/java/umc/cockple/demo/global/config/SecurityConfig.java index d6b0a6579..61b2c3179 100644 --- a/src/main/java/umc/cockple/demo/global/config/SecurityConfig.java +++ b/src/main/java/umc/cockple/demo/global/config/SecurityConfig.java @@ -25,6 +25,7 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final RestAuthenticationEntryPoint restEntryPoint; + private final WebProperties webProperties; @Bean @@ -66,7 +67,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(List.of("http://localhost:5173", "https://cockple.store", "https://www.cockple.store", "https://staging.cockple.store", "https://cockple-fe.vercel.app")); // 배포 시에는 도메인 지정 권장 + // 허용 Origin은 WebProperties(SSOT)에서 주입. 정확 매칭 유지 — allowCredentials(true)와 + // origin 패턴/와일드카드를 함께 쓰면 요청 Origin이 반사되어 심각한 취약점이 되므로 절대 금지. + config.setAllowedOrigins(webProperties.getAllowedOrigins()); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); diff --git a/src/main/java/umc/cockple/demo/global/config/WebProperties.java b/src/main/java/umc/cockple/demo/global/config/WebProperties.java new file mode 100644 index 000000000..3b1ce7634 --- /dev/null +++ b/src/main/java/umc/cockple/demo/global/config/WebProperties.java @@ -0,0 +1,27 @@ +package umc.cockple.demo.global.config; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +/** + * 도메인/오리진 관련 웹 설정의 단일 소스(SSOT) + * 값은 application.yml의 기본값 또는 환경변수(CORS_ALLOWED_ORIGINS, COOKIE_DOMAIN)로 주입 + */ +@Getter +@Setter +@Configuration +@Validated +@ConfigurationProperties(prefix = "cockple.web") +public class WebProperties { + + @NotEmpty(message = "cockple.web.allowed-origins must not be empty") + private List allowedOrigins; + + private String cookieDomain; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6120c9375..f1f3743b7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -71,6 +71,11 @@ spring: gcs: bucket: ${GCS_BUCKET} +cockple: + web: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app} + cookie-domain: ${COOKIE_DOMAIN:.cockple.store} + kakao: client-id: ${KAKAO_CLIENT_ID} client-secret: ${KAKAO_CLIENT_SECRET} diff --git a/src/test/java/umc/cockple/demo/global/config/WebPropertiesTest.java b/src/test/java/umc/cockple/demo/global/config/WebPropertiesTest.java new file mode 100644 index 000000000..e44455809 --- /dev/null +++ b/src/test/java/umc/cockple/demo/global/config/WebPropertiesTest.java @@ -0,0 +1,71 @@ +package umc.cockple.demo.global.config; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * WebProperties 바인딩 검증. + * 도메인 값을 하드코딩에서 설정 바인딩으로 옮긴 뒤에도, 운영과 "동일한" origin 목록/쿠키 도메인이 + * 실제로 그대로 풀리는지(no-op) 배포 없이 결정적으로 확인한다. + */ +class WebPropertiesTest { + + private WebProperties bind(Map props) { + Binder binder = new Binder(new MapConfigurationPropertySource(props)); + return binder.bind("cockple.web", WebProperties.class).get(); + } + + @Test + @DisplayName("application.yml 기본값(콤마 구분)이 운영과 동일한 origin 목록으로 바인딩된다") + void bindsCommaSeparatedOriginsExactlyAsBefore() { + // application.yml의 CORS_ALLOWED_ORIGINS 기본값과 동일한 문자열 + String defaultOrigins = + "http://localhost:5173,https://cockple.store,https://www.cockple.store," + + "https://staging.cockple.store,https://cockple-fe.vercel.app"; + + WebProperties props = bind(Map.of( + "cockple.web.allowed-origins", defaultOrigins, + "cockple.web.cookie-domain", ".cockple.store" + )); + + // 리팩토링 전 하드코딩되어 있던 값과 정확히 일치해야 한다(순수 no-op) + assertThat(props.getAllowedOrigins()).containsExactly( + "http://localhost:5173", + "https://cockple.store", + "https://www.cockple.store", + "https://staging.cockple.store", + "https://cockple-fe.vercel.app" + ); + assertThat(props.getCookieDomain()).isEqualTo(".cockple.store"); + } + + @Test + @DisplayName("cookie-domain이 비면 null/blank로 바인딩되어 host-only 쿠키로 동작한다") + void blankCookieDomainMeansHostOnly() { + WebProperties props = bind(Map.of( + "cockple.web.allowed-origins", "http://localhost:5173", + "cockple.web.cookie-domain", "" + )); + + String cookieDomain = props.getCookieDomain(); + assertThat(cookieDomain == null || cookieDomain.isBlank()).isTrue(); + } + + @Test + @DisplayName("env로 새 도메인을 주입하면 코드 변경 없이 목록이 교체된다") + void envOverrideReplacesOrigins() { + WebProperties props = bind(Map.of( + "cockple.web.allowed-origins", "https://new-domain.com,https://api.new-domain.com" + )); + + assertThat(props.getAllowedOrigins()) + .containsExactly("https://new-domain.com", "https://api.new-domain.com"); + } +} diff --git a/src/test/resources/application-integrationtest.yml b/src/test/resources/application-integrationtest.yml index 07569887b..e75889e10 100644 --- a/src/test/resources/application-integrationtest.yml +++ b/src/test/resources/application-integrationtest.yml @@ -33,6 +33,9 @@ cockple: object-storage-delete-outbox: scheduler: enabled: false + web: + allowed-origins: http://localhost:5173 + cookie-domain: "" kakao: client-id: test-client-id diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index e055d686f..3741cc941 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -30,3 +30,6 @@ cockple: object-storage-delete-outbox: scheduler: enabled: false + web: + allowed-origins: http://localhost:5173 + cookie-domain: "" From db0c6df9b7fa204f5dfacc646ed2fc479c38f9f7 Mon Sep 17 00:00:00 2001 From: kanghana1 Date: Fri, 10 Jul 2026 17:32:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20terraform=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=B3=80=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- terraform/storage.tf | 2 +- terraform/variables.tf | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/terraform/storage.tf b/terraform/storage.tf index 333b02378..dd0e9bf75 100644 --- a/terraform/storage.tf +++ b/terraform/storage.tf @@ -16,7 +16,7 @@ resource "google_storage_bucket" "cockple_assets" { uniform_bucket_level_access = true cors { - origin = ["https://cockple.store", "https://staging.cockple.store"] + origin = ["https://${var.root_domain}", "https://staging.${var.root_domain}"] method = ["GET", "PUT", "POST", "DELETE"] response_header = ["Content-Type"] max_age_seconds = 3600 diff --git a/terraform/variables.tf b/terraform/variables.tf index 1d549e13f..95970edc5 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -16,10 +16,16 @@ variable "cloudflare_api_token" { } variable "cloudflare_zone_id" { - description = "cockple.store Cloudflare Zone ID" + description = "루트 도메인(root_domain)의 Cloudflare Zone ID" type = string } +variable "root_domain" { + description = "서비스 루트 도메인. 도메인 변경 시 이 값(+ cloudflare_zone_id)만 교체." + type = string + default = "cockple.store" +} + variable "ssh_public_key" { description = "인스턴스 접속용 SSH 공개키" type = string From c32380dd667d6bb35ad6ecc44c11ae3a39a84979 Mon Sep 17 00:00:00 2001 From: kanghana1 Date: Fri, 10 Jul 2026 17:47:43 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20env=20=EB=B0=B0=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 9 ++++++++- docker-compose.yml | 4 ++++ scripts/deploy.sh | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ae1712d65..b9e12e315 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -73,7 +73,8 @@ jobs: DB_PASSWORD,GCS_BUCKET,GCS_BACKUP_BUCKET, KAKAO_CLIENT_ID,KAKAO_CLIENT_SECRET,KAKAO_REDIRECT_URI_PROD,KAKAO_REDIRECT_URI_STAGING,KAKAO_ADMIN_KEY, JWT_SECRET_KEY,FIREBASE_SERVICE_ACCOUNT_KEY,FCM_FAKE_LATENCY_MS, - OTLP_METRICS_ENABLED,OTEL_URL,OTEL_AUTH,OTLP_STEP + OTLP_METRICS_ENABLED,OTEL_URL,OTEL_AUTH,OTLP_STEP, + CORS_ALLOWED_ORIGINS_PROD,CORS_ALLOWED_ORIGINS_STAGING,COOKIE_DOMAIN,NGINX_PROD_SERVER_NAME,NGINX_STAGING_SERVER_NAME script: | chmod +x /home/ubuntu/cockple/scripts/deploy.sh bash /home/ubuntu/cockple/scripts/deploy.sh \ @@ -96,3 +97,9 @@ jobs: OTEL_URL: ${{ vars.OTEL_URL }} OTEL_AUTH: ${{ secrets.OTEL_AUTH }} OTLP_STEP: ${{ vars.OTLP_STEP }} + # 도메인(SSOT) — 비밀 아님 → Variables. 미설정 시 빈 값 → docker-compose 기본값(현재 도메인) 사용(no-op). + CORS_ALLOWED_ORIGINS_PROD: ${{ vars.CORS_ALLOWED_ORIGINS_PROD }} + CORS_ALLOWED_ORIGINS_STAGING: ${{ vars.CORS_ALLOWED_ORIGINS_STAGING }} + COOKIE_DOMAIN: ${{ vars.COOKIE_DOMAIN }} + NGINX_PROD_SERVER_NAME: ${{ vars.NGINX_PROD_SERVER_NAME }} + NGINX_STAGING_SERVER_NAME: ${{ vars.NGINX_STAGING_SERVER_NAME }} diff --git a/docker-compose.yml b/docker-compose.yml index 8784f6a7e..0c09549ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,6 +57,8 @@ services: JAVA_TOOL_OPTIONS: "-Xms768m -Xmx768m" SPRING_PROFILES_ACTIVE: prod LOG_PATH: /logs + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS_PROD:-http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app} + COOKIE_DOMAIN: ${COOKIE_DOMAIN:-.cockple.store} DB_PASSWORD: ${DB_PASSWORD} GCS_BUCKET: ${GCS_BUCKET} KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID} @@ -83,6 +85,8 @@ services: JAVA_TOOL_OPTIONS: "-Xms128m -Xmx512m" SPRING_PROFILES_ACTIVE: staging LOG_PATH: /logs + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS_STAGING:-http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app} + COOKIE_DOMAIN: ${COOKIE_DOMAIN:-.cockple.store} FCM_FAKE_LATENCY_MS: ${FCM_FAKE_LATENCY_MS:-0} # 관측성(Observability) — OTLP 메트릭 push (staging 전용). 미설정 시 off. OTLP_METRICS_ENABLED: ${OTLP_METRICS_ENABLED:-false} diff --git a/scripts/deploy.sh b/scripts/deploy.sh index eeb16e2b1..d627e76be 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -31,6 +31,11 @@ OTLP_METRICS_ENABLED=${OTLP_METRICS_ENABLED:-false} OTEL_URL=${OTEL_URL:-} OTEL_AUTH=${OTEL_AUTH:-} OTLP_STEP=${OTLP_STEP:-1m} +CORS_ALLOWED_ORIGINS_PROD=${CORS_ALLOWED_ORIGINS_PROD:-} +CORS_ALLOWED_ORIGINS_STAGING=${CORS_ALLOWED_ORIGINS_STAGING:-} +COOKIE_DOMAIN=${COOKIE_DOMAIN:-} +NGINX_PROD_SERVER_NAME=${NGINX_PROD_SERVER_NAME:-} +NGINX_STAGING_SERVER_NAME=${NGINX_STAGING_SERVER_NAME:-} EOF echo "${FIREBASE_SERVICE_ACCOUNT_KEY}" > /home/ubuntu/cockple/firebase-service-account.json From 6e82b30e8da01544c3ca1ae6475d100c8efa9ff9 Mon Sep 17 00:00:00 2001 From: kanghana1 Date: Sun, 12 Jul 2026 12:30:54 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20WS=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 ++-- .../chat/presentation/websocket/ChatWebSocketConfig.java | 4 +++- src/main/resources/application.yml | 3 ++- .../umc/cockple/demo/global/config/WebPropertiesTest.java | 8 +++++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0c09549ee..2c46d8315 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,7 +57,7 @@ services: JAVA_TOOL_OPTIONS: "-Xms768m -Xmx768m" SPRING_PROFILES_ACTIVE: prod LOG_PATH: /logs - CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS_PROD:-http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS_PROD:-http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app,https://cockple-fe.vercel.app/} COOKIE_DOMAIN: ${COOKIE_DOMAIN:-.cockple.store} DB_PASSWORD: ${DB_PASSWORD} GCS_BUCKET: ${GCS_BUCKET} @@ -85,7 +85,7 @@ services: JAVA_TOOL_OPTIONS: "-Xms128m -Xmx512m" SPRING_PROFILES_ACTIVE: staging LOG_PATH: /logs - CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS_STAGING:-http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS_STAGING:-http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app,https://cockple-fe.vercel.app/} COOKIE_DOMAIN: ${COOKIE_DOMAIN:-.cockple.store} FCM_FAKE_LATENCY_MS: ${FCM_FAKE_LATENCY_MS:-0} # 관측성(Observability) — OTLP 메트릭 push (staging 전용). 미설정 시 off. diff --git a/src/main/java/umc/cockple/demo/domain/chat/presentation/websocket/ChatWebSocketConfig.java b/src/main/java/umc/cockple/demo/domain/chat/presentation/websocket/ChatWebSocketConfig.java index 221d884dc..a2b426378 100644 --- a/src/main/java/umc/cockple/demo/domain/chat/presentation/websocket/ChatWebSocketConfig.java +++ b/src/main/java/umc/cockple/demo/domain/chat/presentation/websocket/ChatWebSocketConfig.java @@ -5,6 +5,7 @@ import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import umc.cockple.demo.global.config.WebProperties; @Configuration @EnableWebSocket @@ -13,13 +14,14 @@ public class ChatWebSocketConfig implements WebSocketConfigurer { private final ChatWebSocketHandler chatWebSocketHandler; private final JWTWebSocketAuthInterceptor jwtWebSocketAuthInterceptor; + private final WebProperties webProperties; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry .addHandler(chatWebSocketHandler, "/ws/chats") .addInterceptors(jwtWebSocketAuthInterceptor) - .setAllowedOrigins("http://localhost:5173", "https://cockple.store", "https://www.cockple.store", "https://cockple-fe.vercel.app/", "https://staging.cockple.store") + .setAllowedOrigins(webProperties.getAllowedOrigins().toArray(new String[0])) .withSockJS(); // 브라우저 호환성 } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f1f3743b7..fccd0b611 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -73,7 +73,8 @@ gcs: cockple: web: - allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app} + # WS도 이 목록 공유. cockple-fe.vercel.app/(끝슬래시)는 WS 기존값 유지용(무해). + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5173,https://cockple.store,https://www.cockple.store,https://staging.cockple.store,https://cockple-fe.vercel.app,https://cockple-fe.vercel.app/} cookie-domain: ${COOKIE_DOMAIN:.cockple.store} kakao: diff --git a/src/test/java/umc/cockple/demo/global/config/WebPropertiesTest.java b/src/test/java/umc/cockple/demo/global/config/WebPropertiesTest.java index e44455809..e460752a5 100644 --- a/src/test/java/umc/cockple/demo/global/config/WebPropertiesTest.java +++ b/src/test/java/umc/cockple/demo/global/config/WebPropertiesTest.java @@ -26,22 +26,24 @@ private WebProperties bind(Map props) { @DisplayName("application.yml 기본값(콤마 구분)이 운영과 동일한 origin 목록으로 바인딩된다") void bindsCommaSeparatedOriginsExactlyAsBefore() { // application.yml의 CORS_ALLOWED_ORIGINS 기본값과 동일한 문자열 + // (WS 편입으로 CORS/WS가 공유. cockple-fe.vercel.app/ 는 WS 기존값 유지용) String defaultOrigins = "http://localhost:5173,https://cockple.store,https://www.cockple.store," - + "https://staging.cockple.store,https://cockple-fe.vercel.app"; + + "https://staging.cockple.store,https://cockple-fe.vercel.app,https://cockple-fe.vercel.app/"; WebProperties props = bind(Map.of( "cockple.web.allowed-origins", defaultOrigins, "cockple.web.cookie-domain", ".cockple.store" )); - // 리팩토링 전 하드코딩되어 있던 값과 정확히 일치해야 한다(순수 no-op) + // 기존 CORS + WS 하드코딩 origin을 모두 포함해야 한다(순수 no-op) assertThat(props.getAllowedOrigins()).containsExactly( "http://localhost:5173", "https://cockple.store", "https://www.cockple.store", "https://staging.cockple.store", - "https://cockple-fe.vercel.app" + "https://cockple-fe.vercel.app", + "https://cockple-fe.vercel.app/" ); assertThat(props.getCookieDomain()).isEqualTo(".cockple.store"); }