프론트랑 협업을 굉장히 오랜만에 했던 프로젝트라서 CORS 라는 개념에 대해 완전히 잊고 살았다
@CrossOrigin
어노테이션으로 해결되지 않았던 CORS 에러를 Filter를 사용해 구현하였습니다.
CORS는 웹 브라우저가 다른 출처의 리소스를 요청할 때 사용하는 메커니즘으로, 사용자 보안을 위해 도입된 정책입니다. 이 정책은 악의적인 웹사이트가 사용자의 데이터를 탈취하는 것을 방지합니다. CORS 정책에 따라 웹 브라우저는 다른 출처의 리소스를 요청할 때 'Origin'
헤더를 포함하여 서버에 요청을 보냅니다. 서버는 이 요청을 검토하여 해당 출처의 요청을 허용할지 결정합니다.
서버가 요청을 허용하면, 'Access-Control-Allow-Origin'
헤더를 응답에 포함시켜 브라우저에 전달합니다. 이 헤더가 없거나 요청한 출처를 허용하지 않는 경우, 브라우저는 리소스를 반환하지 않습니다.
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
log.info("CorsFilter.doFilter START");
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String origin = request.getHeader("Origin");
log.info("CorsFilter Origin: " + origin);
if (!allowedOrigin.contains(origin)) {
chain.doFilter(req, res); // allowedOrigin 목록에 없는 경우, 필터 체인으로 요청을 계속 진행
return;
}
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, provider");
response.setHeader("Access-Control-Expose-Headers", "Authorization, provider");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
log.info("CorsFilter.doFilter OPTION called");
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
}
allowedOrigin
목록을 HashSet
을 활용하여 초기화합니다.allowedOrigin
목록에 없는 origin
인 경우, 요청을 계속 진행합니다.Access-Control-Allow-Origin
: 요청을 허용할 출처(origin)를 결정합니다. origin
변수에 허용할 도메인이 들어갑니다.Access-Control-Allow-Credentials
: 자격 증명(예: 쿠키, 인증 헤더 등)을 포함한 요청을 허용할지 여부를 결정합니다. true
로 설정하면 자격 증명을 포함한 요청이 허용됩니다.Access-Control-Allow-Methods
: 클라이언트가 서버로 요청할 때 사용할 수 있는 HTTP 메서드를 지정합니다.Access-Control-Max-Age
: 브라우저가 프리플라이트 요청의 결과를 캐시할 수 있는 시간을 초 단위로 지정합니다.Access-Control-Allow-Headers
: 클라이언트가 서버로 요청을 보낼 때 사용할 수 있는 헤더를 지정합니다.Access-Control-Expose-Headers
: 클라이언트가 서버 응답에서 접근할 수 있는 헤더를 지정합니다.OPTIONS
메서드로 요청이 온 경우 OK
응답을 반환합니다."Preflighted" 요청은 “simple requests”와는 달리, 먼저 OPTIONS
메서드를 통해 다른 도메인의 리소스로 HTTP 요청을 보내 실제 요청이 전송하기에 안전한지 확인합니다. Cross-origin 요청은 사용자 데이터에 영향을 줄 수 있기 때문에 이와 같이 미리 전송(preflighted)합니다.
구현한 필터를 Config
에 등록해야 필터를 거쳐 서버가 실행됩니다.
@Configuration
public class AppConfig {
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistration(CorsFilter filter) {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setOrder(1);
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns("/api/v1/*"); // 적절한 URL 패턴으로 변경해야 함
return registrationBean;
}
@Bean
public FilterRegistrationBean<AuthenticationFilter> accessTokenFilterRegistration(AuthenticationFilter filter) {
FilterRegistrationBean<AuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setOrder(3);
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns("/api/v1/chats/rooms/*", "/api/v1/users/*"); // 필터를 적용할 URL 패턴 지정
return registrationBean;
}
@Bean
public FilterRegistrationBean<ExceptionHandlerFilter> exceptionHandlerFilterRegistration(ExceptionHandlerFilter filter) {
FilterRegistrationBean<ExceptionHandlerFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setOrder(2);
registrationBean.setFilter(filter);
return registrationBean;
}
}
Spring Security를 사용하지 않기 때문에 인증 과정 또한 필터로 구현하게 되었습니다. 따라서 총 세 가지 필터가 등록되었습니다: CorsFilter, ExceptionHandlerFilter, AuthenticationFilter 순서입니다. 필터의 적용 순서는 중요하며, 클라이언트의 요청에 대한 보안 처리가 가장 앞단에서 이루어져야 합니다. 따라서 CORS 처리가 가장 먼저 이루어지도록 설정했습니다. 인증 필터에서 발생하는 예외는 @ControllerAdvice
어노테이션으로 처리할 수 없어서, 예외 처리 필터를 추가로 구현하여 인증 필터를 통과하도록 설정했습니다.