catsridingCATSRIDING|OCEANWAVES
Dev

Spring Security CORS 설정하기

jynn@catsriding.com
Nov 13, 2023
Published byJynn
999
Spring Security CORS 설정하기

Configure CORS Policy with Spring Security

웹 애플리케이션에서 서버로 API를 호출할 때 서버와 클라이언트의 출처가 서로 다른 경우 브라우저의 CORS 정책에 위배되어 CORS 에러가 발생합니다.

이번 포스팅에서는 CORS란 무엇이며 Spring Security를 통해 CORS 정책을 설정하는 방법에 대해 살펴봅니다.

CORS Policy

CORS란 Cross-Origin Resource Sharing의 약자로 번역하면 교차 출처 리소스 공유입니다.

Cross-Origin Resource Sharing
CORS 교차 출처 리소스 공유는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처와 다를 때 교차 출처 HTTP 요청을 실행합니다.
© MDN

브라우저는 보안적인 이유로 다른 출처에 대한 요청을 기본적으로 제한하고 있습니다. 그래서 웹 애플리케이션에서 외부 서버의 특정 자원에 대해 접근을 하기 위해서는 해당 서버의 승인이 필요합니다.

CORS 동작 방식

  • 이 정책은 브라우저에서 지정한 스펙입니다. 따라서, 브라우저 위에서 동작하는 웹 애플리케이션은 반드시 이행해야 합니다.
  • JavaScript로 발생하는 요청에 대해서만 적용되며, HTML 태그로 인한 요청에는 적용되지 않습니다.
  • JavaScript를 통해 서버에 요청을 보낼 때 브라우저에서 자동으로 출처(Origin)를 확인합니다. 서로 출처가 다른 경우 요청 헤더에 Origin 출처를 담아 HTTP 요청을 전송합니다.
  • 요청을 받은 서버는 처리를 완료하고 응답할 때 Access-Control-Allow-Origin과 같은 헤더를 함께 담아 전송합니다.
  • 브라우저는 서버의 응답 헤더를 비교하여 최종적으로 CORS 정책 통과 여부를 판단합니다.

이 CORS 정책은 브라우저에서 정한 스펙이다 보니, 브라우저 단에는 CORS 에러가 발생하더라도 서버에서는 정상적으로 요청을 처리한 것으로 나타납니다.

Origin 비교

CORS 이야기를 하면서 계속해서 등장하는 출처(Origin)에 대하여 명확한 기준을 확인해둘 필요가 있습니다. 브라우저는 요청하는 곳의 출처와 현재 출처를 비교할 때 URL의 세 가지 구성 요소를 기반으로 판단합니다.

  • Scheme: HTTP 또는 HTTPS
  • Domain: 도메인 주소
  • Port: 애플리케이션 포트

만약 세 가지 구성 요소중 하나라도 일치하지 않는다면, 브라우저는 서로 다른 출처라고 판단합니다.

⛔ https://catsriding.dev ≠ http://catsriding.dev  <- Scheme이 다른 경우
⛔ https://app.catsriding.dev ≠ https://www.catsriding.dev  <- Domain이 다른 경우
⛔ https://catsriding.dev:8080 ≠ https://catsriding.dev:3000  <- Prot가 다른 경우

CORS 정책 검증

CORS 정책을 검증하는 방식은 HTTP 요청 스펙에 따라 구분하여 진행됩니다.

Simple Request

브라우저가 정한 특정 조건을 만족할 경우, JavaScript로 작성된 요청은 Simple Request(기본 요청) 방식으로 처리됩니다. 이 방식은 아래에 제시된 HTTP 요청 스펙을 따르며, 주로 과거에 설계된 API들에서 주로 사용되었습니다.

  • HTTP Methods
    • GET
    • POST
    • HEAD
  • HTTP Headers
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width

이 Simple Request 방식은 서버로 한 번의 본 요청만을 보내며, 서버에서 내려온 응답 헤더의 값을 통해 CORS 정책을 검증하여 최종적으로 통과 여부를 확인하는 방식입니다.

오래 전에 구현된 API가 이 조건에 해당하는 이유는 Content-Type으로 JSON 포맷을 지원하지 않기 때문입니다. 현재 널리 사용되는 REST API는 다음 Preflight Request 방식이 적용됩니다.

Preflight Request

Simple Request 방식의 조건에 벗어나는 경우에는 모두 이 Preflight Request(예비 요청)방식으로 진행됩니다. Preflight의 사전적 의미처럼, 브라우저에서 서버로 요청을 보낼 때 본 요청에 앞서 자동으로 예비 요청을 먼저 전송합니다. 결과적으로 API 호출 시 총 두 번의 요청이 서버로 전송됩니다.

이 예비 요청에는 OPTIONS 라는 HTTP 메서드가 사용되며, 단순히 출처에 대한 정보 이외에도 본 요청에 대한 정보들이 함께 포함됩니다.

OPTIONS https://app.catsriding.dev/v1/users HTTP/1.1
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: http://localhost:3000

만약 서버에서 이 출처를 허용하고 있는 경우에는 Access-Control-Allow-Methods 응답 헤더가 전달됩니다.

HTTP/1.1 200 OK
Connection: keep-alive
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

서버에서 요청자의 출처를 허용하고 있지 않다면, 브라우저는 CORS 에러를 발생시키고 서버로 부터 내려온 응답을 사용하지 않고 폐기합니다.

참고로, Preflight Request는 성공하였지만 실제 요청에서 CORS 에러가 발생할 수도 있습니다. Preflight Request 이후에 본 요청을 전송하면서 두 요청간의 시간차가 발생하여 그동안 추가된 정보가 있거나 서버에 변동 사항이 있을 수 있기 때문입니다.

CORS 에러

웹 서비스를 개발하다 보면 누구나 한 번쯤은 마주하게 되는 그런 에러 메시지가 있습니다.

Access to fetch at 'https://app.catsriding.dev/v1/users' from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

If an opaque response serves your nees, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

에러 메시지를 간단히 요약하자면 app.catsriding.dev 서버에서 localhost:3000 출처에 대한 접근을 허용하고 있지 않다는 의미입니다. 이를 해결하기 위해서는 서버에서 localhost:3000 출처에 대한 접근을 허용해주어야 합니다.

CORS Configuration

CORS 에러를 해결하기 위해서는 서버에서 CORS 정책과 관련된 설정을 추가해야 합니다.

- `Access-Control-Allow-Origin`: 허용되는 출처 설정
- `Access-Control-Allow-Methods`: 허용되는 HTTP 메서드 설정
- `Access-Control-Allow-Headers`: 허용되는 HTTP 헤더 설정
- `Access-Control-Allow-Credentials`: 쿠키 또는 토큰을 통한 사용자 자격 증명 포함 허용 여부 설정
- `Access-Control-Max-Age`: Preflight의 캐시 유효 시간 설정

Spring Security는 CorsConfigurer를 통해 CORS 정책과 관련된 설정을 처리하고 있습니다.

  • CorsConfigurer는 Spring Security 필터 체인에 CorsFilter를 추가합니다.
  • corsFilter라는 이름의 Bean이 제공이 된다면 이 필터가 우선적으로 사용됩니다.
  • 만약 corsFilter 이름으로 등록된 빈이 없다면 다음은 corsConfigurationSource 빈이 정의되었는 지 확인하고, 등록된 경우 이 값을 CORS 설정값으로 사용합니다.
  • corsConfigurationSource 빈도 정의되어 있지 않은 경우, Spring MVC가 클래스 경로에 있으면 HandlerMappingIntrospector가 사용됩니다.

CorsFilter

CorsConfigurer가 동작하는 순서대로 CorsFilter 빈을 등록하여 CORS 이슈를 해결하는 방법을 먼저 살펴보겠습니다.

CORS 설정을 담당할 CorsConfig 클래스를 추가하고 CorsFilter 빈을 등록합니다.

CorsConfig.java
@Configuration  
public class CorsConfig {  
  
    @Bean  
    public CorsFilter corsFilter() {  
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();  
        CorsConfiguration corsConfiguration = new CorsConfiguration();  
        corsConfiguration.setAllowCredentials(true);  
        corsConfiguration.addAllowedOrigin("*");  
        corsConfiguration.addAllowedHeader("*");  
        corsConfiguration.addAllowedMethod("*");  
        corsConfiguration.setMaxAge(1800L);  
  
        source.registerCorsConfiguration("/v1/**", corsConfiguration);  
        return new CorsFilter(source);  
    }  
}
  • setAllowCredentials(): 요청에 사용자의 자격 증명이 담긴 쿠키나 토큰이 포함 가능한지 여부를 설정합니다.
  • addAllowedOrigin(): 패턴으로 추가된 출처만 브라우저에서 리소스에 접근할 수 있도록 허용합니다.
  • addAllowedOriginPattern(): CORS 스펙에서 모든 출처 허용과 같은 * 와일드카드가 규정되어 있지는 않습니다. 그래서 사용자 인증을 위해 setAllowCredentials(true)로 설정해야 하는 경우에는 addAllowedOrigin() 대신에 이 메서드를 사용해야 합니다.
  • addAllowedHeader(): 허용하는 HTTP 헤더를 설정합니다.
  • addAllowedMethod() : 허용하는 HTTP 메서드를 설정합니다.
  • setMaxAge(): Preflight 요청에 대한 캐시 유효시간을 설정합니다.
  • registerCorsConfiguration(): 이 CORS 설정을 적용할 URL 패턴을 지정합니다.

CORS 필터를 빈에 등록했다면 다음은 Spring Security 필터 체인에 이 필터를 추가합니다.

SecurityConfig.java
@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
public class SecurityConfig {  
  
    private final CorsFilter corsFilter;  
  
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
  
        http  
                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class);
                  
        return http.build();   
    }  
}

CorsConfigurationSource

이번에는 CorsFilter 등록 대신 CorsConfigurationSource 빈을 등록하여 CORS 관련 정책을 설정하는 방법입니다. CorsFilter를 등록하는 방식과 크게 다르지 않습니다.

SecurityConfig.java
@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
public class SecurityConfig {  

    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
  
        http  
                .cors(customizer -> customizer.configurationSource(corsConfigureSource()));  
    
        return http.build();  
    }  
  
    @Bean  
    public CorsConfigurationSource corsConfigureSource() {  
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();  
        CorsConfiguration corsConfiguration = new CorsConfiguration();  
        corsConfiguration.setAllowCredentials(true);  
        corsConfiguration.addAllowedOriginPattern("*");  
        corsConfiguration.addAllowedHeader("*");  
        corsConfiguration.addAllowedMethod("*");  
        corsConfiguration.setMaxAge(1800L);  
        corsConfiguration.addExposedHeader(AUTHORIZATION);  
  
        source.registerCorsConfiguration("/v1/**", corsConfiguration);  
        return source;  
    }  
}
  • Spring
  • Security