본문 바로가기
개인공부

cors란?

by 리승우 2022. 10. 24.

CORS?

Cross Origin Resource Sharing

CORS는 한 도메인 또는 Origin의 웹 페이지가 다른 도메인 (도메인 간 요청)을 가진 리소스에 액세스 할 수 있게하는 보안 메커니즘이다.

CORS는 서버와 클라이언트가 정해진 헤더를 통해 서로 요청이나 응답에 반응할지 결정하는 방식으로 CORS라는 이름으로 표준화 되었다. CORS는 최신 브라우저에서 구현된 동일 출처 정책(same-origin policy) 때문에 등장했다.

동일 출처 정책?

동일 출처 정책은 동일한 출처의 리소스에만 접근하도록 제한하는 것이다. 여기서 출처는 프로토콜, 호스트명, 포트가 같다는 것을 의미한다.

https://naver.com:80을 예시로 들면, https는 프로토콜, naver.com은 호스트명 80은 포트다.

왜 동일한 출처에서만 접근하도록 허용하는 것일까? 모든 출처를 허용하면 어떻게 될까?

https://bank.com 이라는 도메인 사이트가 있다 이 사이트의 api 주소는 https://bank.com/api이다. 사용자가 은행 사이트에서 로그인을 한 후 인증 토큰을 받았다. 그런데 사용자가 로그인한 상태에서 https://evil.com사이트에 접속하게 되면, https://evil.com사이트에서 https://bank.com/api로 ajax 요청을 보낼 때 유저가 획득한 인증 토큰이 자동으로 첨부되어 사용자인척하면서 요청을 보낼 수 있게 된다.

이렇게 자동으로 쿠키가 첨부되기 때문에 보안상의 이유로 브라우저는 HTTP 호출을 동일한 출처로 제한했다.

왜 CORS가 생겼을까?

그럼 왜 CORS가 필요하게 됐을까?
이전에는 동일한 도메인에서 리소스를 받아왔는데, 지금은 클라이언트에서 도메인이 다른 서버에서 제공하는 API를 사용하는 일이 많아졌다.
그래서 이전처럼 동일한 도메인간의 요청만 할 수 없어졌고 CORS가 생겼다.

CORS는 어떻게 동작할까?

동일 출처 정책은 브라우저에서 임의로 하는 것이다. 즉 브라우저를 통하지 않고 요청을 보내거나 브라우저에서 동일 출처 정책이 아니라면, 동일 출처가 아니라도 요청을 보내고 응답을 받을 수 있다. 그럼 브라우저에서는 다른 출처로 요청을 보낼 때 어떻게 동작할까?

브라우저는 다른 출처로 요청을 보낼 때 다음과 같은 절차를 거친다. 우선 다른 출처라도, 다 같은 방식으로 동작하지 않는다. CORS 요청에는 simple request와 preflighted request 두 가지가 있다.

Simple request

simple 요청은 pre-flighted 요청을 보내지 않는다.

simple request란 무엇인가.

아래 3가지 조건 중 모두 만족하면, simpe request 이다.

  • GET 요청, HEAD, POST 중의 한 가지 방식을 사용
  • POST 방식일 경우 conte-type이 아래 셋 중 하나여야 한다.
    • application/x-www-form-unlencoded
    • multipart/form-data
    • text/plain

simple request 과정

simple 요청은 다음과 같은 과정을 거친다.

  1. 요청을 보낸다.
  2. 브라우저는 Host와 같은 헤더를 추가하는 것 외에도 교차 출처 요청에 대해 Origin Request Header를 자동으로 추가한다.
GET /products/ HTTP/1.1
Host: api.domain.com
Origin: https://www.domain.com
  1. 서버에서 Origin 리퀘스트 헤더를 확인합니다. Origin 값이 허용되면, Access-Control-Allow-Origin요청 헤더 Origin 값으로 설정한다.
Http/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Content-Type: application/json
  1. 응답을 받은 브라우저는 Access-Control-Allow-Origin 헤더가 탭의 출처와 일치하는지 확인한다. Access-Control-Allow-Origin 값이 정확히 출처와 일치하거나, "*" 와일드 카드 연산자를 포함하는 경우 검사가 통과된다.

서버는 요청의 출처의 따라 요청을 허용할 지 결정할 수 있다. 브라우저는 Origin 요청 헤더가 정확하게 설정해야 한다.

Preflighted request

preflighted 요청은 simple request와는 다른 유형의 CORS 요청이다. 브라우저에서 진짜 요청을 보내기 전에 미리 확인 요청을 보낸다. 이 요청은 OPTIONS 메소드를 사용한다.

preflighted 요청은 다음과 같은 과정을 거친다.

  1. ajax 요청을 보낸다.
OPTIONS /products/ HTTP/1.1
Host: api.domain.com
Origin: https://www.domain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
  1. 서버는 허용된 메소드 및 헤더를 지정하여 응답한다.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Access-Control-Allow-Method: GET, POST, OPTIONS, PUT
Access-Control-Allow-Headers: Authorization, Content-Type
Content-Type: application/json
  1. 헤더와 메소드가 통과되면, 브라우저는 원래 CORS 요청을 보낸다.
POST /products/ HTTP/1.1
Host: api.domain.com
Authorization: token
Content-Type: application/json
Origin: https://www.domain.com
  1. 응답은 Access-Control-Allow-Origin 헤더에 올바른 출처가 있으므로 검사를 통과한다.

CORS 에러 해결하기

지금까지 CORS에 대한 브라우저가 어떻게 동작하는지 알아보았다. 그렇다면, 우리가 클라이언트 개발자나 서버 개발자면 브라우저에게 어떻게 알려줄 수 있을까?

클라이언트에서 해결하기

  1. 웹 브라우저 실행 옵션이나 플러그인을 통한 동일 출처 정책 회피하기
    • 동일 출처 정책은 브라우저에서 임의로 하는 것이기 때문에 브라우저에서 동일 출처 정책을 사용하지 않으며 된다.
  2. jsonp 방식으로 json 데이터 가져오기
    • 자바스크립트 파일이나 css 파일은 동일 출처 정책에 영향을 받지 않고 가져올 수 있다.
    • 이를 이용해서 자바스크립트 파일을 가져와서 이를 json 형식으로 파싱해서 데이터를 사용할 수 있다.

서버에서 해결하기

스프링 CORS

@CrossOrigin 어노테이션 사용하기

메소드 레벨 및 글로벌 레벨에서 srping mvc 애플리케이션에서 spring cors를 지원하는 방법이다. sprign mvc는 @CorssOrigin 어노테이션을 제공한다. 이 어노테이션은 어노테이션이 달린 메소드 또는 타입을 교차 출처를 허용하는 것으로 표시한다.

기본적으로 @CrossOrigin은 모든 출처, 모든 헤더, @RequestMapping 주석에 지정된 Http 메소드에 최대 30분을 허용한다. 어노테이션에 속성 값을 넣어 기본 값을 대체할 수 있다.

속성값을 살펴보면,

  • origins
    • 허용된 출처, 이 값은 pre-flight 응답과 실제 응답 모두에 access-control-allow-origin헤더에 배치된다.
  • allowedHeaders
    • 실제 요청 중에 사용할 수 있는 요청 헤더 목록이다. pre-flight의 응답 헤더인 access-control-allow-header에 값이 사용된다.
  • allowCredential
    • 브라우저가 요청과 관련된 쿠키를 포함해야 되는지 여부를 결정한다.
    • 이 값이 true이면, pre-flight 응답에는 값이 true로 설정된 access-control-allow-credentials 헤더가 포함된다.
@CrossOrigin(origin="*", allowedHeaders = "*")
@Controller
public class MainController {
	@GetMapping(path = "/")
	public String main(Model model) {
		return "main";
	}
}

CorsFilter 사용하기

서블릿 필터 인터페이스를 이용하여 개발되었다. 웹 서버의 모든 리소스의 요청을 가로채서 Cross domain request인지 체크하여 실제 요청 페이지에 전달하기전에 적절한 CORS 정책과 해더들을 적용한다.

  • Access-Control-Allow-Origin
    • 도메인 간 요청을 할 수 있는 권한이 부여된 도메인을 지정한다.
  • Access-Control-Allow-Credentials
    • 도메인 간 요청에 credential 권한이 있는지 없는지 지정한다.
  • Access-Control-Expose-Headers
    • 노출하기에 안전한 헤더를 나타낸다.
  • Access-Control-Max-Age
    • pre-flighted 요청이 얼마만큼의 시간동안 캐시되는지
  • Access-Control-Allow-Methods
    • 리소스에 접근할 때 메소드가 허용되는지
  • Access-Control-Allow-Headers
    • 어떤 헤더 필드 네임이 실제 요청에서 사용할 수 있는지 가리킨다.
@Component
public class SimpleCorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, PATCH");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");
        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

정리

CORS 에러를 클라이언트에서 해결할 수 있는 방법이 있는지 몰랐다. 서버에서만 허용이 가능한 것인줄만 알았는데, 브라우저에서 컨트롤하는 것이었다니! 정확한 동작 방식을 알고 나니 사용한 코드도 더 잘 이해된다. 🏃🏽‍♀️🏃🏽‍♀️

참고한 자료

https://brownbears.tistory.com/336

 

 

CORS에 대한 기본적인 내용

이렇듯 우리가 겪는 CORS 관련 이슈는 모두 CORS 정책을 위반했기 때문에 발생하는 것이다. 개발하는 입장에서는 저 정책 때문에 신경써야 하는 것들이 늘어나니 귀찮을 수도 있지만, 사실 CORS라는 방어막이 존재하기 때문에 우리가 이 곳 저 곳에서 가져오는 리소스가 안전하다는 최소한의 보장을 받을 수 있는 것이다.

CORS는 Cross-Origin Resource Sharing의 줄임말로, 한국어로 직역하면 교차 출처 리소스 공유라고 해석할 수 있다. 여기서 “교차 출처”라고 하는 것은 “다른 출처”를 의미하는 것인데, 아무래도 Cross라는 영단어가 가지는 뉘앙스가 한국어와 조금은 다르다보니 CORS를 그대로 직역한 교차 출처 리소스 공유라는 말만 보고는 어떤 의미인지 감을 잡기가 조금은 어려운 것 같다.

그래서 필자는 조금 더 쉬운 이해를 위해 교차 출처라는 말 대신 “다른 출처”라는 단어를 사용해서 이 포스팅을 풀어나갈까 한다.

일단 다른 출처간의 리소스 공유에 대해서 알아보기에 앞서 간단하게 이 출처(Origin)라는 것이 정확히 뭘 의미하는지부터 한번 짚고 넘어가도록 하자.

출처(Origin)가 무엇인가요?

서버의 위치를 의미하는 https://google.com과 같은 URL들은 마치 하나의 문자열 같아 보여도, 사실은 여러 개의 구성 요소로 이루어져있다.

 

이때 출처는 Protocol과 Host, 그리고 위 그림에는 나와있지 않지만 :80, :443과 같은 포트 번호까지 모두 합친 것을 의미한다. 즉, 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것이다.

또한 출처 내의 포트 번호는 생략이 가능한데, 이는 각 웹에서 사용하는 HTTP, HTTPS 프로토콜의 기본 포트 번호가 정해져있기 때문이다. HTTP가 정의된 RFC 2616 문서를 보면 다음과 같이 기본 포트 번호가 함께 정의되어있는 것을 볼 수 있다.

3.3.2 http URL


If the port is empty or not given, port 80 is assumed. The semantics are that the identified resource is located at the server listening for TCP connections on that port of that host, and the Request-URI for the resource is abs_path (section 5.1.2).

그러나 만약 https://google.com:443과 같이 출처에 포트 번호가 명시적으로 포함되어 있다면 이 포트 번호까지 모두 일치해야 같은 출처라고 인정된다. 하지만 이 케이스에 대한 명확한 정의가 표준으로 정해진 것은 아니기 때문에, 더 정확히 이야기하자면 어떤 경우에는 같은 출처, 또 어떤 경우에는 다른 출처로 판단될 수도 있다. (진리의 케바케)

우리는 브라우저의 개발자 도구의 콘솔에서 Location 객체가 가지고 있는 origin 프로퍼티에 접근함으로써 손 쉽게 어플리케이션이 실행되고 있는 출처를 알아낼 수도 있다.

console.log(location.origin);
"https://evan-moon.github.io"

SOP(Same-Origin Policy)

웹 생태계에는 다른 출처로의 리소스 요청을 제한하는 것과 관련된 두 가지 정책이 존재한다. 한 가지는 이 포스팅의 주제인 CORS, 그리고 또 한 가지는 SOP(Same-Origin Policy)이다.

SOP는 지난 2011년, RFC 6454에서 처음 등장한 보안 정책으로 말 그대로 “같은 출처에서만 리소스를 공유할 수 있다”라는 규칙을 가진 정책이다.

그러나 웹이라는 오픈스페이스 환경에서 다른 출처에 있는 리소스를 가져와서 사용하는 일은 굉장히 흔한 일이라 무작정 막을 수도 없는 노릇이니 몇 가지 예외 조항을 두고 이 조항에 해당하는 리소스 요청은 출처가 다르더라도 허용하기로 했는데, 그 중 하나가 “CORS 정책을 지킨 리소스 요청”이다. (참고로 CORS라는 이름이 처음 등장한 것은 2009년이라, SOP의 등장보다 빠르다)

Access to network resources varies depending on whether the resources are in the same origin as the content attempting to access them.

Generally, reading information from another origin is forbidden. However, an origin is permitted to use some kinds of resources retrieved from other origins. For example, an origin is permitted to execute script, render images, and apply style sheets from any origin. Likewise, an origin can display content from another origin, such as an HTML document in an HTML frame. Network resources can also opt into letting other origins read their information, for example, using Cross-Origin Resource Sharing.

RFC 6454 - 3.4.2 Network Access

우리가 다른 출처로 리소스를 요청한다면 SOP 정책을 위반한 것이 되고, 거기다가 SOP의 예외 조항인 CORS 정책까지 지키지 않는다면 아예 다른 출처의 리소스를 사용할 수 없게 되는 것이다.

즉, 이렇게 다른 출처의 리소스를 사용하는 것을 제한하는 행위는 하나의 정책만으로 결정된 사항이 아니라는 의미가 되며, SOP에서 정의된 예외 조항과 CORS를 사용할 수 있는 케이스들이 맞물리지 않을 경우에는 아예 리소스 요청을 할 수 없는 케이스도 존재할 수 있다.

근데 왜 이렇게 귀찮은 정책을 만들어서 개발자들을 괴롭히는 것일까? 어차피 개발자는 정해진 서버하고만 통신을 하도록 어플리케이션을 작성할 텐데 말이다.

'개인공부' 카테고리의 다른 글

예외클래스 문제출제  (0) 2022.10.29
인텔리제이 단축키  (0) 2022.10.27
HTTP 프로토콜  (0) 2022.10.20
Was / Web Server 차이점  (0) 2022.10.20
Client / Server 지식  (0) 2022.10.20

댓글