Home Cors
Post
Cancel

Cors

CORS

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

보안상의 이유로 다른 도메인의 리소스를 가져오는 것이 불가능했다. (SOP : Single-Origin-Policy)

하지만 어플리케이션을 개선하고 쉽게 개발하기 위해선 다른 도메인에 요청을 보내는 일은 필연적이다.

이를 해결하고자 등장한 표준 기술이 CORS이다.


CORS란 Cross Origin Resource Sharing의 약자로 도메인이 다른 자원에 리소스를 요청할 때 접근 권한을 부여하는 메커니즘이다.





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란 URL 구조에서 살펴본 Protocol, Host, Port를 합친 것을 말한다.


image


Origin이 다르다는 말은, HTTP, HTTPS 프로토콜이 다르거나, 주소가 다르거나, 포트번호가 다르다는 말이다.

image





Same Origin Policy(SOP) : 동일 출처 정책

동일 출처(Same-Origin) 서버에 있는 리소스는 자유로이 가져올수 있지만,

다른 출처(Cross-Origin) 서버에 있는 이미지나 유튜브 영상 같은 리소스는 상호작용이 불가능하다.


동일 출처가 아닌 경우 접근을 차단하는 이유는

해커가 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting) 등의 방법을 이용해서

우리가 만든 어플리케이션에서 해커가 심어놓은 코드가 실행하여 개인 정보를 가로챌 수 있다.


이러한 악의적인 경우를 방지하기 위해 SOP 정책으로 동일하지 않는 다른 출처의 스크립트가 실행되지 않도록

브라우저에서 사전에 방지하는 것이다.

→ 출처를 비교하는 로직은 서버에 구현된 스펙이 아닌 브라우저에 구현된 스펙이다.


image






브라우저의 CORS 기본 동작

1. 클라이언트에서 HTTP요청의 헤더에 Origin을 담아 전달(ex. Origin: http://localhost:8080)

image


2. 서버는 응답헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달한다.

(ex.Acess-Control-Allow-Origin: http://localhost:8080)

image


3. 클라이언트에서 Origin과 서버가 보내준 Access-Control-Allow-Origin을 비교한다.



이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과

서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해본 후 차단할지 말지를 결정한다.

만약 유효하지 않다면 그 응답을 사용하지 않고 버린다. 👉🏻 CORS 에러


결국 CORS 해결책은 서버의 허용이 필요하다.





CORS 동작 방식

CORS가 동작하는 방식은 3가지가 있다고 한다.


1. 예비 요청 (Preflight Request)

클라이언트에서 요청하려는 URL이 외부 도메인일 경우, 웹브라우저에서 자체적으로 실행된다.


브라우저는 요청을 보낼 때 한번에 바로 보내지않고 먼저 예비 요청을 보내 서버와 잘 통신되는지 확인한 후 본 요청을 보낸다.

예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 안전한 요청인지 미리 확인하는 것이다.


이때, 브라우저가 예비요청을 보내는 것을 Preflight라고 부르며

이 예비요청의 HTTP 메소드를 GET이나 POST가 아닌 OPTIONS라는 요청이 사용된다는 것이 특징이다.



js에서 api 요청 예시

🔷 js에서 start()를 통해 resource를 받아오려고 한다.

🔷 브라우저는 서버로 HTTP OPTIONS를 통해 예비 요청(Preflight)을 먼저 보낸다.

🔷 서버는 예비 요청에 대한 응답으로 어떤 것을 허용하고 금지하고 있는지에 대한 header 정보를 담아서 브라우저로 보낸다.

🔷 브라우저는 보낸 요청과 서버가 응답해준 정책을 비교하여 해당 요청이 안전한지 확인하고 본 요청을 보낸다.

🔷 서버가 본 요청에 대한응답을 하면 최종적으로 응답 데이터를 js로 넘겨준다.



실제 api 요청을 보내면 chrome에서 client와 server가 본 요청을 보내기 전에

예비 요청(preflight) 통신을 하고 있는 것을 볼 수 있다.

image

image

예비 요청은 GET이나 POST가 아닌 OPTIONS라는 독립적인 요청 메소드로 보내진다.



image

image

Request header의 Origin과 Response header의 Access-Control-Allow-Origin의 출처를 비교한다.

만약 둘이 다르게되면 브라우저는 이 요청이 CORS 정책을 위반했다고 판단하고 에러를 내보낸다.



Requset headers (클라이언트 요청 헤더)

  • Origin: 요청을 보내는 페이지의 출처 (도메인)

  • Access-Content-Request-Method: 실제 요청하려는 메소드

  • Access-Content-Request-Headers: 실제 요청에 포함되어 있는 헤더 이름



Response headers (서버 응답 헤더)

  • Access-Control-Allow-Origin: 요청을 허용하는 출처
    ( * 와일드 카드이면 모든 곳에서 허용, 특정하려면 Protocol + Host + Port 입력)

  • Access-Control-Allow-Credentials: 클라이언트 요청이 쿠키를 통해서 자격 증명을 하는 경우에 true
    true를 응답받은 클라이언트는 실제 요청 시 서버에서 정의된 규격의 인증값이 담긴 쿠키를 같이 보내야 한다.

  • Access-Control-Expose-Headers: 클라이언트 요청에 포함되어도 되는 사용자 정의 헤더

  • Access-Control-Max-Age: 클라이언트에서 Preflight 의 요청 결과를 저장할 기간을 지정한다.
    클라이언트에서 Preflight 요청의 결과를 저장하고 있을 시간이다.
    해당 시간 동안은 Preflight 요청을 다시 하지 않게된다.

  • Access-Control-Allow-Methods: 요청을 허용하는 메서드, 기본값은 GET, POST라고 보면된다.
    이 헤더가 없으면 GET과 POST요청만 가능하다.
    만약 이 헤더가 지정되어 있으면 클라이언는 헤더 값에 해당하는 메서드일 경우에만 실제 요청을 시도한다.

  • Access-Control-Allow-Headers: 요청을 허용하는 헤더



요청을 보내기 전에 예비요청을 보낸다면

실제 요청에 걸리는 시간이 늘어나게 되어 어플리케이션 성능에 영향을 미치기 때문에

브라우저 캐시를 이용해 Access-Control-Max-Age를 활용하면 Preflight 요청을 캐싱시켜 최적화를 시켜줄 수 있다.

image

3600초 → 1시간


예비 요청 캐싱 기간에 대해서는, 파이어폭스 브라우저는 86400초(24시간) 까지 가능하지만

크로미움 기반 브라우저는 7200초(2시간)이 최대이다.



예비 요청 캐시 동작과정

🔷 브라우저는 예비(Preflight) 요청을 할 때마다 먼저 Preflight 캐시를 확인하여 해당 요청에 대한 응답이 있는지 확인한다.

🔷 만일 응답이 캐싱 되어 있지 않다면,  서버에 예비 요청을 보내 인증 절차를 밟는다.

🔷 만일 서버로 부터 Access-Control-Max-Age 응답 헤더를 받는다면 그 기간 동안 브라우저 캐시에 결과를 저장한다.

🔷 다시 요청을 보내고 만일 응답이 캐싱 되어 있다면 예비 요청을 서버로 보내지 않고 대신 캐시된 응답을 사용한다.





2. 단순 요청 (Simple Request)

예비 요청(Prefilght)을 생략하고 바로 서버에 직행으로 본 요청을 보낸 후

서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin 헤더를 보내주면

브라우저가 CORS정책 위반 여부를 검사하는 방식이다.


but, 아래 3가지 경우를 만족 할 때만 가능하다.


1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.

2. AcceptAccept-LanguageContent-LanguageContent-TypeDPRDownlinkSave-DataViewport-WidthWidth
헤더일 경우에만 적용된다.

3. Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain중 하나여야한다.
아닐 경우 예비 요청으로 동작된다.



위 조건을 모두 만족되어 단순 요청이 일어나는 상황은 드물다.

대부분 HTTP API 요청은 text/xml 이나 application/json 으로 통신하기 때문에

3번째 Content-Type이 위반되기 때문이다.


따라서 대부분의 API 요청은 예비 요청(preflight)으로 이루어진다 라고 이해하면 된다.





3. 인증된 요청 (Credentialed Request)

인증된 요청은 클라이언트에서 서버에게 자격 인증 정보(Credential)를 실어 요청할 때 사용되는 요청이다.


여기서 말하는 자격 인증 정보란 세션 ID가 저장되어있는 쿠키(Cookie) 혹은

Authorization 헤더에 설정하는 토큰 값 등을 일컫는다.


즉, 클라이언트에서 일반적인 JSON 데이터 외에도 쿠키 같은 인증 정보를 포함해서

다른 출처의 서버로 전달할 때 CORS의 세가지 요청 중 하나인 인증된 요청으로 동작된다




1. 클라이언트에서 인증 정보를 보내도록 설정하기

기본적으로 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이

브라우저의 쿠키와 같은 인증과 관련된 데이터를 함부로 요청 데이터에 담지 않도록 되어있다.


  • same-origin(기본값) : 같은 출처 간 요청에만 인증 정보를 담을 수 있다.

  • include : 모든 요청에 인증 정보를 담을 수 있다.

  • omit : 모든 요청에 인증 정보를 담지 않는다.


이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션이다.

만일 이러한 별도의 설정을 해주지 않으면 쿠키 등의 인증 정보는 절대로 자동으로 서버에게 전송되지 않는다.



2. 서버에서 Access-Control-Allow-Origin 헤더 설정하기

  • 응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 한다.

  • 응답 헤더의 Access-Control-Allow-Origin 의 값에 와일드카드 문자(*)는 사용할 수 없다.

  • 응답 헤더의 Access-Control-Allow-Methods 의 값에 와일드카드 문자(*)는 사용할 수 없다.

  • 응답 헤더의 Access-Control-Allow-Headers 의 값에 와일드카드 문자(*)는 사용할 수 없다.



응답의 Access-Control-Allow-Origin 헤더가 와일드카드(*)가 아닌 분명한 Origin으로 설정되어야 하고

Access-Control-Allow-Credentials 헤더는 true로 설정되어야 한다.

그렇지 않으면 브라우저의 CORS 정책에 의해 응답이 거부된다.

(인증 정보는 민감한 정보이기 때문에 출처를 정확하게 설정해주어야 한다)





직접 서버에서 HTTP 헤더 설정을 통해 출처를 허용하게 설정하는 가장 정석적인 해결책이다.



@CrossOrigin

가장 단순한 해결책은 @CrossOrigin 을 사용하는 것이다.

@CrossOrigin을 사용하면 다른 도메인의 클라이언트가 나의 서버에 요청 보내는 것을 허용한다.


ex) 서버 : http://localhost:8080, 클라이언트 : http://localhost:8081

1
2
3
4
5
6
7
8
@RestController
public class Controller {
    @CrossOrigin("http://localhost:8081")
    @GetMapping("/hello")
    public String say() {
        return "hello-world";
    }
}


1
2
3
4
@RestController
@CrossOrigin(origins = "http://localhost:8081", allowedHeaders = "GET")
public class Controller {
}

하지만 위처럼 작성한다면 수 많은 Controller에 일일히 작성해야한다.





Filter

*Filter 는 꼭 javax.servlet 의 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
35
36
37
import javax.servlet.*;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
    private static final String[] allowedOrigins = {
        "http://localhost:8080", "http://localhost:8081"
    };

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @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", allowedOrigins);
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS, PUT, POST, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers",
                "Origin, X-Requested-With, Content-Type, Accept, Authorization");

        if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        }else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void destroy() {

    }
}





Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
http.authorizeRequests()
            .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
/*
CORS preflight 요청에 대해 모든 경로에서 접근을 허용
→ CORS preflight 요청은 인증처리를 하지 않겠다

CORS semantic 상으로 CORS prefight에는 Authorization 헤더를 줄 이유가 없으므로
CORS preflight 요청에 대해서는 401 응답(Unauthorized→  인증이 필요함) 을 하면 안된다. 
*/


@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOriginPatterns(List.of("https://www.domain.com"));
    configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setMaxAge(3600L); // 1 HOUR  
    configuration.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

CorsConfiguration : CORS 정책을 정의

UrlBasedCorsConfigurationSource : CORS 구성을 URL 패턴에 기반하여 설정

source.registerCorsConfiguration("/**", configuration); : 모든 URL에 대해 CORS 구성을 적용





setAllowedOrigins("*")setAllowedOriginPatterns("*")

위에서 설명했듯이

setAllowedOrigins("*")를 적용하면 setAllowCredentials(true)는 사용할 수 없고

setAllowedOriginPatterns("*")를 적용해야 했다. 정확히 말하자면 와일드카드를 사용할 수 없다.


더 유연한 Origin 지정이 필요한 경우에는 setAllowedOriginPatterns를 사용하고,(ex.https://*.domain.com)

명시적인 Origin을 지정하는 경우에는 setAllowedOrigins를 사용하는데 (ex.https://domain.com)

문제는 명시적으로 작성해도 잘 동작하지 않아 setAllowedOriginPatterns를 사용하게 되었다.


setAllowCredentials(true)는 CORS(Cross-Origin Resource Sharing) 관련 설정 중 하나로,

브라우저 간의 요청에서 인증 정보를 포함하도록 허용하는 옵션이다.



S3

S3 CORS(Cross-origin 리소스 공유)

Amazon S3 > 버킷 > ${버킷 이름} > 권한 탭

제일 하단으로 내리면 CORS(Cross-origin 리소스 공유)가 보인다.

편집을 누르고 아래를 붙여넣으면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
  {
    "AllowedHeaders": [
      "Authorization"
    ],
    "AllowedMethods": [
      "GET",
      "HEAD"
    ],
    "AllowedOrigins": [
      "https://www.domain.com"
    ],
    "ExposeHeaders": [
      "Access-Control-Allow-Origin"
    ]
  }
]





CORS 에러 CODE

이 글에서 cors 에러가 떠서 security를 수정했는데 남은 기능을 test 해보니 전부 CORS 에러가 떴다.

문제는 모든 api에 .antMatchers("/ws/**").permitAll()와 같이

.permitAll()을 해버리면 인증 없이도 접근 가능한 페이지가 많아진다.

따라서 cors 에러를 해결했다고 볼 수가 없다.


글 하단에 있는 REFERENCE들을 참고하여 수정했다.


image
Class CorsConfiguration

addAllowedOrigin()setAllowedOrigins()의 차이는

addAllowedOrigin은 허용된 origin을 하나씩 추가하는 것이고

setAllowedOrigins은 list 형태로 여러 개를 한번에 추가할 수 있다.



setAllowedOrigins()을 활용해 origin을 추가했다.





REFERENCE

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