Post

Cors

Cors

CORS

기존 브라우저 정책은 서로 다른 도메인으로부터 리소스가 필요한 경우

보안상의 이유로 다른 도메인의 리소스를 가져오는 것이 불가능했다.

이를 SOP (Same-Origin Policy, 동일 출처 정책) 이라고 한다.


하지만 실제 서비스 개발에서는 프론트엔드와 백엔드 서버의 도메인이 다르거나

외부 API 서버와 통신해야 하는 경우가 매우 많다.

이 문제를 해결하기 위해 등장한 기술이 CORS 다.


CORS란 Cross-Origin Resource Sharing 의 약자로

서로 다른 출처(Origin) 간의 리소스 요청을 허용하기 위한 메커니즘이다.




URL 구조

URL 구성 요소

  • Protocol(Scheme) : http, https

  • Host : 사이트 도메인

  • Port : 포트 번호

  • Path : 서버 내부 경로

  • Query String : 요청 파라미터(key, value)

  • Fragment : 해시 값



1
https://localhost:8080/board?page=1

Protocol : https

Host : localhost

Port : 8080

Path : /board

Query String : ?page=1




Origin 이란?

Origin은 다음 세 가지를 합친 개념

  • Protocol

  • Host

  • Port


아래 셋 중 하나라도 다르면 다른 Origin

  • http ↔ https

  • localhost ↔ 127.0.0.1

  • 8080 ↔ 8081

image




Same-Origin Policy (SOP)

동일 출처 정책은 같은 Origin 에 있는 리소스만 접근 가능하도록 제한하는 브라우저 정책이다.

다른 출처의 리소스는 기본적으로 차단된다.

SOP이 필요한 이유는 CSRF, XSS 같은 공격으로부터 사용자를 보호하기 위해서다.

SOP은 서버가 아니라 브라우저에 구현되어 있다.




브라우저의 CORS 기본 동작 원리

1. 클라이언트가 요청을 보낼 때 Origin 헤더를 함께 보낸다.

Origin: http://localhost:8080


2. 서버는 응답 헤더에 다음을 포함한다.

Access-Control-Allow-Origin: http://localhost:8080


3. 브라우저는 자신이 보낸 Origin과 서버가 허용한 Origin을 비교한다.

일치하지 않으면 응답을 차단한다.

이것이 CORS 에러다.




CORS 동작 방식

1. Preflight Request (예비 요청)

브라우저는 실제 요청 전에 OPTIONS 메서드로 먼저 확인 요청을 보낸다.

이 요청을 통해 서버가 해당 요청을 허용하는지 검사한다.



동작 순서

1. 브라우저가 OPTIONS 요청 전송

image

image


2. 서버가 허용 정책을 응답

image


3. 브라우저가 정책 검사

image


4. 통과 시 실제 요청 전송

image


서버 응답 헤더

Access-Control-Allow-Origin: http://localhost:8080

Access-Control-Allow-Methods: GET, POST, PUT, DELETE

Access-Control-Allow-Headers: Content-Type, Authorization

Access-Control-Allow-Credentials: true

Access-Control-Max-Age: 3600

3600초 동안 Preflight 요청을 생략할 수 있다.




2. Simple Request (단순 요청)

아래 조건을 모두 만족하면 Preflight 없이 바로 요청한다.

1. GET, HEAD, POST 중 하나

2. 허용된 헤더만 사용

3. Content-Type이 다음 중 하나

  • application/x-www-form-urlencoded

  • multipart/form-data

  • text/plain


하지만 대부분 application/json을 사용하므로 실제로는 Preflight가 발생한다.




3. Credentialed Request (인증된 요청)

쿠키나 Authorization 헤더 같은 인증 정보를 포함하는 요청


클라이언트 설정

1
2
3
4
fetch("https://api.adme.com/api", {
    method: "GET",
    credentials: "include"
});

credentials 옵션

  • same-origin (기본값)

  • include

  • omit




서버 설정 조건

Access-Control-Allow-Credentials: true

Access-Control-Allow-Origin 에 * 사용 불가

반드시 명확한 Origin을 지정해야 한다.




Spring 에서 CORS 설정

1. @CrossOrigin

ex) 서버 : https://api.adme.com, 클라이언트 : "https://www.adme.com"


1
2
3
4
5
6
7
8
9
@RestController
public class Controller {

    @CrossOrigin("https://www.adme.com")
    @GetMapping("/hello")
    public String say() {
        return "hello-world";
    }
}

단점은 모든 Controller에 작성해야 한다.




2. Filter 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    private static final String ALLOWED_ORIGIN = "https://www.adme.com";

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws java.io.IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        response.setHeader("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers",
                "Origin, Content-Type, Accept, Authorization");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        chain.doFilter(req, res);
    }
}




3. Spring Security Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean
public CorsConfigurationSource corsConfigurationSource() {

    CorsConfiguration configuration = new CorsConfiguration();

    configuration.setAllowedOriginPatterns(List.of("https://www.adme.com"));

    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));

    configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));

    configuration.setAllowCredentials(true);

    configuration.setMaxAge(3600L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

    source.registerCorsConfiguration("/**", configuration);

    return source;
}




setAllowedOrigins vs setAllowedOriginPatterns

setAllowedOrigins(“*”) 는 setAllowCredentials(true) 와 함께 사용할 수 없다.

*와일드카드가 필요하면 setAllowedOriginPatterns 를 사용

1
configuration.setAllowedOriginPatterns(List.of("https://*.adme.com"));




정리

CORS는 서버 문제가 아니라 브라우저 보안 정책이다.

서버가 정확한 Origin을 명시적으로 허용해서 해결한다.





REFERENCE

This post is licensed under CC BY 4.0 by the author.