본문 바로가기
코딩 공부/web & Java

[Spring / Web] CORS

by 현장 2023. 12. 1.

CORS(Cross-Origin-Resource Sharing)

출처가 다른 자원들을 공유한다는 뜻으로, 한 출처에 있는 자원에서 다른 출처에 있는 자원에 접근하도록 하는 개념입니다. 직역하면, 교차되는 출처 자원들의 공유입니다. 다른 출처에 있는 자원을 요청한다고 하면, 이를 교차 출처 요청이라고 부릅니다.

🏷️ 출처(Origin)

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

위의 구성요소 중에서 Protocol + Host + Port 3가지가 같으면 동일 출처(Origin)라고 합니다.  브라우저 개발자 도구의 콘솔 창에 location.origin를 실행하면 출처를 확인할 수 있다.

✅ 동일 출처 예시

URL 설명
http://example.com:80
http://example.com
HTTP 기본 Port인 80번이 생략되어있으므로 동일 출처입니다.
http://example.com/app1/index.html
http://example.com/app2/index.html
Protocol, Host, Port(생략)이 같으며, Path부터 다르므로 동일 출처입니다.

✅ 다른 출처 예시

URL 설명
http://example.com/app1
https://example.com/app2
Protocol이 다릅니다
http://example.com
http://www.example.com
http://myapp.example.com
Host가 다릅니다
http://example.com
http://example.com:8080
80, 8080으로 포트가 다릅니다

✅ 동일 출처 요청 vs 다른 출처 요청

https://escapefromcoding.tistory.com/724

왼쪽의 핸드폰의 URL은 domain-a.com 입니다. 오른쪽 서버의 URL은 domain-a.com과 domain-b.com 2가지 입니다.


domain-a.com 유저가 domain-a.com 서버에 요청하면 동일 정책이기 때문에 아무런 문제가 없지만, domain-a.com 유저가 domain-b.com 서버에 요청하면 호스트가 다르기 때문에 다른 출처 요청을 합니다

동일 출처 정책(Same-origin policy)

동일 출처 정책(Same-origin policy)는 다른 출처로부터 조회된 자원들의 읽기 접근을 막아 다른 출처 공격을 예방합니다. 그러나,  다른 출처에서 얻은 이미지를 담는 <img>, 외부 주소를 담는 <link> 같은 여러 태그들을 허용합니다. 동일 출처 정책의 정확한 구현 명세는 없지만 최신의 브라우저들은 일정 규칙을 따르고 있습니다. 

https://escapefromcoding.tistory.com/724

동일출처 정책은 다른 출처 자원을 가져오는 것을 굉장히 제한적으로 허용했으며 SPA와 미디어 중심 웹 사이트들이 더욱 늘어나고 있으므로 관련 규칙들도 계속 늘어납니다. 따라서, 다른 출처 리소스에 접근성을 높이기 위해서 CORS가 등장했습니다.

✔️ 다른 출처 요청의 위험성

<img>, <frame> 등이 웹에 등장하면서, 페이지 로딩 이후에 브라우저에서 이러한 하위 자원들을 가져올 수 있게 되어서 동일 출처, 다른 출처 모두 호출이 가능하게 되었습니다.

 

CORS 정책이 없고 모든 다른 출처 요청이 가능한 브라우저를 생각해봅시다.

https://escapefromcoding.tistory.com/724

위와같이 홈페이지를 서핑하고 있는데, <script>가 심어진 evil.com 페이지를 열게되면 <script>가 실행되어 은행에 'Delete /account'를 요청하도록 되어 있습니다. 이로 인해서 AJAX 호출로 은행 API를 호출하여 나의 은행 계좌를 삭제해버리는 사고가 발생합니다. 

 

따라서, 다른 출처의 접근을 막기 위해서 동일 출처 정책이 등장했습니다.

🏷️ 다른 출처 요청 정책

1. 단순요청(Simple Request)

https://escapefromcoding.tistory.com/724

단순 요청은 서버에 API를 요청하고, 서버는 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에 보냅니다. 이때 브라우저는 Access-Control-Allow-Origin 헤더를 확인해서 CORS 동작을 수행할지 판단합니다.

 

✔️ 단순요청(Simple Request) 조건

▪️ GET, HEAD, POST 요청만 가능합니다

▪️ Accept, Accept-Language, Contet-Language, Content-Type과 같은 CORS 안전 리스트 헤더 혹은 User-Agent 헤더가능합니다.

▪️ Contet-Type 헤더는 application/x-www-form-urlencoded, multipart/form-data and text/plain만 가능합니다.

▪️ XMLHttpRequest 객체를 사용하여 요청하면, 요청에서 사용된 XMLHttpRequest.upload에 의해 반환되는 객체에 어떠한 이벤트 리스너도 등록되지 않습니다.

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

프리플라이트(Preflight) 방식은 서버에 예비 요청을 보내서 안전한지 판단한 후 본 요청을 보내는 방법입니다. 브라우저는 요청을 한번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송합다.

Preflight 
브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 라고 부르는 것이라고 합니다. 이 예비 요청에는 HTTP 메소드 중 OPTIONS  메소드가 사용됩니다. 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것입니다.

 

요청 헤더
- origin : 어디서 요청을 했는지 서버에 알려주는 주소
- access-control-request-method : 실제 요청이 보낼 HTTP 메서드
- access-control-request-headers : 실제 요청에 포함된 header

응답 헤더
- access-control-allow-origin : 서버가 허용하는 출처
- access-control-allow-methods : 서버가 허용하는 HTTP 메서드 리스트
- access-control-allow-headers : 서버가 허용하는 header 리스트
- access-control-max-age : 프리 플라이트 요청의 응답을 캐시에 저장하는 시간

https://escapefromcoding.tistory.com/724

플리플라이트 요청은 OPTIONS 를 사용해 자신의 주소, origin, access-control-request-method, access-control-request-headers를 같이 보냅니다.

 

정상적인 응답으로 access-control-allow-origin, access-control-allow-method, access-control-allow-headers, access-control-max-age를 응답받습니다.

 

정상 요청과 응답이 가능하다는 프리 플라이트 덕분에 실제 요청을 합니다. 그리고 정상적인 응답을 받습니다.

3. 신용 요청(Credentialed Request)

https://escapefromcoding.tistory.com/724

신용 요청은 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방법으로 쿠키, 인증 헤더, TLS 클라이언트 인증서 등의 신용정보와 함께 요청합니다. 기본적으로, CORS 정책은 다른 출처 요청에 인증정보 포함을 허용하지 않습니다. 하지만 요청에 인증을 포함하는 플래그가 있거나 access-control-allow-credentials가 true로 설정 한다면 요청할 수 있습니다. 


🏷️ Spring Boot에서 cross-origin 설정

1. 메서드 설정

@RestController
@RequestMapping("/account")
public class AccountController {
	
	// 1. 모든 출처가 허용됩니다.
	// 2. 허용된 HTTP 메서드는 @RequestMapping에 선언된 메서드들입니다.
	// 3. 프리플라이트 응답은 30분 동안 캐시됩니다.
    @CrossOrigin
    @RequestMapping(method = RequestMethod.GET, path = "/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

2. 컨트롤러 설정

@CrossOrigin(origins = "http://example.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @RequestMapping(method = RequestMethod.GET, path = "/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

컨트롤러에 설정했으므로 AccountController에 있는 retrieve() 와 remove() 함수 모두에 적용됩니다. 

3. 개별 설정

@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("http://example.com")
    @RequestMapping(method = RequestMethod.GET, "/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

스프링에서 여러가지 CORS 정책을 복합해서 설정 할 수 있습니다. 모든 메서서드들은 3600초가 캐시 시간입니다. retrieve() 메서드는 허용 출처가 "http://example.com" 밖에 안됩니다. 하지만, remove() 메서드는 별도의 설정이 없으므로 모든 출처가 가능합니다.

4. 전역 설정

@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**") // 전체
                    .allowedMethods("*") // 모든 메서드
                    .allowedOrigins("http://localhost:3000"); // 허용할 주소
        }
    };
}

📖 Reference

코동이

hoo00nn

'코딩 공부 > web & Java' 카테고리의 다른 글

[Web] JWT  (1) 2023.12.08
[Spring/Web] CSRF  (1) 2023.12.04
[WEB] MPA와 SPA  (0) 2023.11.25
[JPA] @Entity는 기본 생성자를 왜 가져야 하는가?  (0) 2023.11.22
[Spring] Response Customizing  (1) 2023.11.20