CORS란?
Cross-Origin Resource Sharing = 교차 출처 리소스 공유 정책
자세히는 추후 말하겠다.
출처(Origin)란?
URL의 Protocol + Host + Port를 의미하는데, 간단히 말해선 웹 페이지나 리소스의 출처를 나타내는 개념이다.
그러면, 아무래도 보안 때문에 같은 출처끼리의 리소스를 공유하는 정책이랑 다른 출처끼리의 리소스를 공유하는 정책이 각각 존재할 것 같다는 생각이 든다.
1. SOP (Same-Origin Policy)
동일 출처 정책은 웹 브라우저가 동일한 출처에서만 리소스를 공유하도록 제한하는 보안 정책이다.
기본적으로 같은 출처끼리는 리소스를 공유할 수 있도록 허용된다. 만일, 다른 출처끼리도 제약 없이 공유가 허용된다면, 해커가 개인정보를 가로챌 수도 있는 등 문제가 발생하기 쉽기에 SOP 정책을 두어 다른 출처의 스크립트가 실행되지 않도록 브라우저에서 사전에 방지한다.
두 웹페이지나 리소스의 출처 비교와 차단은 브라우저가 한다 ???
서버에 요청을 했는데, 에러가 뜨면 서버에 문제가 발생했다고 여기기 쉽상이다. 하지만, 사실 브라우저의 문제다.
사실 서버는 리소스 요청에 의한 응답 데이터는 잘 전송했다. 하지만, 브라우저가 응답(헤더)을 분석해서 동일 출처가 아니면 cors 에러를 띄우며 받을 수 없도록 차단하는 것이다.
* cors 에러를 방지하기 위해선 서버가 헤더 정보를 추가로 주어야 한다.
2. CORS (Cross-Origin Resource Sharing)
교차 출처 리소스 공유 정책은 웹 브라우저가 다른 출처의 리소스 공유에 대한 허용/비허용 정책이다.
기본적으로 다른 출처끼리는 리소스를 공유하지 못한다. 이는 웹 브라우저의 동일 출처 정책(Same-Origin Policy) 때문인데, 보안상의 이유로 출처가 다른 리소스 간의 상호작용을 제한한다. 다만, 리소스를 공유해야 하는 상황은 굉장히 흔하게 발생한다. 예를 들어, 백엔드에서 만든 API를 프론트엔드 애플리케이션이 호출하여 데이터를 사용하는 경우나, 타 기관의 API를 재가공하여 사용하는 경우 등이 있다. 이러한 상황에서는 다른 출처 간의 상호작용이 필수적이다.
이러한 필요를 충족하기 위해, CORS(Cross-Origin Resource Sharing) 정책은 몇 가지 예외 조항을 두어 다른 출처의 리소스 요청이라도 허용할 수 있게 한다. 서버는 특정 출처에서의 요청을 허용하는 CORS 헤더를 응답에 포함시켜, 해당 출처의 리소스에 한해 공유를 허용한다. 이를 통해, 필요한 경우 안전하게 리소스를 공유할 수 있다.
즉, CORS 덕분에 SOP 정책을 위반해도 CORS 정책에 따르면 다른 출처의 리소스와 상호작용할 수 있는 것이다.
동작 과정은?
1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 전달한다.
2. 서버는 응답 헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달한다.
Access-Control-Allow-Origin: 이 리소스를 접근하는 것이 허용된 출처 url
3. 클라이언트에서 Origin과 서버가 보내준 Access-Control-Allow-Origin을 비교한다.
비교해서 차단할지 말지 결정하는데,
같다면 문제 없이 리소스를 가져오고, 다르다면 응답을 사용하지 않고 버리며 cors 에러를 반환한다.
=> 결국 백엔드에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재헤서 클래이언트에게 응답해야 하는 것이다.
CORS 에러 해결책은?
여러 방법이 있는데, 그들 중 예비 요청(Preflight Request) 방식을 사용했다.
이는 브라우저가 특정 종류의 HTTP 요청을 실행하기 전에 서버의 허용 여부를 확인하기 위해 보내는 사전 요청이다. 예비 요청은 주로 보안상의 이유로 도입되었으며, HTTP OPTIONS 메서드를 사용하여 이루어진다. 허용될지 여부를 확인한 뒤에는 허용된 요청에 대한 메타데이터(예: 허용된 HTTP 메서드, 허용된 헤더 등)를 클라이언트에게 전달하게 된다.
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 1. 허용할 출처(Origin) 설정
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://reborn.persi0815.site"));
// 2. 허용할 HTTP 메서드 설정
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
// 3. 자격 증명(쿠키, 인증 정보) 허용 여부 설정
configuration.setAllowCredentials(true);
// 4. 프리플라이트 요청의 캐시 시간 설정 (초 단위)
// 프리플라이트 요청은 특정 조건을 만족하는 CORS 요청 전에 브라우저가 서버에 보내는 사전 요청
configuration.setMaxAge(3600L);
// 5. URL 패턴에 대해 CORS 설정 등록
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 모든 경로
return source;
}
Preflight Request 단계별 로직
1. 클라이언트가 요청을 준비한다.
요청에는 비표준 HTTP 메서드(예: PUT, DELETE), 사용자 정의 헤더(예: Authorization), 또는 Content-Type이 application/json과 같은 특정 값으로 설정된 경우가 포함될 수 있다.
2. 브라우저가 예비 요청(OPTIONS 메서드)을 전송한다.
OPTIONS /api/resource HTTP/1.1
Host: example.com
Origin: http://localhost:3000 // 요청이 발생한 출처
Access-Control-Request-Method: PUT // 실제 요청에서 사용하려는 HTTP 메서드를 지정
Access-Control-Request-Headers: Content-Type, Authorization // 실제 요청에서 사용하려는 사용자 정의 헤더를 지정
3. 서버가 예비 요청을 수신한 뒤 응답한다.
서버는 예비 요청을 수신하고, 요청된 출처, 메서드 및 헤더가 허용되는지 검사한 뒤 응답한다.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000 // 허용된 출처를 지정
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 허용된 HTTP 메서드를 지정
Access-Control-Allow-Headers: Content-Type, Authorization // 허용된 사용자 정의 헤더를 지정
Access-Control-Allow-Credentials: true // 격 증명(쿠키, 인증 정보)의 사용을 허용할지 여부를 지정
Access-Control-Max-Age: 3600 // 브라우저가 예비 요청의 결과를 캐시할 수 있는 시간을 지정
4. 브라우저가 예비 요청 응답을 처리한다.
서버의 응답을 수신하고 서버가 요청을 허용하는지의 여부를 검사한다.
5. 응답이 허용되면 브라우저는 실제 요청을 서버에 전송한다.
실제 요청에는 원래의 메서드와 헤더가 포함된다.
PUT /api/resource HTTP/1.1
Host: example.com
Origin: http://localhost:3000
Content-Type: application/json
Authorization: Bearer token
6. 서버가 실제 요청된 작업을 수행한 뒤 요청에 대한 응답을 클라이언트에게 응답한다.
HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "Resource updated successfully"
}
7. 브라우저가 서버의 응답을 수신하고, 클라이언트 애플리케이션에게 응답을 전달한다.
출처 (잘 나와있다!!)
'Backend > Spring' 카테고리의 다른 글
[Spring/Java] FCM을 통해 Push 알림 보내기 (0) | 2024.07.08 |
---|---|
[Spring/Java] 동시성 제어 통한 게시물 좋아요 및 댓글 수 관리 (비관적 락, 반정규화, 세마포어) (0) | 2024.07.07 |
[Sping/Java] JPQL vs QueryDSL (0) | 2024.07.07 |
[Spring/Java] JPQL & Slice 객체(paging)이용하여 특정 조건의 게시물을 특정 방식으로 가져오기 (0) | 2024.07.07 |
[Spring/Java] Paging이란? Page 객체 vs Slice 객체 (+ 코드) (0) | 2024.07.07 |