(위키북스) Spring Security in Action
[ ▶ CORS ]
기본적으로 브라우저는 사이트가 로드된 도메인 이외의 도메인에 대한 요청은 허용하지 않는다. 그렇기에 CORS (교차 출처 리소스 공유) 로 정책을 완화하여 일부 조건에서 서로 다른 출처 간의 요청을 허용하게 한다.
특히 대다수의 프론트엔드와 백엔드는 별도의 애플리케이션으로 구성되기 때문에 더욱 중요한 기능이다.
[ ▷ CORS 작동 방식 ]
애플리케이션이 두 개의 서로 다른 도메인 간에 호출하는 것은 모두 금지된다. 하지만 그러한 호출이 필요할 때가 있다. 이떄 CORS를 이용하여 애플리케이션이 요청을 허용할 도메인, 그리고 공유할 수 있는 세부 정보를 지정할 수 있다.
CORS 매커니즘은 HTTP 기반이며 주요한 헤더는 다음과 같다.
- Access-Control-Allow-Origin: 도메인의 리소스에 접근할 수 있는 외부 도메인(원본)을 지정한다.
- Access-Control-Allow-Methods: 다른 도메인에 대해 접근을 허용하지만 특정 HTTP 방식만 허용하고 싶을 때 일부 HTTP 방식을 지정할 수 있다. 예를 들어 example.com이 일부 엔드포인트를 호출할 수 있게 하면서 HTTP GET만 허용할 수 있다.
- Access-Control-Allow-Headers: 특정 요청에 이용할 수 있는 헤더에 제한을 추가한다.
스프링 시큐리티는 보통 이러한 헤더를 응답에 추가하지 않는다. 애플리케이션에서 CORS를 구성하지 않고 교차 출처 호출을 할 경우 애플리케이션은 요청하고 응답을 받을 때 여기에 서버가 수락하고 출처가 나열된 Access-Control-Allow-Origin 헤더가 있을 것이라고 예상한다. 만약 이 헤더가 없다면 브라우저는 응답을 수락하지 않는다.
CORS가 권한 부여나 CSRF 보호와 비슷한 제한이라고 생각할 수 도 있지만 틀렸다. CORS는 제한을 가하기보다 교차 도메인 호출의 엄격한 제약 조건을 완화하도록 도와주는 기능이다.
또한, 제한이 적용되어도 일부 상황에서는 엔드포인트를 호출할 수가 있다.
△ 사전 요청 (Preflight)
종종 브라우저는 요청을 허용해야 하는지 테스트하기 위해 먼저 HTTP OPTIONS 방식으로 호출하는 경우가 있다. 이 테스트 요청을 이라고 한다. 이 요청이 실패하면 브라우저는 원래 요청을 수락하지 않는다.
사전 요청을 할지 결정하는 것은 브라우저의 책임이고 개발자가 이 논리를 구현해야 하는 것은 아니다. 하지만 특정 도메인에서는 CORS 정책을 지정하지 않았음에도 백엔드에 대해 교차 출처 호출을 하는 경우가 있다.
HTTP 방식이 GET, POST, 또는 OPTIONS일 때 브라우저가 사전 요청을 생략하면 몇 가지의 기본헤더만 가지게 된다.
[ 공식 문서: https://fetch.spec.whatwg.org/#simple-cross-origin-request-0 ]
△ 정리
브라우저는 출처가 응답에 지정되지 않으면 응답을 수락하지 않는다. CORS 메커니즘은 결국 브라우저에 관한 것이며 엔드포인트를 보호하는 방법은 아니다. CORS 메커니즘이 유일하게 보장하는 것은 허용하는 출처 도메인만 브라우저의 특정 페이지에서 요청을 수행할 수 있다는 것이다.
[ ▷ @CrossOrigin 어노테이션으로 CORS 정책 적용 ]
@CrossOrigin 어노테이션은 다른 도메인에서의 요청을 허용하도록 CORS를 구성할 수 있다. 엔드포인트를 정의하는 메서드 바로 위에 @CrossOrigin 어노테이션을 배치하고 허용된 출처와 메서드를 이용해 구성할 수 있다.
애플리케이션에서 교차 출처 호출이 작동하게 하려면 컨트롤러 클래스에서 test() 메서드 위에 @CorssOrigin 어노테이션만 추가하면된다.
@PostMapping("/test")
@ResponseBody
@CrossOrigin("http://localhost:8080") // localhost 출처에 대한 교차 출처 호출을 허용한다.
public Stirng test() {
logger.info("Test method called");
return "HELLO";
}
@CrossOrigin 값 매개 변수는 여러 출처를 정의하는 배열을 받는다. 예를 들어 @CorrOrigin({"example.com", "example.org"} 와 같이 지정할 수 있다. 또한 어노테이션의 allowHeaders 특성과 method 특성으로 허용되는 헤더와 메서드를 설정할 수 있다.
출처와 헤더에 별표(*)를 이용하면 모든 헤더나 출처를 지정할 수 있지만 사용에 주의해야 한다. 허용하려는 출처와 헤더를 필터링하고 어떤 도메인이든 애플리케이션의 리소스에 접근하는 코드를 구현하는 것은 허용하지 않는 것이 좋다.
모든 출처를 허용하면 애플리케이션이 XSS (교차 사이트 스크립트) 요청에 노출되거나 DDoS 공격에 취약해질 수 있다. 테스트 환경에서도 모든 출처를 허용하지 않는 것이 바람직하다. 테스트와 운영에 같은 데이터 센터를 이용하는 잘못 정의된 인프라에서 애플리케이션이 실행되는 경우가 있다. 보안이 적용되는 모든 계층을 독립적으로 처리해야 하고 인프라가 허용되지 않으므로 애플리케이션에 특정 취약성이 없다고 가정하는 것은 현명하지 않다.
@CrossOrigin으로 엔드포인트가 정의되는 위치에서 직접 규칙을 지정하면 규칙이 투명해지는 장점이 있지만, 코드가 장황해지고 많은 코드를 반복해야 할 수 있다는 단점도 있다. 또한 개발자가 새로 구현한 엔드포인트에 어노테이션을 추가하는 것을 망각할 수도 있다.
[ ▷ CorsConfigurer로 CORS 적용 ]
@CrossOrigin 어노테이션은 쉽게 이용할 수 있지만, CORS 구성을 한곳에서 정의하는 것이 더 편한 경우가 많다.
@Configuration
@EnableWebSecurity
public class ProjectConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors(c -> {
CorsConfigurationSource source = request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("*"));
config.setAllowedMethods(List.of("*"));
return config;
};
c.configurationSource(source);
});
http.csrf().disable();
http.authorizeRequests()
.anyRequest().permitAll();
return http.build();
}
}
HttpSecurity 객체에서 호출하는 cors() 메서드는 Customizer<CorsConfigurer> 객체를 매게 변수로 받는다. 이 객체를 위해 HTTP 요청의 CorsConfiguration을 반환하는 CorsConfigurationSource를 설정했다. CorsConfiguration은 허용되는 출처, 메서드, 헤더를 지정하는 객체다. 이 방식을 이용하려면 최소한 허용할 출처와 메서드를 지정해야 하며 출처만 지정하면 애플리케이션이 요청을 허용하지 않는다. 이는 CorsConfiguration 객체가 기본적으로 아무 메서드로 지정하지 않기 때문이다.
위 예제에선 configure() 메서드에서 람다식으로 CorsConfigurationSource의 구현을 제공했지만 실제 애플리케이션에선 이 코드를 다른 클래스로 나누는 것이 좋다, 실제 애플리케이션은 이보다 복잡할 수 있으므로 구성 클래스로 나누지 않으면 코드가 읽기 어려워질 수 있다.
[ ▷ 정리 ]
- CORS는 특정 도메인에서 호스팅되는 웹 애플리케이션이 다른 도메인의 콘텐츠에 접근하려고 할 때 발생하며 기본적으로 브라우저는 이러한 접근을 허용하지 않는다. CORS 구성을 이용하면 리소스의 일부를 브라우저에서 실행되는 웹 애플리케이션의 다른 도메인에서 호출할 수 있다.
- CORS를 구성하는 방법에는 @CrossOrigin 어노테이션으로 엔드포인트별로 구성하는 방법과 HttpSecurity 객체의 cors() 메서드로 중앙화된 구성 클래스에서 구성하는 방법이 있다.
'Spring Boot > Spring Security' 카테고리의 다른 글
OAuth2와 OpenID Connect (4) | 2024.12.18 |
---|---|
CSRF 보호 (1) | 2024.11.01 |
Jason Web Token (0) | 2024.11.01 |