[WEB] CORS(Cross-Origin Resource Sharing)

2021. 12. 5. 22:58[ Basic ]/# 네트워크

SOP(Same Origin Policy)

다른 출처의 리소스를 사용하는 것을 제한하는 보안 방식이다. SOP를 사용하면 보안에 도움이 된다. 만약 사용자가 서버에서 제공되는 URL 외 다른 URL로 접속해 서버에 request를 보낼 경우 이에 대한 출처를 cross-origin으로 판단해 SOP에 위반되며 해당 request는 허용되지 않는다.

 

이때 다른 출처(cross-origin)의 리소스가 필요할 경우, 해당 리소스가 있는 출처에 접근하고자 하는 설정에 필요한 것이 CORS이다. 

 

cf. 출처(Origin) 란?

https://beomy.github.io/tech/browser/cors/

URL의 protocol, host(도메인), port를 통해 같은 출처인지 판단한다. 셋 중에 하나라도 다르면 다른 출처이고 모두가 같을 경우에만 같은 출처이다.

 

ex) http://localhost 와 동일 출처 예시 : http://localhost:80, http://localhost/api/v1 등

 


CORS

- Cross-Origin Resource Sharing으로 다른 출처의 자원을 공유하는 방식이다. 추가 HTTP 헤더를 사용해 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 '브라우저'에 알려주는 방식이다. (브라우저가 있어야 한다는 점에 유의!)

- CORS는 HTTP의 규격임

- 브라우저가 Cross-Origin을 판단하기 때문에 브라우저 없이 요청이 가능 API 테스트에서는 통과됨

 

https://evan-moon.github.io/2020/05/21/about-cors/

- 위처럼 웹사이트 주소가 웹서버로 요청보내는 과정에서 특정 웹 사이트만 요청을 보낼 수 있도록 허용함 

- 그리고 허용여부를 브라우저 프로그램이 받는다.

 

 

CORS 접근제어 시나리오

- 단순 요청(SImple Request)

- 프리플라이트 요청(Preflight Request)

- 인증정보 포함 요청(Credentialed Reqeust)

 

 

1. 프리플라이트 요청(Preflight Request)

다른 도메인의 리소스에 요청이 가능한 지 서버에 물어보는 작업이다.(사전요청) 만약 요청이 가능하다면 실제 요청을 보낸다. Preflight 요청은 HTTP의 OPTIONS 메소드 방식으로 요청을 보내며 요청이 불가하다면 403 에러를 보낸다.

 

https://beomy.github.io/tech/browser/cors/

 

 

프리플라이트 요청 HTTP 구조

1) Request

OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org

- HTTP 메소드로 OPTIONS을 활용

- Origin: 요청 출처

- Access-Control-Request-Method : 실제요청에서 보낼 메소드

- Access-Control-Request-Headers : 실제 요청의 추가 헤더에 무엇을 보낼 수 있는지

 

2) Response

HTTP/1.1 204 No Content
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

- Access-Control-Allow-Origin : 해당 Origin이 허가되어 있음을 의미

- Access-Control-Allow-Methods : 서버 측에서 허가된 메소드

- Access-Control-Allow-Headers : 서버 측에서 허가된 헤더들

- Access-Control-Max-Age : 프리플라이트 응답의 캐시 기간(86400초를 캐싱함)

 

cf. 프리플라이트 요청을 하면 실질적으로 2번의 요청이 보내진다. 첫 번째가 프리플라이트 요청이고 두 번째가 실제 요청이다. 이렇게 되면 매번 요청마다 2번의 요청을 보내야 한다. 따라서 프리플라이트 요청을 캐싱해두고 이후에 똑같은 요청을 보낼 때 캐싱된 것을 확인하고 프리플라이트 요청을 보내지 않고 바로 실제 요청을 보낸다. 이때 캐싱 기간을 명시하는 것이 Access-Control-Max-Age이다.

 

cf. 프리플라이트 응답 메시지가 가져야 하는 특징

- 응답코드가 200대여야 한다.

- 응답바디는 비어있는 것이 좋다.

 

 

 

2. 단순 요청(SImple Request)

프리플라이트와 다르게 바로 실제 요청을 보내면서 그 즉시 해당 URL이 cross-origin인지 확인하는 방식이다. SImple Request를 보내려면 2가지 조건을 만족해야 한다.

 

- GET, POST, HEAD 메소드 방식이어야 함.

- Content-Type이 다음 세 가지 중 하나여야 함.

     1) application/x-www=form-urlencoded

     2) multipart/form-data

     3) text/plain

- 헤더는 Accept, Accept-Language, Content-Language, Content-Type 만 허용.

 

https://beomy.github.io/tech/browser/cors/

request header

- Origin : 허용이 가능할지 묻는 출처 명시.

 

response header

- Access-Control-Allow-Origin : 서버는 허용되는 출처를 알림.

 

 

cf. Simple Request는 한 번만 요청을 보내면 알 수 있지만 Preflight Request는 두 번의 요청이 있어야 실질 요청을 할 수 있다. 그렇다면 Preflight Request가 존재하는 이유는?

 

- CORS를 모르는 서버(아무런 설정이 없는 서버)를 위해 존재한다. 즉, CORS를 모르는 서버는 Allow-Origin과 같은 헤더를 줄 수 없기 때문에 브라우저단에서 해당 값이 없는 것을 CORS 에러로 판단한다. CORS를 모르는 서버의 경우, cross-origin에 대한 요청이 와도 처리를 모두 하지만 브라우저 단에서 CORS 에러를 발생시킨다. 하지만 cross-origin에 대한 처리를 하면 안 되기 때문에 이를 방지하기 위해 Preflight Request가 존재하는 것이다. Preflight Request의 경우, 사전 요청을 하나 두기 때문에 실질 요청이 오기 전에 해당 요청이 cross-origin인지 먼저 판단한다. 

 

https://velog.io/@shin6403/CORS%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

위와 같이 Preflight Request 시나리오 상에서, CORS에 대한 설정이 있는 서버라면 예비 요청의 응답으로 Allow-Origin과 같은 헤더를 응답하는데, 그렇지 않은 서버의 경우, 해당 헤더가 존재하지 않게 된다. 따라서 이러한 상황을 브라우저가 CORS 에러로 판단하는 것이며 CORS 설정이 없는 서버의 경우에 대응하기 위해 Preflight Request가 존재하는 것이다. Preflight Request는 서버를 안전하게 지켜준다.

 

 

3. 인증정보 포함 요청(Credentialed Reqeust)

인증 관련 헤더를 포함할 경우 사용하는 요청이다. 쿠키나 JWT 토큰을 담아서 보내고 싶을 때 요청을 credentials : include로 보내고 서버에서 응답 헤더로 Access-Control-Allow-Crendentials: true로 설정해서 보내야 한다. Access-Control-Allow-Crendentials 헤더를 true로 줄 경우, Access-Control-Allow-Origin을 *로 줄 수 없다. 이 경우엔 정확한 출처를 포함해야 한다.

 

 

CORS 해결하기

- 직접 헤더에 설정하기(X)

- 스프링 부트 이용하기

 

샘플코드)

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("https://*.vercel.app")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods(
                        HttpMethod.GET.name(),
                        HttpMethod.HEAD.name(),
                        HttpMethod.POST.name(),
                        HttpMethod.PATCH.name(),
                        HttpMethod.PUT.name(),
                        HttpMethod.DELETE.name()
                );
    }
}

- https://*.vercel.app 패턴을 갖는 origin과 http://localhost:3000 origin으로 부터의 요청을 모두 허용함

 

 

Reference

- https://youtu.be/-2TgkKYmJt4

- https://beomy.github.io/tech/browser/cors/