발걸음/일지

5주차_회고

리꾸므 2022. 9. 4. 11:57

5주차 keyword : CORS

 

CORS란?

 Cross-Origin Resource Sharing Policy의 두문자로 에러나 오류가 아니다. 현재 Ip가 아닌 다른 Ip로 리소스를 요청하는 구조로 하나의 보안 정책이라 할 수 있다.  CORS를 이해하기 위해서 먼저 Origin, SOP, Access-control-Allow-Origin에 대해 알아보자.

 

 

 

 

 

1. Origin

 Origin은 요청이 시작된 서버의 위치를 나타내는 문구이다. 만약 내가 로그인 서버로 로그인 요청을 한다면 여기서 서버 Ip는 2개가 존재한다.

 

1. Client 서버

2. 로그인 API 서버

 

이 2개의 Ip는 서로 다른 Ip의 번호를 갖고 있게 된다. 예를 들어 Client 서버는 http://client:80이라하고 로그인 서버는 http://login:3000/이라고 해보자 그럼 여기서 클라이언트 서버가 로그인 서버에게 HTTP요청을 보낼때 Origin이 다르다 라고 하는 것이다. 이를 Crooss Orgin이라고 하고, 만약 로그인 서버가 내부 통신을해서 서버 내 데이터를 가져온다면 그것을 Same Origin이라고 한다.

 

 

1_1.Origin 구분하는 방법

 Origin을 구분할때는 3가지 이용한다.

 

1. 스키마

2. HOST

3. Port

위 경로가 있다면 여기에 스키마, Host, Port가 모두 포함되어 있다.

이런 주소라면 서로 모든 Origin이 동일한 상태인 것이다.

 

 

 

 

 

2. SOP

 Same Origin Policy 두문자로 2011년 RFC 6454에서 등장한 보안정책이다. 동일한 출처의 Origin만 리소스를 공유할 수 있다를 의미한다. 이 SOP 표준에 따라야지 XSS라던지 CSRF라는 보안상의 Issue를 막을 수 있게 된다.  

 

만약 동일한 출처가 아니라면 OPTIONS를 이용한 Preflight를 이용하여 여러 검증을 거치게 되는데 이 검증이 바로  CORS이다.

 

 

 

 

 

3. Access-Control-Allow-Origin

 Access-Control-Allow-Origin은 바로 CORS를 해결할 수 있는 고마운 녀석이다. 우리가 SOP정책에 따라선 같은 Origin만 자원공유 가능하다 했든네 그럼 다른 곳은 자원이용을 못하는 것일까?  다행이도 Access-Control-Allow-Origin 덕분에 서로 다른 Origin에서 자원 공유가 가능해졌다.

 

 

 

 

 

4. 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 에러인 것이다.

 

 

 

 

 

5. Spring Boot Application에서 CORS 해결하기

 그럼 CORS는 왜 발생하는가? Preflight 요청에서 적절한 Access-Control을 확인하지 못하면 발생한다. 이런 Preflight 상황에서 적절한 Access-Control을 위한 해결 방법이 3가지가 존재한다. 

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

 

5_1. CorsFilter 생성하기

  1.  Access-Control을 확인할 수 있도록 CorsFilter를 아무위치에서도 좋으니 Filter라는 디렉토리를 생성한다음 그 안에 클래스로 하나 생성해준다.  
  2. 해당 클래스를 Bean으로 등록하기 위해 @Component 어노테이션 추가, Filter 인터페이스를 구현하여 Override 한다.
  3. 주의점은 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가 잘 확인할 수 있도록 설정해줄 수 있다.

 

 

5_2. 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가지다

  • 컨트롤러 클래스 단에서 설정
  • 메서드 단에서 설정

단점은 컨트롤러가 많을 수록 설정해야 하는 어노테이션이 많아진다는 것이다.

 

 

 

5_3. WebMvcConfigurer에서 설정하기

 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 을 허용한 클래스에서 등록을 할 수도 있다.

 

 

 

 

 

 

 

참고

https://wonit.tistory.com/572