CORS 에러 한 번에 끝내기: Spring Security 6 + 프록시(Nginx) 조합
프로덕션에서 발생하는 CORS(교차 출처) 오류의 7~8할은 설정 위치가 겹치거나 엇갈린 데서 옵니다. 이 글은 Spring Security 6 기반 백엔드와 Nginx 리버스 프록시 조합에서 하나의 진실된 CORS 설정(Single Source of Truth)을 만드는 실전 가이드입니다.
1) 문제 증상 정리
Response to preflight request doesn't pass access control check- 프론트는
OPTIONS예비요청(Preflight)으로 403/404, 서버 로그는 조용함 - 로컬(프록시 없음)에서는 되는데 운영(프록시 있음)에서만 실패
핵심은 Preflight(OPTIONS)를 정확히 허용하고, Origin/Headers/Methods를 서버가 명시적으로 선언하도록 만드는 것입니다.
2) Spring Security 6 표준 설정
2-1) CorsConfigurationSource 한 곳에서 선언
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // SPA/토큰 기반이면 비활성(세션이면 다른 전략)
.cors(Customizer.withDefaults()) // <-- CorsConfigurationSource 사용
.authorizeHttpRequests(registry -> registry
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Preflight 허용
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(o -> o.jwt()); // 필요시
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration cfg = new CorsConfiguration();
cfg.setAllowedOrigins(List.of("https://app.example.com")); // 정확한 Origin
cfg.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
cfg.setAllowedHeaders(List.of("Authorization","Content-Type","X-Request-Id"));
cfg.setExposedHeaders(List.of("X-Request-Id")); // 클라이언트가 읽어야 하는 헤더
cfg.setAllowCredentials(true); // 쿠키/자격증명 사용시 true
cfg.setMaxAge(Duration.ofHours(1)); // Preflight 캐시 1h
UrlBasedCorsConfigurationSource src = new UrlBasedCorsConfigurationSource();
src.registerCorsConfiguration("/**", cfg);
return src;
}
}
중요: setAllowCredentials(true)를 쓰면 AllowedOrigins에 "*" 와일드카드는 쓸 수 없습니다.
정확한 Origin을 넣으세요.
2-2) 컨트롤러 레벨 애노테이션은 피하기
@CrossOrigin을 여기저기 붙이면 설정이 분산되어 추적/운영이 어려워집니다. 전역 빈으로 하나만 관리하세요.
3) Nginx 리버스 프록시에서 해야 할 일/하지 말아야 할 일
3-1) “하지 말아야 할” 중복 CORS
Nginx에서 add_header Access-Control-Allow-Origin * 같은 전역 헤더를 넣으면
Spring Security가 보낸 CORS 헤더와 충돌/불일치가 생깁니다.
Nginx는 CORS 헤더를 넣지 말고 프록시만 하세요.
3-2) 올바른 프록시 설정 예시
server {
listen 443 ssl;
server_name api.example.com;
# SSL 설정 생략(Certbot 등으로 구성)
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
# ❌ CORS 헤더를 여기서 add_header 하지 마세요.
# Spring Security가 전담하도록 둡니다.
}
}
단, 정적 파일을 Nginx가 직접 서빙한다면 정적 위치(location /assets/ 등)에 한해
별도 CORS 헤더를 줄 수 있습니다. API와 정적을 구분하세요.
4) Preflight가 꼭 통과해야 하는 이유
- 브라우저는 안전하지 않은 메서드/헤더에 대해 사전 확인(OPTIONS)을 합니다.
- 서버가 정확히 허용 범위를 회신하면 본 요청을 보냅니다.
- 프록시에서 OPTIONS를 301/302/403으로 만들거나, 보안 필터에서 막으면 항상 실패합니다.
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
Access-Control-Allow-Headers: Authorization,Content-Type,X-Request-Id
Access-Control-Allow-Credentials: true
Vary: Origin
Vary: Origin은 캐시 프록시가 Origin별로 응답을 구분하도록 하는 표준입니다(꼭 넣어야 함). Spring은 자동으로 넣어주지만, 프록시 캐시를 쓰면 헤더 보존을 확인하세요.
5) 흔한 함정 8가지
- Credentials=true + *: 보안상 불가. 정확한 Origin만 허용.
- OPTIONS 미허용: Security에서 OPTIONS 전체
permitAll(). - Nginx가 add_header로 덮어쓰기: API 위치에선 CORS 헤더 금지.
- 프런트와 API 도메인이 뒤섞임: 프런트는
https://app.example.com, API는https://api.example.com처럼 명확히. - 요청/응답 헤더 불일치: 프런트가 보낸
Access-Control-Request-Headers를 서버에서 허용했는지 확인. - 캐시 문제: CDN/프록시가 Preflight 응답을 잘못 캐시.
Vary: Origin필수. - 로컬만 되고 운영에서 실패: 운영 Nginx가 OPTIONS를 403/405 처리.
error_log로 확인. - 프록시 체인: 게이트웨이와 백엔드 모두 CORS를 켜면 충돌. 한 곳만 진실.
6) 점검 순서(체크리스트)
- 브라우저 네트워크 탭에서
OPTIONS→200/204인지 - 응답 헤더에
Access-Control-*,Vary: Origin포함 - Security 로그에서 요청 매칭/인가 룰 확인(디버그 시)
- Nginx 에러로그에 403/405가 없는지
7) FAQ
Q. 정적 파일은 Nginx, API는 백엔드인데 CORS 어디에 둘까요?
A. 역할 분리. 정적 위치엔 Nginx CORS, API 위치는 Spring Security CORS. 단, API에선 Nginx CORS 금지.
Q. 여러 프런트 도메인(운영/스테이징)을 허용하려면?
A. setAllowedOriginPatterns를 써서 패턴 기반(예: https://*.example.com) 허용이 가능합니다.
이 구성을 따르면 “로컬은 되는데 운영은 안 된다” 류의 CORS 문제를 근본적으로 줄일 수 있습니다.
'서버 인프라 실무' 카테고리의 다른 글
| 서버 첫 보안: SSH 포트 변경 + 공개키 로그인 + 루트 접속 차단(Ubuntu 24.04 기준) (0) | 2025.10.19 |
|---|---|
| Ubuntu 24.04 설치 USB 만들기 & 서버 최소 설치 옵션(보안·파티션·네트워크까지) (0) | 2025.10.18 |
| 리눅스 서버에 Resin WAS 올리기: 설치·보안·systemd·Nginx 연동(HTTPS)까지 (0) | 2025.10.17 |
| Tomcat 10 설치+튜닝 체크리스트: 커넥터/스레드풀/압축/보안 헤더 한 번에 정리 (0) | 2025.10.16 |
| 리눅스 서버에 Tomcat WAS 올리기: 설치·보안·systemd·Nginx 연동까지 한 번에 (0) | 2025.10.16 |