[Spring][Security] CORS

2 분 소요

들어가며

CORS 문제를 백엔드 단계에서 해결하는 과정을 기록하기 위한 포스팅

CORS란

Cross-Origin Resource Shaing의 약자로써 출처가 다른 서버의 리소스에 대한 접근을 통제하는 브라우저 메커니즘을 말한다. 출처라 함은 프로토콜 + 호스트 + 포트를 의미한다. OPTIONS(Origin, Access-Control-Request-Method 헤더값) 요쳥을 통해 response 헤더의 Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods 등으로 접근 가능 여부를 판단하고 실제 본 요청을 보낸다. 허용되지 않을 경우 403 에러가 발생한다. 이러한 요청 기작을 pre-flight라고 한다.

해결방법

프론트

헤더값에 Access-Control-Allow-Origin을 추가해주는 프록시 서버를 통해 우회적으로 서버 리소스에 접근이 가능하다.

백엔드

Spring Security의 CorsFilter을 활성화 하여 헤더값을 추가할 수 있다. 접근을 허용하고 싶은 Origin, Method, Header를 설정해주면 된다.

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter implements AuditorAware<String> {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.
                .cors()
                ...
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();

        corsConfiguration.addAllowedOrigin("http://localhost:3000");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);

        return source;
    }
}
public class CorsFilter extends OncePerRequestFilter {

    private final CorsConfigurationSource configSource;

    private CorsProcessor processor = new DefaultCorsProcessor();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
    		FilterChain filterChain) throws ServletException, IOException {

    	CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
    	boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
    	if (!isValid || CorsUtils.isPreFlightRequest(request)) {
    		return;
    	}
    	filterChain.doFilter(request, response);
    }
}
public class DefaultCorsProcessor implements CorsProcessor {

    private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);


    @Override
    @SuppressWarnings("resource")
    public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
                                  HttpServletResponse response) throws IOException {

        Collection<String> varyHeaders = response.getHeaders(HttpHeaders.VARY);
        if (!varyHeaders.contains(HttpHeaders.ORIGIN)) {
            response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
        }
        if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)) {
            response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
        }
        if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS)) {
            response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
        }

        if (!CorsUtils.isCorsRequest(request)) {
            return true;
        }

        if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
            logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
            return true;
        }

        boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
        if (config == null) {
            if (preFlightRequest) {
                rejectRequest(new ServletServerHttpResponse(response));
                return false;
            }
            else {
                return true;
            }
        }

        return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
    }

    // 실제 검증을 하는 메소드 -> 허용 가능한 Origin, Method, Header를 검사하고 헤더에 값을 추가한다.
    protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
                                     CorsConfiguration config, boolean preFlightRequest) throws IOException {

        String requestOrigin = request.getHeaders().getOrigin();
        String allowOrigin = checkOrigin(config, requestOrigin);
        HttpHeaders responseHeaders = response.getHeaders();

        if (allowOrigin == null) {
            logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
            rejectRequest(response);
            return false;
        }

        HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
        List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
        if (allowMethods == null) {
            logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
            rejectRequest(response);
            return false;
        }

        List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
        List<String> allowHeaders = checkHeaders(config, requestHeaders);
        if (preFlightRequest && allowHeaders == null) {
            logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
            rejectRequest(response);
            return false;
        }

        responseHeaders.setAccessControlAllowOrigin(allowOrigin);

        if (preFlightRequest) {
            responseHeaders.setAccessControlAllowMethods(allowMethods);
        }

        if (preFlightRequest && !allowHeaders.isEmpty()) {
            responseHeaders.setAccessControlAllowHeaders(allowHeaders);
        }

        if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
            responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
        }

        if (Boolean.TRUE.equals(config.getAllowCredentials())) {
            responseHeaders.setAccessControlAllowCredentials(true);
        }

        if (preFlightRequest && config.getMaxAge() != null) {
            responseHeaders.setAccessControlMaxAge(config.getMaxAge());
        }

        response.flush();
        return true;
    }
}

댓글남기기