SOP 정책 (동일 출처 정책)
Same-origin policy라고 부르며 말 그대로 동일한 출처의 리소스만 상호작용을 진행하겠다는 의미.
서로 소통하려는 두 URL의 프로토콜, 호스트, 포트가 모두 같아야 동일한 출처로 인정하게 된다.
즉 아래 두 개의 주소는 프로토콜, 포트가 동일하지만 호스트의 이름이 달라서 상호작용이 불가능하다. 이때 데이터를 요청하면 CORS가 발생하게 되는 것.
https://service.example.com:8080
CORS (교차 출처 리소스 공유)
Cross-Origin Resource Sharing이라고 부르며 이름 그대로 서로 다른 출처끼리 리소스 공유를 가능하게 해주는 방법.
프로젝트를 진행하면 실제로 "CORS가 발생했다" 라고 많이 말을 하는데, CORS는 다른 출처의 요청을 허용하지 않았으니 CORS를 허용시켜 달라는 의미. 즉 CORS가 요청을 막는 것이 아니다.
결국 요청을 받는 곳에서 CORS 설정을 통해 다른 외부 출처를 허용시켜줘야 한다.
Preflight
브라우저의 CORS 요청 전에 서버 측에서 그 요청의 메서드와 헤더에 대해 인식하고 있는지를 확인하는 CORS 요청이다.
OPTIONS 메서드를 사용한 요청으로
- Access-Control-Request-Method
- Origin
- Access-Control-Request-Headers(선택)
이렇게 2~3가지의 HTTP request headers를 사용하는 OPTIONS 요청이다.
일반적으로 브라우저가 Preflight는 자동으로 요청을 진행한다. 그러나 Simple Request의 경우 Preflight가 생략된다.
Simple Request
이름 그대로 단순 요청이다. 모든 요청이 Simple Request는 아니고 특정 조건을 모두 만족하는 요청만 Simple Request로 분류돼서 Preflight를 동작하지 않는다.
- 다음 중 하나의 메서드
- GET, HEAD, POST
- 유저 에이전트가 자동으로 설정한 헤더
- Accept
- Accept-Language
- Content-Language
- Content-Type (컨텐트 타입은 아래 조건만 해당됩니다.)
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 요청에 사용된 XMLHttpRequestUpload 객체에는 이벤트 리스너가 등록되어 있지 않습니다. 이들은 XMLHttpRequest.upload 프로퍼티를 사용하여 접근합니다.
- 요청에 ReadableStream 객체가 사용되지 않습니다.
즉 특정 HTTP 메서드, 유저 에이전트가 자동으로 설정한 헤더 또는 CORS-safelisted 헤더, 조건에 맞는 Content-Type, XMLHttpRequestUpload 객체에 이벤트 리스너가 없어야 함, 요청에 ReadableStream 객체가 사용되지 않을 경우.
위 조건을 전부 만족해야 SimpleRequest라고 판단하는 것이다.
해결 방법
외부 특정 출처를 허용시키면 된다.
문제가 발생하는 이유는 송신 URL, 수신 URL이 서로 다르기에 발생한다.
시나리오
클라이언트(유저)가 토큰을 조회하는 API를 요청하려고 함.
클라이언트 : localhost:5173
서버 : localhost:8080
위 시나리오를 보면 유저는 토큰 조회 버튼을 누르려고 하는 상태.
그러면 토큰 조회 API가 서버로 전송되는데 문제는 2개의 포트번호가 서로 다르기 때문에 SOP를 만족하지 못한다. 즉 CORS 에러가 발생하는 것.
그러면 서버(8080)에서는 결국 클라이언트(5173)의 URL을 허용시켜서 "localhost:5173으로 들어오는 요청은 전부 받아도 돼!"라는 것을 명시해주어야 한다.
테스트 환경에서는 스프링 시큐리티를 이용했기에 CorsConfig를 따로 만들었다.
CorsConfig
@Configuration
public class CorsConfig {
@Bean
@Primary
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(Arrays.asList("http://localhost:5173"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
가장 주목할 부분은 "http://localhost:5173"을 허용시켜주었다는 것.
전체 코드를 하나씩 살펴보자
CorsConfiguration를 하나 생성해서 사용한다. 얘는 CORS를 설정하기 위한 컨테이너. 즉 해당 객체에 이것 저것 CORS 설정을 진행하는 것.
CorsConfiguration config = new CorsConfiguration();
setAllowCredentials를 true로 변경해서 사용자 자격 증명을 지원하겠다고 변경해 준다.
config.setAllowCredentials(true);
public void setAllowCredentials(@Nullable Boolean allowCredentials) {
this.allowCredentials = allowCredentials;
}
여기서 사용자 자격 증명은 쿠키, CSRF 토큰을 의미한다.
만약에 프론트에서 withCredentials: true로 설정해서 자격 증명을 보내는데 서버에서 false로 해놓으면 어떻게 될까?
위 내용을 해석해 보면 "요청의 credentials를 include로 해놨으면 Access-Control-Allow-Credentils는 true여야 한다"
즉 프론트에서 자격 증명 포함해서 보내면 서버에서는 이거 true로 해서 받아라. 이런 의미이다.
setAllowedOriginPatterns는 List로 전달시키는 URL은 허용하겠다는 의미를 가진다.
config.setAllowedOriginPatterns(Arrays.asList("http://localhost:5173"));
public CorsConfiguration setAllowedOriginPatterns(@Nullable List<String> allowedOriginPatterns) {
if (allowedOriginPatterns == null) {
this.allowedOriginPatterns = null;
}
else {
this.allowedOriginPatterns = new ArrayList<>(allowedOriginPatterns.size());
for (String patternValue : allowedOriginPatterns) {
addAllowedOriginPattern(patternValue);
}
}
return this;
}
즉 아무것도 안 들어오면 null로 설정하고, List가 들어오면 size 만큼 새로운 ArrayList를 만들어서 URL을 추가시킨다.
setAllowedMethods는 리스트로 전달하는 메서드만 허용하겠다는 의미이다.
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
public void setAllowedMethods(@Nullable List<String> allowedMethods) {
this.allowedMethods = (allowedMethods != null ? new ArrayList<>(allowedMethods) : null);
if (!CollectionUtils.isEmpty(allowedMethods)) {
this.resolvedMethods = new ArrayList<>(allowedMethods.size());
for (String method : allowedMethods) {
if (ALL.equals(method)) {
this.resolvedMethods = null;
break;
}
this.resolvedMethods.add(HttpMethod.valueOf(method));
}
}
else {
this.resolvedMethods = DEFAULT_METHODS;
}
}
해당 코드도 동일하게 List가 없으면 null로 설정하고 List가 있으면 ArrayList를 새로 만들어 추가시킨다.
- allowedMethods : 사용자가 지정한 HTTP 메서드 목록을 저장한다.
- resolvedMethods : 실제로 사용할 HTTP 메서드 목록을 저장한다.
만약에 ALL.equals(method)가 true가 되면 resolvedMethods는 null로 설정하고 종료한다.
그니깐 resolvedMethods가 null이면 전체 메서드를 허용한다는 의미이다.(이거 조금 헷갈리지 않나? 싶었는데)
resolvedMethod가 null이 아닌 경우에는 지정해 놓은 메서드만 허용하는 것.
setAllowedHeaders는 CORS 설정에서 허용할 HTTP 헤더를 지정하는 데 사용한다. 클라이언트가 서버에 요청할 때 사용할 수 있는 헤더를 지정하는 것.
config.setAllowedHeaders(List.of("*"));
public void setAllowedHeaders(@Nullable List<String> allowedHeaders) {
this.allowedHeaders = (allowedHeaders != null ? new ArrayList<>(allowedHeaders) : null);
}
*를 사용하면 전체 헤더를 허용한다는 의미이다.
아니면 아래처럼 특정 헤더만 선택적으로 허용할 수도 있다.
Arrays.asList("Content-Type", "Authorization", "X-Requested-With")
여기서도 한 가지 특이한 주석 설명을 발견했다.
즉 allowCredentials를 true로 설정한 경우 allowedHeaders를 "*"로 설정하는 것을 허용하지 않는다는 것.
내 코드는 여태 저렇게 되어 있었는데?...
내가 이해했던 내용은 아래에 자세하게 설명하려고 한다.
하나의 테스트를 더 진행해 보자.
테스트 시나리오
클라이언트에서 POST 요청을 보내는데 Content-Type : application/json으로 요청을 보낸다고 가정.
서버의 설정은 아래와 동일하게 진행.
config.setAllowCredentials(true);
config.setAllowedHeaders(List.of("*"));
config.setAllowedOriginPatterns(Arrays.asList("http://localhost:5173"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
POST 요청을 보내지만 Content-Type이 application/json이라서 Preflight가 먼저 진행된다.
위 메서드를 보면 OPTIONS가 나가서 정상적으로 먼저 Preflight가 동작하는 것을 확인할 수 있다.
여기서 주목해야 하는 점이 분명 서버에서는 응답 헤더를 *로 설정했는데 Response Header의 Access-Control-Allow-Headers를 보면 content-type으로 설정되어 있다.
힌트는 setAllowedHeaders 메서드의 주석에 있다.
다시 주석을 살펴보면 allowCredentials(true), headers("*") 이 조합을 허용하지는 않는다. 그런데 사용한다면 CORS 실행 전 요청에 지정된 헤더를 복사하여 처리한다고 설명되어 있다.
즉 내가 이해하기로는 *로 해놓으면 Request의 헤더를 보고 이 헤더를 복사해서 Response Headers에 담는다는 것.
그렇게 Preflight 응답을 클라이언트에게 보내주고, 클라이언트는 해당 Response를 보고 보내도 된다 싶으면 실제 Request를 보내는 것이다.
참고 자료
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
교차 출처 리소스 공유 (CORS) - HTTP | MDN
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라
developer.mozilla.org
https://developer.mozilla.org/ko/docs/Glossary/Preflight_request
사전 요청 - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN
교차 출처 리소스 공유 사전 요청은 본격적인 교차 출처 HTTP 요청 전에 서버 측에서 그 요청의 메서드와 헤더에 대해 인식하고 있는지를 확인하는 CORS 요청입니다.
developer.mozilla.org
'CS지식' 카테고리의 다른 글
spring-oauth-client 라이브러리의 동작 흐름 정리 (0) | 2024.06.27 |
---|---|
Transaction의 개념 (Feat, ACID) (0) | 2024.05.26 |
4-way handshake(feat, TCP 연결 해제) (1) | 2024.05.21 |
TCP와 UDP의 차이점에 대해 설명해주세요 (0) | 2024.05.20 |