2024.07.04 가장 최신 버전의 Spring Security 6.3.1 기준으로 SOP, CORS 를 소개합니다.
코드 작성 방법부터 보고 싶다면, 가장 아래에 5. cors() & CorsFilter 를 참고하시기 바랍니다.
1. SOP
SOP(동일 출처 정책, Same Origin Policy) 이란
- 웹 브라우저 보안을 위해 Same-Origin 의 서버에서만 리소스를 주고 받도록 상호작용을 제한하는 보안 정책
- Same-Origin 은 Protocol, Host, Port 가 모두 동일한 출처를 의미
SOP 정책은 웹 페이지의 자바스크립트가 다른 출처(Origin)로부터 리소스 요청하는 행위를 방지함으로써 정보 누출, CSRF(Cross-Site Request Forgrey) 같은 공격을 방지한다.
- 도메인(Hostname): myshop.com
- 출처(Origin): https://www.myshop.com
SOP 장점
- 웹 페이지의 자바스크립트가 다른 출처로부터 리소스 요청을 방지함으로써 정보 노출 방지
- CSRF(Cross-Site Request Forgrey) 공격 방지
SOP 단점
- SOP는 다른 출처로부터 API 호출을 제한한다. 따라서 웹 애플리케이션에서 외부 서비스의 데이터를 활용하기가 어렵다.
- 다른 출처로부터 리소스 요청을 가능하도록 하기 위해서는 CORS 정책을 구현해야 한다.
SOP 예제
동일 출처(Same-Origin)의 정의:
- 프로토콜: http:// 또는 https://와 같은 프로토콜이 동일해야 한다.
- 호스트명: www.myshop.com과 같은 호스트명이 동일해야 한다.
- 포트: 포트 번호(예: 80, 443 등)가 동일해야 한다.
예제: 동일 출처
- 출처(Origin): https://www.myshop.com
다음은 모두 동일 출처로 간주된다:
- https://www.myshop.com/page1
- https://www.myshop.com/page2
- https://www.myshop.com:443 (포트 443은 https의 기본 포트)
예제: 다른 출처
- 출처(Origin): https://www.myshop.com
다음은 동일 출처가 아닙니다:
- http://www.myshop.com (프로토콜이 다름)
- https://subdomain.myshop.com (호스트명이 다름)
- https://www.myshop.com:8080 (포트가 다름)
SOP 정책에 따르면, 자바스크립트는 다른 출처에서 리소스를 요청하는 것이 원칙적으로 금지이다. 예를 들어, https://www.myshop.com 있는 웹 페이지의 자바스크립트는 https:// api.myshop.com 또는 http://www.myshop.com과 같은 다른 출처의 리소스를 요청할 수 없다.
이러한 제한을 해결하기 위해 CORS(Cross-Origin Resource Sharing)라는 메커니즘이 도입되었다. CORS는 서버 측에서 특정 출처의 요청을 허용하도록 설정하여 SOP의 제한을 완화할 수 있게 설정한다. Spring Boot에서 CORS를 구성하여, 다양한 출처에서 서버 리소스에 접근할 수 있도록 설정할 수 있다. CORS 설정을 통해 SOP의 제한을 완화하여 여러 출처에서 서버와 상호작용할 수 있게 한다. CORS 가 필요한 이유에 대해 SOP 으로 학습했다. 이제 CORS 가 무엇인지 구체적으로 학습하자.
2. CORS
CORS(교차 출처 리소스 공유, Cross-Origin Resource Sharing) 이란
- '출처가 다른 서버 간의 리소스 공유'를 허용한다는 것을 의미
SOP 정책이 서로 다른 출처인 경우에 리소스 요청 혹은 응답을 차단하는 정책이라면, CORS 정책은 반대로 서로 다른 출처이더라도 리소스 요청 혹은 응답을 허용할 수 있도록 제공하는 정책이다. https://www.myshop.com 을 다시 떠올리며 아래의 어떠한 URL이 SOP에 부합하는지 한 번 확인한다.
3. CORS 에러 대응하기
- Access-Control-Allow-Origin 응답 헤더 설정하자.
서버에서 Access-Control-Allow-Origin 헤더를 설정해서 요청을 수락할 출처를 명시적으로 지정할 수 있다. 이 헤더를 설정하면 출처가 다르더라도 https://myshop.com 의 리소스 요청을 허용하게 된다.
'Access-Control-Allow-Origin': <origin> | *
* 를 설정하면 출처에 상관없이 리소스에 접근할 수 있는 와일드카드이기 때문에 보안에 취약하다. 따라서 'Access-Control-Allow-Origin': https://myshop.com 처럼 직접 허용할 출처를 설정하는 방법을 권장한다.
4. CORS 종류
CORS 종류는 Simple Request, Preflight Request, Credential Request, Non-Credential Request 네 가지이다.
Simple Request
단순 요청(본 요청, Simple Request) 은 예비 요청(Preflight Request) 과정 없이 자동으로 CORS가 작동하여 서버에 단순 요청을 한 후, 서버가 응답 헤더에 Access-Control-Allow-Origin 과 같은 값을 전송하면 브라우저가 이를 비교하여 CORS 정책 위반 여부를 검사하는 방식이다.
예비 요청(Preflight Request)과 단순 요청(Simple Request)의 로직은 전반적으로 유사하지만, 예비 요청의 존재 여부만 다르다. 예비 요청을 생략하는 단순 요청을 할 수 있는 조건은 다음과 같다:
- 요청 메소드
- GET
- HEAD
- POST
- 허용된 헤더
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR, Downlink, Save-Data, Viewport-Width, Width (브라우저에 따라 다를 수 있음)
- Content-Type
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
이 중 2번과 3번 조건이 까다롭다. 예를 들어, 사용자 인증에 사용되는 Authorization 헤더는 사용할 수 없으며, 많은 HTTP API에서 사용되는 text/xml이나 application/json 콘텐츠 타입도 허용되지 않는다. 따라서 Simple Request 요청 시나리오를 현실적으로 만족시키기 어려운 경우가 많다.
Simple Request 정리
- 요청 메소드: GET, HEAD, POST 중 하나.
- 허용된 헤더: Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width (브라우저마다 차이가 있을 수 있음).
- Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나.
이 조건들을 모두 만족해야만 예비 요청을 생략할 수 있다.
Preflight Request
예비 요청(Preflight Request) 은 브라우저가 특정 CORS 요청을 보내기 전에 먼저 서버에 OPTIONS 메소드를 사용하여 보내는 요청이다. 예비 요청은 본 요청(=단순 요청, Main Request)을 보내기 전에 해당 요청이 서버에서 허용되는지 확인하는 과정이다.
일반적으로 웹 개발 시 가장 자주 마주치는 요청 시나리오. 특정 조건을 만족하지 않는 CORS 요청의 경우, 브라우저는 요청을 예비 요청(Preflight Request)과 본 요청으로 나누어 서버에 전송한다. Preflight Request 는 브라우저가 본 요청을 보내기 전에 보내는 예비 요청이다. 예비 요청은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 과정으로, HTTP 메소드 중 OPTIONS 메소드가 사용된다.
예비 요청의 목적
- 안전성 확인: 브라우저가 본 요청을 보내기 전, 서버가 해당 요청을 허용하는지 확인
- 헤더 검사: 서버가 CORS 정책을 통해 어떤 헤더를 허용하는지 확인
- 메소드 검사: 서버가 CORS 정책을 통해 어떤 HTTP 메소드를 허용하는지 확인
예비 요청이 필요한 조건
Preflight Request는 다음과 같은 경우에 발생한다.
- HTTP 메소드가 Simple Request가 아닌 경우: PUT, DELETE, PATCH 등.
- 사용하는 헤더가 Simple Request의 허용된 헤더 목록에 없는 경우: Authorization, X-Custom-Header 등.
- Content-Type이 Simple Request의 허용된 값이 아닌 경우: application/json, text/xml 등.
Preflight Request 정리
Preflight Request 는 브라우저가 본 요청을 보내기 전에 OPTIONS 메소드를 사용하여 서버에 보내는 예비 요청이다. 예비 요청을 통해 서버가 본 요청을 허용하는지 확인하고, 브라우저가 본 요청을 안전하게 보낼 수 있다.
웹 페이지가 보낸 요청에서 출처에 대한 정보 뿐만 아니라 예비 요청 이후에 전송할 본 요청에 대한 다른 정보들도 함께 포함되어 있는 것을 확인할 수 있다. 해당 예비 요청에서 브라우저는 Access-Control-Request-Headers 를 사용하여 자신이 본 요청에서 Content-Type 헤더를 사용할 것을 알려주거나, Access-Control-Request-Method 를 사용하여 GET 메소드를 사용할 것을 서버에게 미리 알려주고 있다.
서버가 보내준 응답 헤더에 포함된 Access-Control-Allow-Origin: https://security.io 의 의미는 해당 URL 이 아니라 다른 출처로 요청할 경우에는 CORS 정책 위반으로 판단하고 오류 메시지를 보내고 응답을 거부한다.
CORS 에러 대응하기 - 심화
Access-Control-Allow-Origin
- 헤더에 작성된 출처만 브라우저가 리소스를 접근할 수 있도록 허용한다.
- *, https://security.io
Access-Control-Allow-Methods
- 예비 요청에 대한 응답. 실제 요청에 사용할 수 있는 메서드를 나타낸다.
- 기본값: GET, POST, HEAD, OPTIONS, *
Access-Control-Allow-Headers
- 예비 요청에 대한 응답. 실제 요청에 사용할 수 있는 헤더 필드 이름을 나타낸다.
- 기본값: Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Reuqest-Headers, Custom Header, *
Access-Control-Allow-Credentials
- 실제 요청에 쿠키 혹은 인증 등의 사용자 자격 증명이 포함될 수 있음을 나타낸다.
- 클라이언트의 credentials:include 옵션인 경우 true 는 필수이다.
Access-Control-Max-Age
- 예비 요청 결과를 캐싱할 수 있는 시간. 해당 시간 동안은 예비 요청을 다시 하지 않는다.
5. cors() * CorsFilter
CORS 의 사전 요청(Preflight request)에는 쿠키(JSESSIONID)가 포함되어 있지 않으므로, Spring Security 가 수행되기 전에 처리되어야 한다. 사전 요청에 쿠키가 없으면 Spring Security 는 해당 요청을 인증되지 않은 것으로 판단하여 거부할 수 있다. 이를 방지하기 위해 CorsFilter 를 사용하여 CORS 처리를 Spring Security 이전에 수행하도록 설정할 수 있다. CorsFilter 에 CorsConfigurationSource 를 제공함으로써 Spring Security 와 통합할 수 있다.
SpringBoot: CORS 설정
RequiredArgsConstructor
@Configuration
public class CustomSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(httpSecurityCorsConfigurer ->
httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource()));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOriginPatterns(List.of("*"));
corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"));
corsConfiguration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type"));
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
CorsConfiguration 클래스는 CORS 설정 메서드를 포함하고 있으며 각 메서드 의미는 다음과 같다.
- setAllowedOrigins(List<String> allowedOrigins)
- 허용된 출처(Origin)를 설정(클라이언트가 요청을 보낼 수 있는 출처의 목록)
- 여러 개의 출처를 허용할 수 있으며, 각 출처는 문자열로 표시한다.
- setAllowedMethods(List<String> allowedMethods)
- 허용된 HTTP 메서드를 설정(클라이언트가 사용할 수 있는 HTTP 메서드의 목록)
- 주로 GET, POST, PUT, DELETE 등의 메서드가 사용된다.
- setAllowedHeaders(List<String> allowedHeaders)
- 허용된 요청 헤더를 설정(클라이언트가 요청 헤더에 포함할 수 있는 목록)
- 일반적으로는 인증 헤더인 Authorization과 컨텐츠 유형 헤더인 Content-Type이 포함된다.
- setExposedHeaders(List<String> exposedHeaders)
- 노출할 응답 헤더를 설정(클라이언트에게 응답으로 보낼 헤더의 목록)
- 클라이언트에서 접근해야 하는 특정 헤더가 있을 경우 여기에 추가한다.
- setAllowCredentials(boolean allowCredentials)
- 자격 증명 허용 여부를 설정
- 클라이언트가 요청에 자격 증명(예: 쿠키, 인증 헤더)을 포함할 수 있는지 여부를 결정한다.
- setMaxAge(long maxAge)
- 사전 검증(PreFlight) 요청의 유효 기간을 설정
- 클라이언트가 사전 검증 요청의 결과를 캐시할 수 있는 시간(초 단위)을 지정한다.
- addAllowedOrigin(String allowedOrigin)
- 특정 출처를 추가(이 메서드를 사용하여 허용된 출처를 추가)
- addAllowedMethod(String allowedMethod)
- 특정 HTTP 메서드를 추가(이 메서드를 사용하여 허용된 HTTP 메서드를 추가)
- addAllowedHeader(String allowedHeader)
- 특정 요청 헤더를 추가(이 메서드를 사용하여 허용된 요청 헤더를 추가)
CorsConfigurationSource 인터페이스는 CORS 정책을 정의하는 인터페이스이다. CORS 설정과 인증 및 인가 두 방식으로 SOP를 준수할 수 있다.
'스프링 > 시큐리티' 카테고리의 다른 글
JWT(JSON Web Token): JWT 로그아웃 구현하기(Feat. Stateless) (0) | 2024.07.13 |
---|---|
JWT(JSON Web Token): (이론) JWT 그리고 스프링 부트 적용하기 (0) | 2024.06.03 |