CORS( Cross-Origin Resource Sharing )

뾰족머리삼돌이·2024년 8월 25일
0

Spring Security

목록 보기
9/16

브라우저는 SOP( Same-Origin Policy ) 에 의해 서로다른 Origin 간에 리소스 공유를 금지한다.
여기서 Origin은 URI의 Scheme, 호스트, 포트가 모두 동일해야 동일한 Origin 으로 취급된다.

프로토콜과 스킴은 다른가?

즉, 스크립트를 통해 서로다른 웹 페이지간에 민감한 정보에 접근할 수 없다는 말이다.
CORS( Cross-Origin Resource Sharing )는 이러한 SOP를 특정 Origin을 대상으로는 완화해주는 설정을 의미한다.

CORS 동작방식

기본적으로 CORS 에러를 판별하는 주체는 브라우저다.

  1. 요청 헤더에 Origin으로 현재 출처를 표시하여 요청은 전송
  2. 서버에서 Access-Control-Allow-Origin 헤더로 접근이 허용된 출처를 안내
  3. 브라우저에서 유효한 응답인지를 결정한다.

의 절차를 거쳐 CORS 에러를 발생시킬지 결정한다.
CORS 접근이 허용된 요청이 아닌경우, 403 Forbidden 응답이 반환된다.

MDN 문서에서 소개하는 세 가지의 요청 시나리오를 살펴보자

단순 요청

요청 메서드가 GET, HEAD, POST 중 하나이면서,
요청 헤더에는 Accept, Accept-Language, Content-Language, Content-Type 가 허용된다.
(단, Content-Type의 값은 application/x-www-form-urlencoded, multipart/form-data, text/plain 이어야 함)

위 조건에 맞게 Origin 헤더를 포함한 요청을 보내면 서버측에서 CORS설정과 대조하여 응답을 내려준다

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example

예를들어, 위 요청은 https://foo.example에서 bar.other/resources/public-data/ 로의 GET 요청을 의미한다.
단순 요청의 조건을 모두 충족했으므로, 시나리오에 따라 아래와 같이 응답이 도착한다

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[…XML Data…]

이제 브라우저에서 Origin과 응답의 Access-Control-Allow-Origin를 대조하여 CORS 에러를 발생시킬지 결정한다.

Preflighted 요청

요청 메서드가 OPTIONS 이면서,
요청 헤더에 OriginAccess-Control-Request-Method 가 포함된 요청을 사전요청(pre-flight) 이라 부른다.

실제 요청이전에 사전요청을 보내어 실제 요청이 가능한지를 판단하고, 가능한 상황에서만 실제 요청을 서버로 전송한다.

OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example									// 요청 주체
Access-Control-Request-Method: POST							// 실제 요청 메서드
Access-Control-Request-Headers: content-type,x-pingother	// 실제 요청에 포함될 헤더

HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example			// 허용하는 Origin
Access-Control-Allow-Methods: POST, GET, OPTIONS			// 허용하는 메서드
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type		// 허용하는 헤더
Access-Control-Max-Age: 86400								// 현재 사전요청의 캐시기간
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

예비요청과 그에대한 응답을 나타내는 예시이다.
응답에 포함된 정보들이 실제 요청과 일치하면 서버로 실제 요청을 보낸다.

Credentials을 포함한 요청

Credentials을 포함한 요청은 쿠키나 Authorization 헤더가 포함된 요청을 의미한다.
서버측에서 응답헤더에 Access-Control-Allow-Credentials: true를 포함하지 않으면 응답은 무시된다.

credential이 포함된 요청에 응답할 때는 아래 헤더들에 *를 지정해서는 안된다.

  • Access-Control-Allow-Origin
    와일드카드가 사용되면 CORS 에러가 발생하고 Set-Cookie가 쿠키를 설정하지 않는다
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Expose-Headers

Spring MVC와 CORS

Spring MVC에서 CORS관련 동작을 시작하는 클래스는 CorsFilter이며,
실제 동작은 DefaultCorsProcessor에서 이뤄진다.

동작과정에서 CORS 설정을 읽어와 CorsConfiguration 타입의 인스턴스를 생성하는데,
이 인스턴스를 생성하는데 관여하기 위해서는 @CrossOrigin 혹은 전역 설정이 필요하다.

@CrossOrigin

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {

	@AliasFor("origins")
	String[] value() default {};

	@AliasFor("value")
	String[] origins() default {};

	String[] originPatterns() default {};

	String[] allowedHeaders() default {};

	String[] exposedHeaders() default {};

	RequestMethod[] methods() default {};

	String allowCredentials() default "";

	String allowPrivateNetwork() default "";

	long maxAge() default -1;

}

이 애노테이션은 컨트롤러나 컨트롤러 메서드에 설정할 수 있다.

기본적으로 모든 Origin과 Header, Method를 허용하며, 자격증명을 포함한 요청은 기본적으로 비활성화 되어있다.
maxAge는 30분으로 설정되어있다.

전역 설정

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {

		registry.addMapping("/api/**")
			.allowedOrigins("https://domain2.com")
			.allowedMethods("PUT", "DELETE")
			.allowedHeaders("header1", "header2", "header3")
			.exposedHeaders("header1", "header2")
			.allowCredentials(true).maxAge(3600);

		// Add more mappings...
	}
}

전역 설정은 기본적으로 모든 Origin과 Header를 허용하며, 메서드는 GET, HEAD, POST만 허용한다.
자격증명을 포함한 요청은 기본적으로 비활성화 되어있고, maxAge는 30분으로 설정되어있다.

Spring 시큐리티와 CORS

CORS 시나리오 중 하나인 pre-flight 요청에는 쿠키같은 자격증명 정보가 포함되지 않으므로,
CORS는 스프링 시큐리티의 동작전에 동작한다.

스프링 시큐리티에서 사용자는 CorsConfigurationSource를 통해 CorsFilter를 스프링 시큐리티와 통합시킬 수 있다.

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
    configuration.setAllowedMethods(Arrays.asList("GET","POST"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

스프링 시큐리티는 위 코드처럼 UrlBasedCorsConfigurationSource 인스턴스가 Bean으로 제공되면, 자동으로 CORS 설정을 구성한다.

만약, Spring MVC의 전역 설정이 구성되어있다면 CORS 설정을 생략해도 된다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

	@Bean
	@Order(0)
	public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
		http
			.securityMatcher("/api/**")
			.cors((cors) -> cors
				.configurationSource(apiConfigurationSource())
			)
			...
		return http.build();
	}

	@Bean
	@Order(1)
	public SecurityFilterChain myOtherFilterChain(HttpSecurity http) throws Exception {
		http
			.cors((cors) -> cors
				.configurationSource(myWebsiteConfigurationSource())
			)
			...
		return http.build();
	}

	CorsConfigurationSource apiConfigurationSource() {
		CorsConfiguration configuration = new CorsConfiguration();
		configuration.setAllowedOrigins(Arrays.asList("https://api.example.com"));
		configuration.setAllowedMethods(Arrays.asList("GET","POST"));
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration);
		return source;
	}

	CorsConfigurationSource myWebsiteConfigurationSource() {
		CorsConfiguration configuration = new CorsConfiguration();
		configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
		configuration.setAllowedMethods(Arrays.asList("GET","POST"));
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration);
		return source;
	}

}

또한, 다수의 CorsConfigurationSource Bean을 등록한다면 .cors() DSL로 동작할 CORS 설정을 지정해줘야한다.

0개의 댓글