제목과 같이 Preflight (Options) 는 성공하지만 실제 요청에서 Cors 가 발생하는 이슈가 생겨, 이에 대해 정리한 글이다.
이슈 당시 확인해 본 사항
CORS 발생 당시 아래 2개 최우선으로 확인했다.
- Access-Control-Allow-Origin 과 Access-Control-Allow-Credentials
2개를 왜 확인해야 하는지, 개념을 간략하게 정리해보면 아래와 같다.
Access-Control-Allow-Origin 는 없거나 다르다면 브라우저에서 응답 자체를 막아버리므로 CORS 오류가 발생한다.
Access-Control-Allow-Credentials 는 session 인증의 경우 1) 에 jwt 기반 인증의 경우 2) 에 영향을 미치며 마찬가지로 설정이 없으면 CORS 오류가 발생한다.
- JSESSIONID 등 쿠키 SameSite=None일 때만 cross-origin에 포함 가능
- Authorization 헤더 (Bearer 토큰 등)
마지막으로 Access-Control-Allow-Origin: * 과 Access-Control-Allow-Credentials: true 를 같이 쓸 경우 모든 출처에 인증정보를 공유하는 것은 브라우저 정책상 거부되므로 CORS 가 발생한다. 따라서 credentials를 허용할 거면 반드시 명시적인 Origin을 지정해야 한다.
이를 위해, 서버와 클라이언트에서 다음 각각을 확인했다.
서버에서 Security 의 CORS 설정이 올바른지 확인했다. (Security 를 사용하지 않는다면, WebConfig 의 Cors 설정)
- allowedOrigins 가 잘 설정되었는지
- allowedMethods 에 누락이 없는지
- allowedHeaders 에 지정되지 않은 헤더로 요청을 보내는지
- allowCredentilas 를 true 로 지정했는지
여기서 추가적으로 session 기반 인증에 쿠키설정이 secure true 이므로 https 로 요청한 것이 맞는지 (로컬호스트라면 https://localhost: ) 확인했다.
여기까지 점검하고, 클라이언트가 axios 를 사용하고 있으므로 withCredentials 옵션을 붙였는지 확인했다.
확인 결과 이상이 없었다.
OPTIONS 와 비교
혹시 몰라 http.cors 누락도 확인해보았지만 존재하는 것으로 보아, CorsFilter 가 없어서 발생하는 문제는 아닌 것으로 판단했고, 서버 코드 어딘가에서 문제가 생겼을 것이라 추측했다.
로컬서버에 요청을 넣어서 뭐가 문제인지 확인한 결과. 아래처럼 OPTIONS 에는 Access-Control-Allow-Origin 과 Access-Control-Allow-Credentials 가 있는데, POST 로 요청을 보냈을 때는 존재하지 않는 것을 확인했다.
이슈 당시 해결 방안
디버깅을 진행하기 전, 클라이언트의 원활한 작업을 위해서는 문제를 빠르게 해결해야 했기 때문에 nginx 에서 설정을 추가해 CORS 문제가 발생하지 않도록 조치했다.
아래처럼 map 을 이용해 cors_origin 을 지정하고 여기에 아래처럼 헤더부분에 지정하는 방식으로 구성했다.
map $http_origin $cors_origin {
default "";
"https://baroit.site" $http_origin;
"https://ceo.baroit.site" $http_origin;
...
}
server {
listen 443 ssl;
server_name .site;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
add_header 'Access-Control-Allow-Origin' "$cors_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
if ($request_method = 'OPTIONS') {
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain; charset=UTF-8';
add_header 'Access-Control-Max-Age' 1728000;
return 204;
}
}
}
결과적으로는 문제를 해결할 수 있었다.
왜 OPTIONS 는 성공하는데 실제 요청은 실패했을까?
nginx 구성을 통해 CORS 설정은 해결했지만, 원인을 찾지 못한 것이 걸려서 혹시? 하는 마음에 필터체인 등록순서를 확인해보았다.
현재 프로젝트에는 사장님 사이트와 고객사이트가 나누어져있고, 한 서버에서 처리하고 있으므로 SecurityFilter 를 아래와 같이 각각 구성해 놓은 상태다.
확인 결과 CorsFilter 이전에 커스텀 필터가 위치하고 있으므로 필터 체인 등록 위치가 잘못되었음을 파악했다.
올바르게 아래처럼 CorsFilter 이후에 위치하도록 고치고
다시 확인해보면 헤더가 정상적으로 붙는다.
CORS 문제가 발생한다면, CORS 설정 뿐만 아니라 FilterChain 을 구성할 때 각 Filter 가 어디에 위치하는지 추가로 확인해주는 것이 좋을 것 같다.
'Spring' 카테고리의 다른 글
[Spring] WireMock 로 Feign (ApacheHttp5Client) 테스트시 NoHttpResponseException 발생 케이스와 해결방법 (0) | 2025.05.06 |
---|---|
[Spring] TestContainers, @TestConfiguration 사용시 @DynamicPropertySource 가 적용되지 않는 문제와 해결방안 (0) | 2025.04.10 |
[Spring] API 응답에서 직접 정의한 Error code 는 왜 사용할까? (0) | 2025.03.16 |
[Spring] OpenApI 3.0 Swagger 문서 작성 및 설정 방법 (1) | 2025.02.11 |
[SpringBoot] MapStruct 제대로 활용하기 (0) | 2024.11.03 |