환경 및 초기 셋팅
Kotlin/Spring 환경이며, CORS 정책은 WebMvcConfigurer의 메서드를 오버라이드해서 설정하였다.
모든 출처에 대해서 접근을 허용하였다. 운영할 때는 당연히 이처럼 설정하지는 않고, 초기 개발환경에서만 열어둔다.
@Configuration
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
}
}
Spring Security를 사용하지 않는 환경에서, 요청 시 Authorization 헤더에서 JWT 토큰을 받아 처리하는 간단한 인증 필터를 등록해둔 상태다.
('인증 필터가 존재한다'에 의를 두기 위함이므로, 인증 필터 세부 로직은 첨부하지 않겠다)
@Configuration
class FilterConfig {
@Bean
fun authenticationFilter(
jwtTokenProvider: JwtTokenProvider,
medicalStaffRepository: MedicalStaffRepository,
handlerExceptionResolver: HandlerExceptionResolver
): AuthenticationFilter {
return AuthenticationFilter(
jwtTokenProvider,
medicalStaffRepository,
handlerExceptionResolver
)
}
@Bean
fun authenticationFilterChain(
authenticationFilter: AuthenticationFilter
): FilterRegistrationBean<AuthenticationFilter> {
val registrationBean = FilterRegistrationBean<AuthenticationFilter>()
registrationBean.filter = authenticationFilter
registrationBean.order = 1
registrationBean.addUrlPatterns(
/**
* 인증된 유저만 접근해야하는 API Path
*/
)
return registrationBean
}
}
초기 설정을 통해 모든 요청은 먼저 인증 필터를 거치게 된다.
1. Preflight 요청은 Authorization 헤더를 포함하지 않는다.
분명 모든 출처에 대해서 접근을 허용하였는데, 계속 CORS 에러가 발생한다고 연락을 받았다. Postman 혹은 스웨거 문서에서 요청을 보내본 것이 다였으며, 모든 출처에 대해서 허용하였기에 CORS 에러가 발생할 거라고 예상하지 못했다.
CORS 에러가 발생하는 API들을 확인해본 결과, 인증 필터를 거치도록 설정한 API들이 문제였다.
실제 요청이 Authorization 헤더의 토큰을 포함한다고 하더라도, Preflight 요청은 Authorization 헤더를 포함하지 않았다.
때문에 Preflight 요청이 인증 필터를 넘지 못하고 401에러를 반환하였고, 브라우저 입장에서는 Preflight 요청이 통과되지 못했기에 허용되지 않은 출처로 판단한 것이였다.
해결 할 수 있는 방법은 다음과 같았다
- CORS 로직을 CorsFilter를 통해 수정
- 인증 필터에서 OPTION 메서드일 경우 건너뛰도록 수정
결과적으로 나는 1번의 방법인 CorsFilter를 선택하였다.
필터의 일일이 아래와 같은 로직을 수행하는게 보기좋지 않다고 생각하였다.
// CORS preflight 요청(OPTIONS)은 권한 체크 없이 통과
if (request.method == "OPTIONS") {
filterChain.doFilter(request, response)
return
}
WebMvcConfigurer의 메서드 오버라이딩을 통한 CORS 설정은 삭제하고, 다음과 같아 Filter를 추가하였다.
CorsFilter의 순서는 기존의 인증 필터보다 앞에서 먼저 수행되도록 하였다.
@Configuration
class FilterConfig {
@Bean
fun corsFilter(): FilterRegistrationBean<CorsFilter> {
val source = UrlBasedCorsConfigurationSource()
val config = CorsConfiguration()
config.allowCredentials = true
config.addAllowedOrigin("http://localhost:3022")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
config.maxAge = 3600L
source.registerCorsConfiguration("/**", config)
val registrationBean = FilterRegistrationBean(CorsFilter(source))
//순서는 인증 필터보다 앞에 등록
registrationBean.order = 0
return registrationBean
}
..
}
중간 결과
프론트엔드에서 CORS 에러가 발생하지 않았고, 잠시 평화를 되찾을 수 있었다.
2. Nginx 추가 시 설정
ssl 인증서를 위해 Nginx를 리버스 프록시로 두고 배포하였을 때, 또 다시 CORS 에러가 발생하였다.
해결을 위해 다음 중에서 선택할 수 있을 것이다.
- CORS 처리를 Nginx에서 하도록 이관
- Preflight 요청의 헤더를 Nginx에서 담아서 전달
리버스 프록시에 CORS 처리를 이관시켜서 관리 포인트를 분리시키는것보다는 서버에서 CORS 처리를 하도록 유지하는 것이 낫다고 판단하였다.
Nginx에서는 요청이 서버로 넘어갈 때, 헤더를 추가하여 요청을 넘기도록 하였다.
server {
server_name example.kr;
#access_log /var/log/nginx/host.access.log main;
location / {
proxy_pass http://example-container: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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
### -> CORS preflight 요청을 위한 Origin 헤더 전달
proxy_set_header Origin $http_origin;
proxy_set_header Access-Control-Request-Method $http_access_control_request_method;
proxy_set_header Access-Control-Request-Headers $http_access_control_request_headers;
# 타임아웃 설정
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
.
.
}
정리
1편에서는 CORS에 대한 개념들과 흐름을 정리해보았고, 2편에서는 CORS 설정을 하면서 맞은 상황과 내가 선택한 해결책을 정리해보았다.
CORS에 대한 디테일한 것들을 다시금 돌아볼 수 있었고, CORS와의 전쟁을 치르고 있는 분이 있다면, 이 글이 종전의 시작이 되길 바란다.
'Web' 카테고리의 다른 글
| CORS란? - CORS (1) (0) | 2025.12.18 |
|---|---|
| [WebSocket] 웹 소켓 연결 시 검증은 어디서 해야 할까? (0) | 2024.03.27 |