본문 바로가기
💊 Java & Kotlin & Spring/- Spring

[Spring Boot] CORS 를 해결하는 3가지 방법 (Filter, @CrossOrigin, WebMvcConfigurer)

by Wonit 2021. 8. 14.

 

Server Side Template 방식이 아닌 Front와 Back 으로 나눠서 인프라를 구성해본 경험이 있는 사람들에게는 Cors가 매우 친숙할 수 있다.

 

 

현재 개발 흐름에서 웹 프로젝트를 진행하다가 Cors 를 만날 확률은 가히 100% 라고 해도 과언이 아니다.

 

오늘은 CORS 가 무엇인지 알아보며 또, Spring Boot Application 에서는 어떻게 해당 정책을 해결할 수 있는지 알아보자.

 

CORS 해결하기

 

우선 가장 먼저 말 하고 싶은 것은

 

CORS 는 에러나 오류가 아니다. Cross-Origin Resource Sharing Policy 이다.

 

처음 내가 Front 와 Back 을 나눠서 개발을 할 때에는 단순히 Front 에서 fetch 요청으로 Back 을 바라보고 날리면 될줄 알았는데, 위에서 보이는 에러가 발생하기에 정말 짜증나고 귀찮은 에러 쯤 으로 생각을 했었다.

 

하지만 CORS 는 귀찮을 뿐 하나의 보안 정책이다.

 

CORS 에 대한 자세한 이야기는 해당 블로그 Cross Origin Resource Sharing 이란? 에서 확인할 수 있습니다.

 

이러한 보안 정책이 발생하는 이유를 간단히 이야기 하자면, SOP 때문이다.

 

SOP 란?

 

SOP 는 Same Origin Policy 로, 동일한 출처의 Origin 만 리소스(데이터)를 공유할 수 있는 것이다.

 

만약 동일한 출처가 아니라면 OPTIONS 를 이용한 Preflight 를 이용해서 여러 검증을 거치게 되는데,

 

OPTIONS와 Preflight 에 대한 자세한 사항은 해당 블로그의 HTTP OPTIONS 헤더와 Preflight 그리고 CORS 에서 확인할 수 있습니다.

 

이 검증이 바로 Cors 인 것이다.

 

CORS 의 간단 플로우는?

 

우리가 Front Back 구조에서 SOP 를 위반하고, 적절한 보안 인증을 받기 위해서 브라우저는 다음과 같은 과정을 거치게 된다.

 

 

  1. GET 요청인지 POST 요청인지 파악한다.
  2. Content-Type 과 Custom HTTP Header 를 파악한다.
  3. OPTIONS 요청을 통해서 서버가 적절한 Access-Control-* 를 가졌는지 확인한다.
  4. 만약 적절한 Access-Control 을 가졌다면 실제 XHR을 트리거한다.
  5. 적절하지 못한 Access-Control 를 가졌다면 Error 를 발생시킨다.

 

이 플로우에서 적절한 대처를 하지 못한다면 발생하는 것이 맨 위에서 본 CORS 에러인 것이다.

 

이 문제를 해결하기 위해서 실제 Spring Boot Application 으로 들어가보자.

 

Spring Boot Application 에서 CORS 해결하기

 

본인이 만약 어떤 API 서버든 가지고 있다면 그 서버로 테스트할 수 있습니다. 저는 현재 팀 프로젝트에서 사용중인 API 서버를 마침 CORS 이슈를 해결해야 하기 때문에 해당 서버로 실습을 이어가겠습니다.

 

본론으로 돌아가서 CORS 는 왜 발생한다고 했는지 생각해보자.

 

Preflight 요청에서 적절한 Access-Control 을 확인하지 못하면 발생하게 된다!

 

이런 Preflight 상황에서 적절한 Access-Control 을 위한 해결 방법이 3가지가 존재한다.

 

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

CorsFilter 생성하기

 

첫 번째로 알아볼 방법은 커스텀 필터를 만드는 것이다.


Access-Control 을 확인할 수 있도록 커스텀 Filter 를 생성해보자.

 

 

필자는 이러한 구조인데, 아무 위치에서도 좋으니 filter 라는 디렉토리를 생성한다.

 

그리고 해당 디렉토리 아래에 CorsFilter 라는 클래스를 하나 생성해보도록 하자.

 

 

그리고 해당 클래스를 Bean 으로 등록하기 위해서 프레임워크에게 알려야 한다.


@Component 이라는 어노테이션을 추가하고, Filter 인터페이스를 구현하여 Override 해보자.

 

주의해야할 것이 있는데, Filter 는 꼭 javax.servlet 의 Filter를 사용해야 한다.

 

@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 {

    }

    @Override
    public void destroy() {

    }
}

 

우리는 해당 필터가 실제로 수행할 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() {

    }
}

 

우리가 필요한 헤더를 OPTIONS 가 잘 확인할 수 있도록 설정해줄 수 있다.

 

CrossOrigin 어노테이션 사용하기

 

CrossOrigin 어노테이션을 사용하는 방법은 매우 간단하다.

 

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

 

@RestController
@RequestMapping(value = "/api/threats", 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());
    }
}

 

설정 방법은 2가지가 있는데,

 

  1. 컨트롤러 클래스 단에서 설정
  2. 메서드 단에서 설정

 

둘중 하나의 방법만 사용해도 된다.

 

하지만 단점이라고 한다면 컨트롤러가 많을 수록 설정해야하는 어노테이션이 많아진다는 것이다.

 

WebMvcConfigurer 에서 설정하기

 

해당 사용법도 간단하다.

 

Spring Initializer 를 이용해서 프로젝트를 만들었다면 다음과 같은 main 함수가 존재한다.

 

@SpringBootApplication
public class RestServiceCorsApplication {

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

 

해당 main 함수에서 Bean 으로 Configurer 를 추가해주면 된다.

 

@SpringBootApplication
public class RestServiceCorsApplication {

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

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

}

 

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

 

댓글0