CORS (Cross-Origin Resource Sharing)

작성일

CORS란?

CORS는 Cross-Origin Resource Sharing의 줄임말로, 교차 출처 리소스 공유라고 한다. 교차 출처는 쉽게 말해 다른 출처라고 할 수 있다. 즉, CORS는 다른 출처 간의 자원을 공유하는 정책이다.

출처(Origin)

출처(Origin)의 뜻이 무엇일까? 출처를 알기 위해서 URL의 구조를 살펴보자.

1

출처는 Protocal, Host, 포트번호를 의미한다. 이는 서버의 위치를 알기위해 필요한 기본 정보들이다. 즉 Protocal, Host, 포트번호. 이 세가지가 동일하면 같은 출처이고 다르면 다른 출처이다.

여기서 중요한 한가지는 출처를 비교하는 로직이 서버단에서 구현되는 것이 아니라 브라우저단에서 이루어진다는 것이다. 그래서 CORS 정책을 위반하는 리소스를 요청하더라도 서버단에서 같은 출처에서 온 요청만 받겠다는 설정을 따로 해둔것이 아니라면 일단 정상적으로 응답한다. 그 후 브라우저가 이 응답을 분석해서 CORS 위반이라고 생각하면 그 응답을 버린다.

SOP

SOP는 Same-Origin Policy의 줄임말로, 단어 뜻 그대로 같은 출처만 허용한다는 정책을 의미한다. 과거에는 보안을 위해 엄격하게 같은 출처만 통신하도록 허용하였으나, 최근에는 다른 출처에 있는 리소스를 가져와서 사용하는 일이 아주 흔하므로 SOP의 예외 조항인 CORS 정책을 두게 되었다.

CORS 동작원리

기본적으로 웹에서 다른 출처로 리소스를 요청할 때 HTTP 프로토콜을 사용하여 요청을 보내는데 이때 브라우저는 origin이라는 필드에 요청을 보내는 출처를 담아서 보낸다.

Origin: https://yessm621.github.io

이후 서버가 이 요청에 대한 응답을 할 때 응답헤더 Access-Control-Allow-Origin 값에 이 리소스에 접근하는 것이 허용된 출처를 같이 보내주고 응답 받은 브라우저는 자신이 보낸 Origin과 서버가 보내준 Access-Control-Allow-Origin 값을 비교한 후 이 응답이 유효한지 판별한다.

기본적인 흐름은 위와 같고 CORS의 동작원리 방식은 3가지가 있다.

Preflight Request

브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다. 이때 브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 Preflight라고 부른다. 예비 요청의 목적은 본 요청을 보내기 전에 브라우저가 요청을 보내는 것이 안전한지 확인하는 용도이다.

2

Preflight Request는 예비 요청이 포함되었을 뿐이고 앞에서 설명했던 출처 판별 방식과 동일하다.

  1. 브라우저가 보내는 HTTP 프로토콜의 Origin 필드에 리소스를 요청하는 출처를 보낸다.
  2. 브라우저는 서버 응답의 Access-Control-Allow-Origin 필드 값을 보냈던 Origin 값과 비교하여 CORS 정책 위반을 판별한다.

CORS 정책 위반은 예비 요청의 성공 여부와 상관없이 응답 헤더에 유효한 Access-Control-Allow-Origin이 있는지가 중요하다. 그래서 예비 요청이 실패해서 성공 코드가 아니더라도 헤더에 Access-Control-Allow-Origin 값이 제대로 들어가있다면 CORS 정책위반이 아니다.

Simple Request

Simple Request는 예비 요청 없이 바로 서버에 본 요청을 보내는 것이다. Preflight Request에서 예비 요청만 없어지고 로직은 동일하다.

3

하지만 예비 요청을 생략하는 경우는 특정조건을 만족해야 한다. 조건은 다음과 같다.

  • GET, HEAD 요청
  • Content-Type 헤더가 다음과 같은 POST 요청
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.

Credentialed Request

Credentialed Request는 브라우저에서 보안 상의 이유로 요청에 대한 인증 정보를 포함하는 CORS 요청이다. credentials 옵션을 통해 요청에 인증과 관련된 정보를 담을 수 있다.

  • same-origin: default, 같은 출처 간 요청에만 인증 정보를 담을 수 있다.
  • include: 모든 요청에 인증 정보를 담을 수 있다.
  • omit: 모든 요청에 인증 정보를 담지 않는다.

same-origin과 include 옵션을 사용하면 Access-Control-Allow-Origin 값 확인과 추가적인 조건 검사를 더 하게 된다.

스프링 부트에서 CORS 해결 방법

프론트엔드와 백엔드로 나누어 개발할 경우 CORS 이슈는 매우 흔하게 발생한다. 근본적인 해결방법은 백엔드에서 해결하는 것이다.

근본적인 해결 방법: 서버에서 해결

서버에서 Access-Control-Allow-Origin 헤더에 유효한 값을 포함하여 응답을 브라우저로 보내면 CORS 에러를 해결할 수 있다. 프론트 단에서 CORS 에러를 발견했다면 서버에게 Access-Control-Allow-Origin에 유효한 값을 포함해서 달라고 요청해야 한다.

‘Access-Control-Allow-Origin: *’를 사용하면 모든 출처에서 오는 요청을 받겠다는 의미로 편하지만 심각한 보안 이슈가 발생할 수 있다. 따라서 ‘Access-Control-Allow-Origin: 특정주소’와 같이 출처를 명시해주자.

Access-Control-Allow-Origin: *           // 보안 이슈 발생할 수 있음
Access-Control-Allow-Origin: 특정 주소     // 출처를 명시하면 CORS 에러 해결 가능

이제 스프링에서 CORS를 해결하는 방법을 알아보자. 크게 3가지 방법이 있다.

  1. CorsFilter로 직접 response에 header를 넣어주기
  2. Controller에서 @CrossOrigin 어노테이션 추가하기
  3. WebMvcConfigurer를 이용해서 처리하기

CorsFilter 생성

커스텀 필터를 만드는 것이다. 커스텀 필터를 만들고 빈으로 등록하기 위해 @Component 어노테이션을 추가하고 Filter 인터페이스를 구현하자. 참고로 필터는 javax.servlet의 Filter를 사용해야 한다.

필터가 실제로 수행할 doFilter() 메서드를 커스텀한다.

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

    @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", "http://localhost:3000");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods","*");
        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() {...}
}

CrossOrigin 어노테이션 사용

컨트롤러에서 특정 메서드 혹은 컨트롤러 상단부에 @CrossOrigin를 추가하면 된다.

@RestController
@RequestMapping(value = "/api/post", produces = "application/json")
@CrossOrigin(origins = "http://front-server.com") // 컨트롤러에서 설정
public class ThreatController {

    private final ThreatService threatService;

    public ThreatController(ThreatService threatService) {
        this.threatService = threatService;
    }

    @GetMapping
    @CrossOrigin(origins = "http://front-server.com") // 메서드에서 설정
    public ResponseEntity<ThreatLogCountResponse> getAllThreatLogs() {
        return ResponseEntity.ok(threatService.getAllThreatLogCount());
    }
}

WebMvcConfigurer에서 설정

스프링 프로젝트를 생성하면 main 함수가 존재한다. main 함수에서 빈으로 Configurer를 추가해주면 된다.

@SpringBootApplication
public class YouTreeApplication {

    public static void main(String[] args) {
        SpringApplication.run(YouTreeApplication.class, args);
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedOrigins("http://front-server.com");
            }
        };
    }
}

물론 이 방법은 @Configuration을 허용한 클래스에서 등록을 할 수도 있다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://front-server.com")
                .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(MAX_AGE_SECS);
    }
}