CORS
질문 : CORS(Cross-Origin Resource Sharing) 란?
답변 : 간단하게 얘기하자면 CORS는 브라우저에서 cross-origin 요청을 안전하게 할 수 있도록 하는 메커니즘입니다.
브라우저에서는 보안적인 이유로 cross-origin HTTP 요청들을 제한합니다. 그래서 cross-origin 요청을 하려면 서버의 동의가 필요합니다. 만약 서버가 동의한다면 브라우저에서는 요청을 허락하고, 동의하지 않는다면 브라우저에서 거절합니다.
이러한 허락을 구하고 거절하는 메커니즘을 HTTP-header를 이용해서 가능한데, 이를 CORS라고 부릅니다.
CORS란
CORS(Cross-Origin Resource Sharing)는 웹 보안 모델인 Same-Origin Policy에 의해 제한되는 다른 출처(Origin)에서 리소스에 접근할 수 있도록 하는 메커니즘입니다. 즉, 쉽게 말해서 다른 출처에서 자원을 사용하겠다고 요청이 들어올 때 허용된 출처에게만 자원을 사용할 수 있도록 허용해 주는 것이라고 생각하면 됩니다.
* Same-Origin Policy란?
Same-Origin Policy 란, 한 출처에서 로드된 문서나 스크립트가 다른 출처(origin)의 리소스에 접근하지 못하도록 제한하는 보안 모델로서 웹 보안에 있어서 중요한 역할을 합니다. 여기서 출처(origin)는 프로토콜, 도메인, 포트 번호로 정의됩니다.
예를 들면, http://example.com에서 로드된 문서는 http://example.com의 리소스에만 접근할 수 있습니다.
Same-Origin Policy는 보안에 매우 중요하지만, 때로는 다른 출처에서 제공하는 리소스에 접근해야 하는 상황이 발생합니다. 예를 들면, 외부 서버에서 제공하는 API를 호출하거나, 다른 도메인에 있는 이미지나 폰트 파일을 사용해야 할 때가 있습니다.
* cross-origin이란?
cross-origin이란 다음 중 한 가지라도 다른 경우를 말합니다.
- 프로토콜 - http와 https는 프로토콜이 다르다.
- 도메인 - domain.com과 other-domain.com은 다르다.
- 포트 번호 - 8080포트와 3000포트는 다르다.
CORS는 왜 사용할까?
만약 CORS가 없이 모든 곳에서 데이터를 요청할 수 있게 되면, 다른 사이트에서 원래 사이트를 흉내낼 수 있게 되고 보안상의 문제가 생기게 됩니다. 그래서 웹 브라우저에서는 Same-Origin Policy를 사용하여 서로 다른 도메인에서 리소스를 요청하거나 전송하는 경우 해당 요청을 차단하게 됩니다. 이러한 문제를 해결하기 위해, CORS는 서버 측에서 추가적인 헤더를 설정하여, 클라이언트가 서버에서 허용된 다른 출처의 리소스에 접근할 수 있도록 허용하는 방식입니다.
CORS의 동작 방식
Simple requests인 경우
(브라우저가 사전 검사(Preflight Request)를 보내지 않고, 바로 실제 요청을 서버에 전송합니다.)
- 클라이언트에서 다른 출처의 리소스에 접근하려고 시도합니다.
- 서버는 HTTP 응답 헤더에 Access-Control-Allow-Origin이라는 헤더를 포함하여, 허용된 출처의 리스트를 클라이언트에게 전송합니다.
- 클라이언트는 서버의 응답이 허용된 출처인지를 확인하고, 허용된 출처일 경우에만 해당 리소스에 접근할 수 있습니다.
- 만약 서버에서 Access-Control-Allow-Origin 헤더를 전송하지 않으면, 클라이언트는 Same-Origin Policy로 인해 해당 리소스에 접근할 수 없습니다.
* Simple requests란?
CORS에서 Simple requests란, 브라우저에서 허용하는 간단한 요청에 대한 CORS 정책입니다.
간단한 요청(Simple requests)은 브라우저에서 HTTP 메소드 중 GET, HEAD, POST 중 하나를 사용하고, 요청 헤더 중 일부에 대해서만 허용하는 경우입니다.
Origin과 Resource URL, HTTP 메소드, 요청 헤더 등을 포함한 간단한 요청(Simple requests)의 경우 브라우저는 사전 요청(Preflight Request)을 보내지 않고, 바로 요청을 보내게 됩니다.
Preflight Request인 경우
(브라우저는 사전요청(Preflight Request)를 보내서 서버가 실제 요청을 받아들일 수 있는지 확인합니다.)
- 클라이언트에서 다른 출처의 리소스에 접근하려고 시도합니다.
- 브라우저는 실제 요청을 보내기 전에 사전요청(Preflight Request)을 보냅니다.
- 이때, 요청 메서드는 OPTIONS입니다.
- Access-Control-Request-Method, Access-Control-Request-Headers 등의 헤더를 포함하여 보냅니다.
- 서버는 사전요청에 대한 응답으로 HTTP 응답 헤더에 Access-Control-Allow-Origin이라는 헤더를 포함하여, 허용된 출처의 리스트를 클라이언트에게 전송합니다.
- 클라이언트는 서버의 응답이 허용된 출처인지를 확인하고, 허용된 출처일 경우에만 해당 리소스에 접근할 수 있습니다.
- 브라우저는 실제 요청을 보내기 전에, 서버가 사전요청에 대해 허용한 메서드와 헤더를 확인하고, 허용되지 않은 경우에는 실제 요청을 보내지 않습니다.
- 만약 서버에서 Access-Control-Allow-Origin 헤더를 전송하지 않거나, 허용된 출처가 아닌 경우, 클라이언트는 Same-Origin Policy로 인해 해당 리소스에 접근할 수 없습니다.
* Preflight Request란?
CORS에서 Preflight Request는 브라우저가 본 요청을 보내기 전에 사전 요청을 보내는 것을 말합니다.
사전요청(Preflight Request)은 브라우저에서 보내는 HTTP OPTIONS 메소드를 이용하여 요청을 보내며, 본 요청에서 사용할 수 있는 메소드, 헤더, 크로스 도메인 요청에 필요한 인증 토큰 등의 정보를 서버에게 미리 확인하는 것입니다.
서버는 사전 요청(Preflight Request)을 받아들여, 요청이 안전한지 확인한 후, 클라이언트에게 본 요청을 보내도록 허용 또는 거부할 수 있습니다. 이를 통해 서버는 보안상의 이유로 허용되지 않은 요청을 블록하거나, CORS를 제대로 적용하도록 보장할 수 있습니다.
요청 헤더 목록
- Origin
- Access-Control-Request-Method
- preflight 요청을 할 때 실제 요청에서 어떤 메서드를 사용할 것인지 서버에게 알리기 위해 사용됩니다.
- Access-Control-Request-Headers
- preflight요청을 할 때 실제 요청에서 어떤 header를 사용할 것인지 서버에게 알리기 위해 사용됩니다.
응답 헤더 목록
- Access-Control-Allow-Origin
- 브라우저가 해당 origin이 자원에 접근할 수 있도록 허용합니다. 혹은 *은 credentials이 없는 요청에 한해서 모든 origin에서 접근이 가능하도록 허용합니다.
- Access-Control-Expose-Headers
- 브라우저가 액세스할 수 있는 서버 화이트리스트 헤더를 허용합니다.
- Access-Control-Max-Age
- 얼마나 오랫동안 preflight요청이 캐싱 될 수 있는지를 나타낸다.
- Access-Control-Allow-Credentials
- Credentials가 true 일 때 요청에 대한 응답이 노출될 수 있는지를 나타냅니다.
- preflight요청에 대한 응답의 일부로 사용되는 경우 실제 자격 증명을 사용하여 실제 요청을 수행할 수 있는지를 나타냅니다.
- 간단한 GET 요청은 preflight되지 않으므로 자격 증명이 있는 리소스를 요청하면 헤더가 리소스와 함께 반환되지 않으면 브라우저에서 응답을 무시하고 웹 콘텐츠로 반환하지 않습니다.
- Access-Control-Allow-Methods
- preflight`요청에 대한 대한 응답으로 허용되는 메서드들을 나타냅니다.
- Access-Control-Allow-Headers
- preflight요청에 대한 대한 응답으로 실제 요청 시 사용할 수 있는 HTTP 헤더를 나타냅니다.
CORS 해결 방법
CORS를 해결하는 방법은 (1)서버에서 CORS 헤더를 설정하는 방법과 (2)프록시 서버를 사용하는 방법 으로 크게 두가지 방법이 있습니다.
서버에서 CORS 헤더를 설정하는 방법은 간단하고 구현이 쉬우며, 클라이언트 측에서 추가적인 설정이 필요하지 않습니다. 그러나 프록시 서버를 사용하는 방법은 클라이언트와 서버 간에 추가적인 레이어가 필요하며, 구현이 복잡해질 수 있습니다.
서버에서 CORS 헤더를 설정하는 방법
서버에서 CORS 헤더를 설정하여 허용된 출처를 지정할 수 있습니다. 이를 통해 클라이언트에서 다른 출처의 리소스에 접근할 수 있게 됩니다. 예를 들어, Java Spring Framework에서는 CorsFilter를 사용하여 CORS 헤더를 설정할 수 있습니다.
@Configuration //@Configuration 어노테이션을 사용하여 Spring에서 이 클래스가 설정 파일임을 알리고
@EnableWebMvc //@EnableWebMvc 어노테이션을 사용하여 Spring Web MVC를 활성화시킵니다.
public class CorsConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//WebMvcConfigurer 인터페이스를 구현하여 addCorsMappings() 메서드를 오버라이드하여 CORS 설정을 추가합니다.
registry.addMapping("/**") // 모든 요청
.allowedOrigins("*") //모든 출처(*) 허용
.allowedMethods("GET", "POST", "PUT", "DELETE") // 모든 HTTP 메서드(GET, POST, PUT, DELETE)를 허용
.allowedHeaders("*"); //모든 헤더(*)를 허용
}
}
프록시 서버를 사용하는 방법
프록시 서버를 사용하여 클라이언트의 요청을 다른 출처로 전달하여 CORS를 우회할 수 있습니다. 이를 통해 클라이언트는 프록시 서버와 통신하게 되고, 프록시 서버는 다른 출처와의 통신을 대신 처리합니다. 예를 들어, Node.js에서는 http-proxy-middleware 라이브러리를 사용하여 프록시 서버를 구현할 수 있습니다.
public class ProxyController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping(value = "/proxy/{url}", method = RequestMethod.GET)
//@RequestMapping 어노테이션을 사용하여 /proxy/{url} URL에 대한 GET 요청을 처리하며, {url}은 프록시 서버로 전달할 URL입니다.
//RestTemplate을 사용하여 HTTP 요청을 보내고, 응답을 받습니다.
public ResponseEntity<String> proxy(@PathVariable String url) {
HttpHeaders headers = new HttpHeaders();
headers.set("Access-Control-Allow-Origin", "*");
//프록시 서버는 Access-Control-Allow-Origin 헤더를 설정하여 모든 출처(*)에서 접근 가능하도록 합니다.
HttpEntity<String> entity = new HttpEntity<>("parameters", headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
return response;
}
}
내가 프로젝트에서 사용했던 방법(서버에서 CORS를 처리 했던 방법)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CORS 설정
http.cors().configurationSource(request -> {//CORS(Cross-Origin Resource Sharing)를 구성
var cors = new CorsConfiguration();
cors.setAllowedOriginPatterns(List.of("*"));//모든 출처에서 요청을 허용하도록 구성
cors.setAllowedMethods(List.of("*"));//모든 HTTP 메소드를 허용하도록 설정
cors.setAllowedHeaders(List.of("*"));//모든 헤더를 허용하도록 설정
cors.addExposedHeader("Access_Token");//액세스 토큰을 노출
cors.addExposedHeader("Refresh_Token");//리프레시 토큰을 노출
cors.setAllowCredentials(true);
return cors;
});
http.csrf().disable() //CSRF(Cross-Site Request Forgery) 공격에 대한 보호를 비활성화
.exceptionHandling();//예외처리를 위해 사용
....생략
}
* CSRF 공격이란?
악의적인 사용자가 특정 웹 사이트를 통해 인증된 사용자의 권한을 이용하여 특정 동작(예: 게시글 작성, 글 삭제 등)을 수행하도록 유도하는 공격입니다.
* http.csrf().disable() 를 사용하여 CSRF 보호를 비활성화한 이유
해당 프로젝트에서는 JWT를 사용하였는데 JWT는 서버에서 생성된 토큰을 사용하기 때문에 CSRF 공격에 대한 보호가 이미 내장되어 있습니다.
* CSRF 보호를 비활성화하면 얻을 수 있는 장점
단일 페이지 애플리케이션(SPA)과 같이 동일한 웹 애플리케이션에서 서로 다른 도메인을 사용하는 경우, CSRF 토큰을 사용하기 어려울 수 있기 때문에 CSRF 보호 기능을 비활성화하는 것이 유용합니다. 이를 통해 애플리케이션의 보안을 유지하면서도 불필요한 복잡성과 성능 저하를 방지할 수 있습니다.
결론
CORS는 브라우저에서 cross-origin 요청을 안전하게 할 수 있도록 하는 메커니즘으로서 웹 애플리케이션에서 보안을 유지하는 데 매우 중요한 기술입니다.