본문 바로가기

Infrastructure/Network

[Network] CORS 해결 예제

이전 장(링크) 에서는 CORS란 무엇인지에 대해서 알아보았다.
이번 장에서는 실제로 현업에서 만날수 있는 상황을 예로 들어 해결하는 방법에 대해서 알아본다.


개발 환경

  • OS: CentOS: 7
  • Docker: 20.10.17
  • Docker-compose: 1.27.4
  • Spring Boot: 2.7.0
  • Nginx: 1.21.6

상황

필자가 자주 경험한 두 상황을 예로 들어 해결하는 방법을 알아보려 한다.

상황 1

개발자가 자신의 PC에서 프론트엔드 코드를 직접 빌드하여 서버에 있는 API 서버에게 요청을 보내는 상황이다.

  1. 개발자 PC의 브라우저에 http://localhost:3000으로 접속한다.
    Originhttp://localhost:3000이 된다.

  2. 브라우저의 요청을 받은 Reverse ProxyNginx는 요청을 API 서버로 전달한다.

  3. API 서버Nginx로 요청을 반환한다.

  4. 응답을 받은 브라우저는 Origin과 응답한 서버의 출처인 api.your-domain.com는 서로 다르기 때문에 SOP (Same-origin policy)를 위반했다고 판단하여 CORS 에러를 출력하며 응답에 대한 결과물을 화면에 표시하지 않는다.

상황 2

도커 서버 내부에 리버스프록시, 프론트엔드, API 서버가 실행되고 있으며 고객이 PC의 브라우저를 통해 프론트엔드 서비스에 접속하는 상황이다.

  1. 고객은 https://your-domain.com이라는 주소를 입력하고 서버에 요청을 보낸다.
    이때 Originhttps://your-domain.com이 된다.

  2. 요청을 받은 Nginx는 브라우저에게 화면을 렌더링 할 수 있도록 프론트엔드 코드를 응답한다.

  3. 이후에 API 요청이 필요한 기능을 고객이 클릭하면 API 서버로 요청을 보내게 된다.

  4. 요청은 Nginx를 거쳐서 API 서버에 전달되고 API 서버에 의해 처리된 결과를 Nginx는 고객의 브라우저에게 전달한다.

  5. Originhttps://your-domain.com이고 요청을 처리한 API 서버의 출처는 https://api.your-domain.com으로 출처가 다르기 때문에 브라우저는 SOP를 위반했다고 판단하여 CORS 에러를 출력하며 응답에 대한 결과물을 화면에 표시하지 않는다.


해결방법

대부분의 서비스는 요청에 인증 정보가 포함되기 때문에 이번 예시에도 인증 정보가 헤더에 포함된다고 가정하고 해결해본다.

상황 1

수정하기 전의 Nginx 설정을 살펴본다.

upstream docker-api {
    server api:8080;
}
server {
    listen 80;
    listen [::]:80;
    server_name api.your-domain.net www.api.your-domain.net;

    if ($http_x_forwarded_proto = 'http') {
        return 301 https://$host$request_uri;
    }

    location / {
        proxy_pass http://docker-api;
        proxy_set_header   Host                 $http_host;
        proxy_set_header   X-Real-IP            $remote_addr;
        proxy_set_header   X-Forwarded-For      $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Host     $server_name;
    }
}

api.your-domain.net 도메인으로 http 요청이 오는 경우 https를 사용하여 443 포트로 리다이렉트 하고 있다.
https 요청을 받게 되는 경우 API 서버가 실행되고 있는 컨테이너로 요청을 전달하게 된다. server api:8080에서 apiAPI 서버가 실행되고 있는 컨테이너의 이름이다.

필자의 PC에서 프론트엔드 코드를 실행시켜 localhost:3000 주소로 접속하여 API 서버로 요청을 보내본다.

에러 메시지를 확인해보면 Originhttp://localhost:3000으로부터 http://api.your-domain.net으로 요청을 보냈지만 헤더의 Access-Control-Allow-Origin값이 포함되어 있지 않기 때문에 CORS policy 에 의해 block 되었다고 출력되고 있다.

문제를 해결하기 위해 아래와 같이 Nginx 설정을 수정해본다.

upstream docker-api {
    server api:8080;
}

server {
    listen 80;
    listen [::]:80;

    server_name api.your-domain.net www.api.your-domain.net;

    if ($http_x_forwarded_proto = 'http') {
        return 301 https://$host$request_uri;
    }

    if ($host = 'www.api.your-domain.net') {
        return 301 https://api.your-domain.net$request_uri;
    }

    location / {
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        set $cors '';
        if ($http_origin ~ 'http(s)?:\/\/(localhost(:\d{1,5}?)|your-domain\.net)$') {
            set $cors 'true';
        }

        if ($cors = 'true') {
            add_header 'Access-Control-Allow-Origin' $http_origin;
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With';
            add_header 'Access-Control-Expose-Headers' 'Authorization';
        }

        proxy_hide_header Access-Control-Allow-Origin;
        proxy_hide_header Access-Control-Allow-Credentials;
        proxy_set_header HOST              $http_host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host  $server_name;
        proxy_pass http://docker-api;
    }
}
  • HTTP로 요청이 오는 경우 HTTPS로 리다이렉트 되도록 하였다.
  • www.api.your-domain.net으로 요청이 오는 경우 통일성을 위해 api.your-domain.net으로 리다이렉트 되도록 하였다.
  • HTTP 메서드가 OPTIONS 인 경우 preflight 요청 및 응답을 할 수 있도록 헤더 값을 설정한다.
  • Origin이 우리가 정한 정규식을 통과한 경우 헤더에 허용 가능한 Origin, Credentials, Methods, Headers 값을 설정해준다.
  • NginxReverse Proxy로 사용하는 경우 Nginx에서 추가한 헤더가 반영되지 않는 경우가 있다. proxy_hide_header를 사용해서 반드시 반영되어야 하는 헤더 값을 지정한다.

입력된 Origin의 정보를 Access-Control-Allow-Origin의 값으로 입력하거나 *로 처리하는 방법이 훨씬 간단하게 해결된다.
하지만 상용 서비스에서 모든 Origin을 허용하는 것은 보안상 좋지 않은 방식이다. 우리가 지정한 Origin만 통과하도록 해야한다.
물론 http://localhost:3000이라는 Origin을 허용하는 것도 보안상 좋지 않은 방식이다. 부득이하게 로컬 환경에서 서버의 API를 요청해야 하는 경우 커스텀 헤더 값을 만들고 정해진 헤더 값을 전달하는 요청만 처리되도록 하는 것이 좋다.

설정을 변경하고 Nginx를 재실행하고 다시 접속해보면 CORS에러없이 정상적으로 API 요청의 결과가 화면에 표시되는 것을 확인할 수 있다.


상황 2

로컬 환경의 요청에서 발생하는 CORS 에러는 위의 방식대로 해결되었다.
설정을 살펴보면 https://your-domain.com으로 들어오는 요청도 localhost와 동일하게 정상 처리되어야 한다. 하지만 웹 페이지에 접속해보면 CORS 에러가 발생하는 것을 확인할 수 있다.

Nginx에서 헤더를 추가했기 때문에 정상적으로 요청이 처리되었어야 한다. 하지만 CORS 에러가 발생하였고 정확한 원인은 파악하지 못했다.
하지만 스프링 부트 기준으로 해결방법은 발견하였다. 아래와 같이 CORS 관련 Bean을 등록하여 WAS에서 헤더에 값을 추가할 수 있도록 수정하는 방법이다.
(스프링 시큐리티를 사용하는 서비스는 아래와 같은 방식이 아니라 스프링 시큐리티를 사용하는 방식으로 적용해야 한다. 구글에 수많은 자료가 있으므로 본 문서에서는 다루지 않는다.)

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
    private static final String[] ALLOWED_ORIGINS = {
        "http://localhost:3000",
        "https://your-domain.net"
    };
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins(ALLOWED_ORIGINS)
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

구성파일이 추가된 도커 이미지를 만들고 API 서버를 재실행시키면 정상적으로 CORS 에러 없이 API 요청이 화면에 표시되는 것을 확인할 수 있다.


CORS란 무엇이지 알아보았던 이전 장에 이어 현업에서 마주칠 수 있는 CORS 에러를 직접 해결하는 방법에 대해서 알아보았다.
아직 상황 2에서 왜 추가로 스프링에 빈을 등록하여 헤더에 값을 추가해야 하는지에 대해서는 해결되지 않았다.

물론 같은 도커 서버 내부에서 private 통신을 하기 때문에 API 서버를 요청하는 시점에는 Nginx를 거치지 않기 때문이라고 추측은 된다.
이번에는 시관관계상 글을 마무리하고 추후에 테스트를 진행해보고 결과를 공유하도록 하겠다.


참고한 문서

'Infrastructure > Network' 카테고리의 다른 글

[Network] CORS 란?  (0) 2022.06.20
[HTTP] 헤더 - 4 (쿠키)  (0) 2021.06.28
[HTTP] 헤더 - 3 (일반 정보)  (0) 2021.06.28
[HTTP] 헤더 - 2 (전송 방식)  (0) 2021.06.28
[HTTP] 헤더 - 6 (조건부 요청)  (0) 2021.06.28