ch10 Configuring Cross-Origin Resource Sharing(CORS)

2024. 3. 3. 10:30Spring Security

이 장에서는 다음을 다룹니다.

  • 교차 출처(Cross-Origin) 리소스 공유(CORS)란 무엇인가 
  • 교차 출처 리소스 공유 구성 적용하기

이 장에서는 교차 출처 리소스 공유(CORS)에 대해 논의하고 Spring Security와 함께 적용하는 방법을 다룹니다. 먼저, CORS란 무엇이며 왜 중요한가요? CORS가 필요한 이유는 웹 애플리케이션에서 비롯되었습니다. 기본적으로, 브라우저는 사이트가 로드된 도메인 이외의 어떤 도메인에 대한 요청도 허용하지 않습니다. 예를 들어, example.com에서 사이트에 접근할 경우, 브라우저는 사이트가 api.example.com에 요청을 하는 것을 허용하지 않습니다. 그림 10.1은 이 개념을 보여줍니다.

웹 개발을 하다 보면 다른 출처(origin)의 리소스를 사용해야 할 때가 있습니다. 여기서 출처란 프로토콜, 호스트(도메인), 포트를 의미합니다. 예를 들어, http://example.com에서 서비스되는 웹 페이지가 https://api.example.com에서 데이터를 가져오려고 하는 경우, 이 두 출처는 프로토콜(http와 https)과 서브도메인이 다르기 때문에 '다른 출처'로 간주됩니다.
이런 상황에서는 보안상의 이유로 브라우저의 '동일 출처 정책(Same-Origin Policy)'이 작동하여, 기본적으로 다른 출처에 대한 요청을 제한합니다. 동일 출처 정책은 웹 보안의 중요한 부분이며, XSS(Cross-Site Scripting) 공격과 같은 여러 보안 위협으로부터 사용자를 보호하는 데 도움을 줍니다.
그러나 현대의 웹 애플리케이션은 다양한 출처에서 리소스를 가져와 사용하는 경우가 많으며, 이를 위해 CORS 정책이 도입되었습니다. CORS는 웹 애플리케이션에서 다른 출처의 리소스에 안전하게 접근할 수 있도록 하는 메커니즘을 제공합니다. 서버 측에서는 특정 출처에서의 요청을 허용하거나, 모든 출처에서 요청을 허용할 수 있는 설정을 할 수 있습니다. 예를 들어, api.example.com 서버는 CORS 헤더인 Access-Control-Allow-Origin을 사용하여 example.com에서 오는 요청을 허용할 수 있습니다. 이 헤더를 응답에 포함시키면, 브라우저는 example.com이 api.example.com의 데이터를 요청하는 것을 허용합니다. Spring Security와 함께 CORS를 구성하는 것은 이러한 허용 과정을 보다 세밀하게 제어할 수 있게 해줍니다.
예를 들어, 어떤 HTTP 메서드(GET, POST 등)를 허용할지, 어떤 헤더를 허용할지, 인증 정보(쿠키 등)의 사용을 허용할지 등을 설정할 수 있습니다. Spring Security의 CORS 설정은 보안과 관련된 다른 설정들과 함께 중앙 집중적으로 관리될 수 있어, 애플리케이션의 보안을 강화하는 데 도움이 됩니다.
이러한 CORS 설정을 통해, 개발자는 웹 애플리케이션의 보안을 유지하면서도 필요에 따라 다양한 출처의 리소스를 자유롭게 활용할 수 있는 유연성을 얻을 수 있습니다.

 

그림 10.1 교차 출처 리소스 공유(CORS). example.com에서 접근했을 때, 웹사이트는 api.example.com으로 요청을 할 수 없습니다. 왜냐하면 그것들은 교차 도메인 요청이기 때문입니다.

 

간단히 말해서, 앱은 CORS 메커니즘을 사용하여 이 엄격한 정책을 완화하고 일정 조건에서 다른 출처 간의 요청을 허용합니다. 프론트엔드와 백엔드가 별도의 애플리케이션인 현재와 같은 시대에, 특히 애플리케이션에 이를 적용해야 할 가능성이 높기 때문에 이를 알아야 합니다. Angular, ReactJS, Vue와 같은 프레임워크를 사용하여 개발된 프론트엔드 애플리케이션이 example.com과 같은 도메인에서 호스팅되고, 다른 도메인인 api.example.com에 호스팅된 백엔드의 엔드포인트를 호출하는 것은 흔한 일입니다.

이 장에서는 웹 애플리케이션에 CORS 정책을 적용하는 방법을 배울 수 있는 몇 가지 예제를 개발합니다. 또한 애플리케이션에 보안 취약점을 남기지 않도록 주의해야 할 몇 가지 세부 사항도 설명합니다.

 

10.1 How does CORS work?

이 섹션에서는 CORS가 웹 애플리케이션에 어떻게 적용되는지에 대해 논의합니다. 예를 들어, 여러분이 example.com의 소유주이고 어떤 이유로 example.org의 개발자들이 그들의 웹사이트에서 여러분의 REST 엔드포인트(api.example.com)를 호출하기로 결정했다면, 그들은 그렇게 할 수 없을 것입니다. 이와 유사한 상황이, 예를 들어 어떤 도메인이 iframe을 사용하여 여러분의 애플리케이션을 로드할 때도 발생할 수 있습니다(그림 10.2 참조).

※위 설명에서 도메인이란 인터넷 상에서 웹사이트를 식별하는 주소를 의미합니다. 구체적으로, 도메인 이름은 인터넷의 IP 주소를 사람이 읽고 이해하기 쉬운 형태로 변환한 것입니다. 예를 들어, "example.com"이라는 도메인 이름은 특정 웹 서버의 위치를 가리키며, 이를 통해 사용자는 웹 브라우저에 주소를 입력함으로써 해당 웹사이트에 접근할 수 있습니다.
따라서, "같은 상황이 예를 들어 도메인이 iframe을 사용하여 여러분의 애플리케이션을 로드할 때도 발생할 수 있습니다"라는 문장에서 말하는 "도메인"은 여러분의 애플리케이션-여러분의 웹 어플리케이션이 제공하는 웹 페이지-을 iframe 내에 내장하려는 다른 웹사이트의 주소를 말합니다. 예를 들어, "example.com"이 여러분의 웹사이트이고, "anotherdomain.com"에서 여러분의 웹사이트를 iframe을 통해 자신의 페이지 내에 표시하려고 할 때, "anotherdomain.com"이 바로 그 도메인에 해당합니다. 이 경우, "anotherdomain.com"은 여러분의 애플리케이션을 자신의 웹사이트 내에 삽입하려고 하지만, CORS 정책으로 인해 이러한 통합이 제한될 수 있습니다.
참고: iframe은 어떤 웹 페이지에서 생성된 콘텐츠를 다른 웹 페이지 안에 내장하기 위해 사용하는 HTML 요소입니다(예를 들어, example.org의 콘텐츠를 example.com의 페이지 안에 통합하기 위해 사용됩니다).

 

어떤 애플리케이션이 두 개의 다른 도메인 간에 호출을 하는 상황은 금지되어 있습니다. 하지만, 물론 이러한 호출을 해야 하는 경우들을 찾을 수 있습니다. 이런 상황에서 CORS는 여러분의 애플리케이션이 어떤 도메인에서의 요청을 허용하고 어떤 세부 정보를 공유할 수 있는지를 지정할 수 있게 해줍니다. CORS 메커니즘은 HTTP 헤더를 기반으로 작동합니다(그림 10.3). 

그림 10.2 example.com 도메인에서 iframe을 통해 example.org 페이지가 로드되더라도, example.org에 로드된 콘텐츠에서의 호출은 로드되지 않습니다. 애플리케이션이 요청을 하더라도, 브라우저는 응답을 받아들이지 않습니다.

위 내용은 웹 보안의 중요한 측면 중 하나인 동일 출처 정책(Same-Origin Policy)과 관련이 깊습니다. 이 정책은 웹 애플리케이션이 다른 출처의 리소스를 불러오는 것을 기본적으로 제한합니다. 이는 사용자의 데이터를 보호하기 위해 설계된 것입니다.
예를 들어, example.com 도메인에서 호스팅되는 웹 페이지가 있고, 이 페이지 안에 example.org의 내용을 보여주기 위해 iframe을 사용한다고 가정해봅시다. 여기서 example.org 페이지는 example.com과는 다른 출처(origin)에 속합니다.
사용자가 example.com에서 호스팅되는 페이지를 방문하면, 이 페이지 내에 삽입된 iframe을 통해 example.org의 내용이 로드됩니다. 그러나 example.org에서 실행되는 스크립트가 api.example.com 등 example.com 도메인 외부의 API나 리소스에 접근하려고 시도한다면, 동일 출처 정책에 의해 이러한 요청은 차단됩니다.
즉, example.org 내에서 로드된 콘텐츠는 example.com 도메인의 리소스에 직접 접근할 수 없습니다. 심지어 애플리케이션이 요청을 하더라도, 브라우저는 이러한 요청의 응답을 받아들이지 않을 것입니다. 이는 보안 위협으로부터 사용자를 보호하기 위한 조치입니다.
이러한 제한을 완화하기 위해 CORS(교차 출처 리소스 공유) 정책이 사용됩니다. CORS는 특정 조건 하에서, 예를 들어 서버가 명시적으로 허용하는 경우, 다른 출처 간의 요청을 가능하게 합니다. 서버는 특정 HTTP 헤더를 사용하여, 어떤 출처의 웹 페이지가 자신의 리소스에 접근할 수 있는지를 브라우저에 알릴 수 있습니다.

 

가장 중요한 헤더는 다음과 같습니다.

  • Access-Control-Allow-Origin — 여러분이 구현한 도메인의 리소스에 접근할 수 있는 외부 도메인(출처)을 지정합니다.
  • Access-Control-Allow-Methods 다른 도메인에 대한 액세스를 허용하고 특정 HTTP 메소드만 허용하려는 상황에서 일부 HTTP method만 참조할 수 있습니다.
    예를 들어 example.com을 활성화하여 일부 엔드포인트를 호출하려는 경우 이 방법을 사용합니다. 단, 예를 들어 HTTP GET만 사용해야 합니다.
  • Access-Control-Allow-Headers 특정 요청에서 사용할 수 있는 헤더에 제한을 추가합니다.
    예를 들어, 클라이언트가 특정 요청에 대해 특정 헤더를 보내는 것을 원하지 않습니다.

그림 10.3 Enabling cross-origin requests. example.org 서버는 Access-Control-Allow-Origin 헤더를 추가하여 브라우저가 응답을 수락해야 할 요청의 출처를 지정합니다. 호출이 이루어진 도메인이 출처 목록에 포함되어 있다면, 브라우저는 응답을 수락합니다.

Access-Control-Allow-Origin 헤더
Access-Control-Allow-Origin 헤더는 서버가 응답에 포함시켜, 어떤 출처(origin)의 웹 페이지가 해당 서버의 리소스에 접근할 수 있는지를 브라우저에 알립니다. 이 헤더의 값으로는 구체적인 출처(예: https://example.com), 또는 모든 출처를 허용하는 *가 올 수 있습니다. 예를 들어, example.org 서버가 특정 리소스에 대한 응답으로 Access-Control-Allow-Origin: https://example.com 헤더를 보내면, example.com에서 로드된 웹 페이지만이 example.org의 해당 리소스에 접근할 수 있음을 의미합니다. 만약 이 헤더가 Access-Control-Allow-Origin: *라면, 모든 출처에서 로드된 웹 페이지가 리소스에 접근할 수 있습니다.

브라우저의 역할
브라우저는 CORS 정책을 직접 적용합니다. 웹 페이지가 다른 출처의 리소스에 요청을 보낼 때, 브라우저는 먼저 응답에서 Access-Control-Allow-Origin 헤더를 확인합니다. 만약 이 헤더가 요청을 보낸 출처를 명시적으로 허용하거나 모든 출처를 허용하는 경우, 브라우저는 응답을 웹 페이지에 전달하고, 리소스를 사용할 수 있게 됩니다. 반면, 헤더가 요청 출처를 허용하지 않거나 누락된 경우, 브라우저는 CORS 정책 위반으로 인해 요청을 차단하고, 웹 페이지는 리소스에 접근할 수 없습니다. 이러한 방식으로, CORS는 웹 애플리케이션의 보안을 유지하면서도 필요에 따라 다른 출처의 리소스를 유연하게 활용할 수 있도록 합니다.

 

Spring Security에서는 기본적으로 이러한 헤더들이 응답에 추가되지 않습니다. 그러므로 처음부터 시작해봅시다: 

애플리케이션에서 CORS를 구성하지 않았을 때 교차 출처 호출을 할 때 무슨 일이 발생하는지 알아봅시다. 애플리케이션이 요청을 할 때, 서버가 허용하는 출처를 포함한 Access-Control-Allow-Origin 헤더가 응답에 있기를 기대합니다. 이것이 발생하지 않는다면, 기본적인 Spring Security 동작의 경우처럼, 브라우저는 응답을 수락하지 않을 것입니다. 이를 작은 웹 애플리케이션을 통해 시연해보겠습니다. 다음 코드 스니펫에 제시된 의존성을 사용하여 새 프로젝트를 생성합니다. 이 예제는 프로젝트 ssia-ch10-ex1에서 찾을 수 있습니다.

 

Main 페이지와 REST 엔드포인트에 대한 액션을 가진 컨트롤러 클래스를 정의합니다. 이 클래스가 일반적인 Spring MVC @Controller 클래스이기 때문에, REST 엔드포인트에 @ResponseBody 어노테이션을 명시적으로 추가해야 합니다. 다음 리스팅은 컨트롤러를 정의합니다.

 

Listing 10.1 The definition of the controller class

@Controller
public class MainController {

    private Logger logger =
            Logger.getLogger(MainController.class.getName());

    @GetMapping("/")
    public String main() {
        return "main.html";
    }

    @PostMapping("/test")
    @ResponseBody
//    @CrossOrigin("http://localhost:8080")
    public String test() {							#A
        logger.info("Test method called");
        return "HELLO";
    }
}

#A 다른 출처에서 호출하여 CORS가 어떻게 작동하는지 증명하는 엔드포인트를 정의합니다

 

또한, 예제를 단순화하고 CORS 메커니즘에만 집중할 수 있도록 CSRF 보호를 비활성화하는 구성 클래스를 정의해야 합니다. 또한, 모든 엔드포인트에 대한 인증되지 않은 접근을 허용합니다. 다음 목록은 이 구성 클래스를 정의합니다.

 

Listing 10.2 The definition of the configuration class

@Configuration
public class ProjectConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(
            c -> c.disable()
        );

        http.authorizeHttpRequests(
            c -> c.anyRequest().permitAll()
        );

        return http.build();
    }
}

 

물론, 프로젝트의 resources/templates 폴더 안에 main.html 파일도 정의해야 합니다. main.html 파일은 /test 엔드포인트를 호출하는 JavaScript 코드를 포함합니다. 교차 출처 호출을 시뮬레이션하기 위해, 브라우저에서 도메인 localhost를 사용하여 페이지에 접근할 수 있습니다. JavaScript 코드에서는 IP 주소 127.0.0.1을 사용하여 호출을 합니다. localhost와 127.0.0.1이 같은 호스트를 가리키더라도, 브라우저는 이를 다른 문자열로 보고 서로 다른 도메인으로 간주합니다. 다음 리스팅은 main.html 페이지를 정의합니다.

 

Listing 10.3 The main.html page

<!DOCTYPE HTML>
<html lang="en">
    <head>
        <script>
            const http = new XMLHttpRequest();
            const url='http://127.0.0.1:8080/test';				#A
            http.open("POST", url);
            http.send();

            http.onreadystatechange = (e) => {
                document.getElementById("output")				#B
                    .innerHTML =
                    http.responseText;
            }
        </script>
    </head>
    <body>
        <div id="output"></div>
    </body>
</html>

#A Calls the endpoint using 127.0.0.1 as host to simulate the cross-origin call

#B Sets the response body to the output div in the page body

 

애플리케이션을 시작하고 브라우저에서 localhost:8080으로 페이지를 열면, 페이지에 아무 것도 표시되지 않는 것을 관찰할 수 있습니다. /test 엔드포인트가 반환하는 것이 HELLO이기 때문에, 페이지에 HELLO가 보일 것으로 예상했습니다. 브라우저 콘솔을 확인했을 때, JavaScript 호출에 의해 출력된 오류를 볼 수 있습니다. 오류는 다음과 같습니다:

Access to XMLHttpRequest at 'http://127.0.0.1:8080/test' from origin 
'http://localhost:8080' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

 

오류 메시지는 Access-Control-Allow-Origin HTTP 헤더가 존재하지 않기 때문에 응답이 수락되지 않았다고 알려줍니다. 이러한 동작은 우리가 Spring Boot 애플리케이션에서 CORS에 관해 어떠한 설정도 하지 않았고, 기본적으로 CORS와 관련된 어떠한 헤더도 설정하지 않기 때문에 발생합니다. 그래서 브라우저가 응답을 표시하지 않는 행동은 올바릅니다. 하지만, 애플리케이션 콘솔에서 메소드가 호출되었다는 것을 증명하는 로그에 주목해 주시기 바랍니다. 다음 코드 스니펫은 애플리케이션 콘솔에서 찾을 수 있는 내용을 보여줍니다:

INFO 25020 --- [nio-8080-exec-2] c.l.s.controllers.MainController : Test method called

 

이 측면은 중요합니다! CORS를 권한 부여나 CSRF 보호와 유사한 제한으로 이해하는 개발자들을 많이 만났습니다. 제한이라기보다, CORS는 도메인 간 호출에 대한 엄격한 제약을 완화하는 데 도움을 줍니다. 그리고 제한이 적용되더라도, 어떤 상황에서는 특정 엔드포인트가 호출될 수 있습니다. 이러한 동작은 항상 발생하는 것은 아닙니다. 때때로, 브라우저는 HTTP OPTIONS 메소드를 사용하여 요청이 허용되는지를 테스트하는 호출을 먼저 합니다. 이 테스트 요청을 사전 요청(preflight request)이라고 합니다. 사전 요청이 실패하면, 브라우저는 원래 요청을 시도하지 않을 것입니다.

사전 요청을 하고 그것을 결정하는 것은 브라우저의 책임입니다. 이 로직을 구현할 필요는 없습니다. 하지만 이를 이해하는 것은 중요합니다, 그래서 특정 도메인에 대한 CORS 정책을 지정하지 않았더라도 백엔드로의 출처 간 호출을 보게 될 때 놀라지 않게 됩니다. 이런 상황은 Angular나 ReactJS와 같은 프레임워크로 개발된 클라이언트 사이드 앱이 있을 때도 발생할 수 있습니다. 그림 10.4는 이 요청 흐름을 보여줍니다.

브라우저가 HTTP 메소드가 GET, POST, 또는 OPTIONS일 때 사전 요청(preflight request)을 생략하고, 공식 문서(https://fetch.spec.whatwg.org/#http-cors-protocol)에 설명된 대로 몇 가지 기본 헤더만 포함하는 경우가 있습니다.

우리 예제에서, 브라우저는 요청을 하지만 그림 10.1과 10.2에서 보여주듯이, 응답에 출처가 지정되지 않은 경우 응답을 수락하지 않습니다. 결국, CORS 메커니즘은 브라우저와 관련이 있으며 엔드포인트를 보호하는 방법이 아닙니다. 이것이 보장하는 유일한 것은 여러분이 허용하는 origin 도메인만이 브라우저의 특정 페이지에서 요청을 할 수 있다는 것입니다.

 

10.2 Applying CORS policies with the @CrossOrigin annotation

이 섹션에서는 @CrossOrigin 어노테이션을 사용하여 다른 도메인에서의 요청을 허용하는 방법에 대해 논의합니다. @CrossOrigin 어노테이션을 엔드포인트를 정의하는 메소드 바로 위에 직접 배치하고, 허용되는 출처와 메소드를 사용하여 구성할 수 있습니다. 이 섹션에서 배우게 될 것처럼, @CrossOrigin 어노테이션을 사용하는 장점은 각 엔드포인트에 대한 CORS를 쉽게 구성할 수 있다는 것입니다.

 

그림 10.4 단순 요청의 경우, 브라우저는 original 요청을 직접 서버로 보냅니다. 서버가 original를 허용하지 않으면 브라우저는 응답을 거부합니다. 일부 경우에는, 브라우저가 서버가 original를 수락하는지 테스트하기 위해 사전 요청을 보냅니다. 사전 요청이 성공하면, 브라우저는 원래 original을 보냅니다.

Needs a preflight request?

 

웹 브라우저가 다른 출처(예: http://example.api.com)의 도메인의 루트 경로에 대해 GET 메소드 요청을 보낼 때 해당 도메인 웹 서버가 Access-Control-Allow-Origin 헤더를 포함하여 응답을 보내는 시점은 다음과 같습니다:

1. 요청 수신 시: 서버(http://example.api.com)는 웹 브라우저로부터 GET 요청을 받습니다. 이 요청은 다른 출처(예: http://example-frontend.com)에서 발생했을 수 있습니다.

 

2. 응답 생성 시: 서버는 이 요청에 대한 응답을 생성합니다. 이 때, 서버의 CORS 정책에 따라 Access-Control-Allow-Origin 헤더를 응답에 포함시킬지 결정합니다. 서버가 모든 출처에서의 요청을 허용하도록 설정되어 있거나, 요청을 보낸 특정 출처를 허용 목록에 추가한 경우, 해당 헤더가 응답에 포함됩니다.

3. 응답 전송 시: 서버는 Access-Control-Allow-Origin 헤더가 포함된 응답을 웹 브라우저로 전송합니다. 이 헤더는 요청을 보낸 출처가 서버에 의해 허용되었는지를 브라우저에 알리는 역할을 합니다.

  • 예를 들어, 서버가 Access-Control-Allow-Origin:  * 를 응답에 포함시키면, 이는 모든 출처에서 온 요청을 허용함을 의미합니다.
  • 만약 서버가 Access-Control-Allow-Origin: http://example-frontend.com을 설정하면, 오직 http://example-frontend.com에서 온 요청만 허용한다는 것을 의미합니다.

웹 브라우저는 서버로부터 받은 응답을 검토하고, Access-Control-Allow-Origin 헤더에 기반해 요청을 보낸 출처가 허용되었는지 확인합니다. 만약 허용되었다면, 요청에 대한 응답(데이터)을 페이지에 로드하고, 그렇지 않다면 CORS 정책 위반으로 인해 요청이 거부되며, 관련 오류 메시지가 콘솔에 출력됩니다.

따라서, 웹 브라우저가 다른 출처의 도메인에 GET 요청을 보낼 때 서버가 이에 대한 응답에 Access-Control-Allow-Origin 헤더를 설정하여 보내는 것은 서버의 CORS 정책에 의해 결정되며, 이는 교차 출처 리소스 공유를 허용하거나 제한하는 중요한 메커니즘입니다.

 


CORS Non-Simple Request

CORS Non-/Simple Request 테스트를 위한 코드는 다음과 같습니다.

Spring Security 코드 snippet

		http.cors(
                c -> {     // statement....
                    CorsConfigurationSource source = request -> {
                        CorsConfiguration configuration = new CorsConfiguration();
                        configuration.setAllowedOrigins(List.of("http://localhost:8080"));
                        configuration.setAllowedMethods(List.of("*"));
                        configuration.setAllowedHeaders(List.of("*"));
                        return  configuration;
                    };
                    c.configurationSource(source);
                });

 

Controller 클래스의 핸들러 메서드 코드 snippet

	@PostMapping("/test")
    @ResponseBody
//    @CrossOrigin("http://localhost:8080")
    public String postTest() {
        log.info("postTest method called");
        return "POST Method"; // response body 부분에 payload될 텍스트, 이 때는 템플릿 파일 이름이 아님? 왜 @ResponseBody....
    }
    
    @PutMapping("/test")
    @ResponseBody
//  @CrossOrigin("http://localhost:8080")
    public String putTest() {
    	log.info("putTest method called");
    	return "PUT Method"; // response body 부분에 payload될 텍스트, 이 때는 템플릿 파일 이름이 아님? 왜 @ResponseBody....
    }

 

웹 페이지는 다음과 같습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <button onclick="sendRequest('POST')">POST Request</button>
    <button onclick="sendRequest('PUT')">PUT Request</button>
    <div id="output"></div>

    <script>
        function sendRequest(method) {
            const http = new XMLHttpRequest();
            const url = 'http://127.0.0.1:8080/test';  // IP 주소 사용
            http.open(method, url);  // 메서드를 동적으로 설정
            http.send();

            http.onreadystatechange = (e) => {
                if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
                    document.getElementById("output").innerHTML = 
                        `${method} 요청을 전송했습니다. 서버 응답: ${http.responseText}`;
                }
            }
        }
    </script>
</body>
</html>

 

이 웹페이지를 localhost:8080에서 호스팅합니다.

local host PC에서 CORS를 테스트하기 위해 웹 브라우저를 속이는 트릭을 웹페이지에 다음과 같이 적용하였습니다.

const url = 'http://127.0.0.1:8080/test';

현재 웹 브라우저의 origin은 localhost입니다만,

 

CORS 테스트를 위해 타 Origin의 도메인을 localhost의 Loopback 127.0.0.1 어드레스를 사용합니다.

그러면, 웹 브라우저는 http://127.0.0.1:8080을 타 Origin이라고 착각해서, 타 Origin에게 보안상 민감한 사항이 될 수 있는 PUT 메서드를 전송하려고 하기 때문에, Non-/Simple Request를 전송합니다.

먼저 아래 그림처럼 PUT Request 버튼을 선택해서 타 Origin에게 PUT 메서드의 Http Request를 전송하려 합니다.

그러면 웹 브라우저는 먼저 Option 메서드의 Non-Simple Request를 먼저 전송해, 타 도에인의 CORS 정책을 확인합니다.

Non-Simple Request를 PreFlight Request라고도 합니다.

 

타 도메인의 CORS 정책이 타 도메인의 요청을 허용하는 것을 확인[위 이미지의 빨간색 하이라이트 부분]한 웹 브라우저는 그제서야 실제 원하는 URL 자원을 얻기 위해 해당 Http Request[PUT Method]를 타 도메인에게 전송합니다.

 

 

 

CORS Simple Request

이번에는 POST Request 버튼을 선택해서, 웹 브라우저에서 Simple Request를 보내는 경우를 확인해 보겠습니다.

 

타 도메인에게 전송하고자한 Http 리퀘스트의 메서드가 보안상 민감하지 않은 POST 메서드이므로, PreFlight Option 메서드의 리퀘스트를 전송하지 않고 위 그림처럼 POST 메서드를 바로 전송합니다. 이는 위 헤더들[빨간색 하이라이트 부분]을 상태와 응답 상태 코드로 Http Request/Response 성공여부를 판단합니다.


 

10.1 절에서 만든 애플리케이션을 사용하여 @CrossOrigin이 어떻게 작동하는지 시연합니다. 애플리케이션에서 교차 출처 호출을 작동시키기 위해 필요한 것은 컨트롤러 클래스의 test() 메소드 위에 @CrossOrigin 어노테이션을 추가하는 것뿐입니다. 다음 목록은 localhost를 허용된 출처로 만들기 위해 어노테이션을 사용하는 방법을 보여줍니다.

 

Listing 10.4 Making localhost an allowed origin

@PostMapping("/test")
@ResponseBody
@CrossOrigin("http://localhost:8080") #A
public String test() {
     logger.info("Test method called");
     return "HELLO";
}

#A Allows the localhost origin for cross-origin requests

 

애플리케이션을 다시 실행하고 테스트할 수 있습니다. 이제 페이지에 /test 엔드포인트에서 반환된 문자열인 HELLO가 표시되어야 합니다.

 

@CrossOrigin의 value 속성은 다중 origin를 정의할 수 있도록 배열을 받습니다; 예를 들어, @CrossOrigin({"example.com", "example.org"})와 같이 사용할 수 있습니다. 또한, allowedHeaders 속성과 methods 속성을 사용하여 허용되는 헤더와 Http Method를 설정할 수도 있습니다. origin와 헤더 모두에 대해, 모든 헤더나 모든 origin를 나타내기 위해 별표(*)를 사용할 수 있습니다. 하지만 이 방법을 사용할 때는 주의를 기울일 것을 권장합니다. 항상 허용하고자 하는 origin와 헤더를 필터링하고, 어떤 도메인도 여러분의 애플리케이션의 리소스에 접근하는 코드를 구현하도록 허용하지 않는 것이 더 좋습니다.

모든 origin를 허용함으로써, 애플리케이션을 교차 사이트 스크립팅(XSS) 요청에 노출시키게 되며, 결국 DDoS 공격으로 이어질 수 있습니다. 개인적으로는 테스트 환경에서조차 모든 출처를 허용하는 것을 피합니다. 때때로 애플리케이션이 테스트와 프로덕션 모두 같은 데이터 센터를 사용하는 잘못 정의된 인프라에서 실행되는 경우가 있습니다. 우리가 1장에서 논의한 바와 같이, 보안이 적용되는 모든 레이어를 독립적으로 처리하고 인프라가 허용하지 않는다고 해서 애플리케이션이 특정 취약점을 가지고 있지 않다고 가정하는 것을 피하는 것이 현명합니다.

엔드포인트가 정의된 곳에서 직접 규칙을 지정하기 위해 @CrossOrigin을 사용하는 장점은 규칙의 좋은 투명성을 생성한다는 것입니다. 단점은 많은 코드를 반복하게 되어 장황해질 수 있으며, 새로 구현된 엔드포인트에 어노테이션을 추가하는 것을 개발자가 잊어버릴 위험도 있습니다. 10.3 절에서는 구성 클래스 내에서 CORS 구성을 중앙집중식으로 적용하는 방법에 대해 논의합니다.

 

10.3 Applying CORS using a CorsConfigurer

10.2절에서 배웠듯이 @CrossOrigin 어노테이션을 사용하는 것은 쉽지만, 많은 경우에 한 곳에서 CORS 구성을 정의하는 것이 더 편리할 수 있습니다. 이 절에서는 10.1절과 10.2절에서 작업한 예제를 변경하여 구성 클래스에서 Customizer를 사용하여 CORS 구성을 적용합니다. 다음 목록에서는 허용하고자 하는 출처를 정의하기 위해 구성 클래스에서 필요한 변경 사항을 찾을 수 있습니다.

 

Listing 10.5 Defining CORS configurations centralized in the configuration class

@Configuration
public class ProjectConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.cors(c -> {								#A
            CorsConfigurationSource source = request -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(List.of("*"));
                config.setAllowedMethods(List.of("*"));
                config.setAllowedHeaders(List.of("*"));
                return config;
            };
            c.configurationSource(source);
        });

        http.csrf(
            c -> c.disable()
        );

        http.authorizeHttpRequests(
            c -> c.anyRequest().permitAll()
        );

        return http.build();
    }
}

#A CORS 구성을 정의하기 위해 cors()를 호출합니다. 그 안에서, 허용된 출처와 메소드를 설정하기 위해  CorsConfiguration 객체를 생성합니다.

 

HttpSecurity 객체에서 호출하는 cors() 메소드는 파라미터로 Customizer<CorsConfigurer> 객체를 받습니다. 이 객체에 대해, HTTP 요청에 대한 CorsConfiguration을 반환하는 CorsConfigurationSource를 설정합니다. CorsConfiguration은 허용된 Origin, 메소드, 헤더가 무엇인지를 명시하는 객체입니다. 이 접근 방식을 사용한다면, 적어도 origin와 메소드는 지정해야 합니다. origin만 지정하면, 애플리케이션은 요청을 허용하지 않습니다. 이러한 행동은 CorsConfiguration 객체가 기본적으로 어떤 메소드도 정의하지 않기 때문에 발생합니다.

이 예제에서, 설명을 간단하게 하기 위해 SecurityFilterChain 빈을 직접 사용하여 람다 표현식으로 CorsConfigurationSource의 구현을 제공합니다. 실제 애플리케이션에서는 이 코드를 다른 클래스에 분리할 것을 강력히 권장합니다. 실제 애플리케이션에서는 코드가 훨씬 길어질 수 있으므로, 구성 클래스와 분리하지 않으면 읽기 어려워집니다.

Summary

  • CORS refers to the situation in which a web application hosted on a specific domain tries to access content from another domain. 
  • By default, the browser doesn’t allow cross-origin requests to happen. CORS configuration thus enables you to allow a part of your resources to be called from a different domain in a web application run in the browser. 
  • You can configure CORS both for an endpoint using the @CrossOrigin annotation or centralized in the configuration class using the cors() method of the HttpSecurity object.