CORS 어떻게 해결할까? (Spring boot, Nginx)

tkdwns414·2023년 12월 2일
8

CORS

웹과 협업하는 개발자라면 처음 한 번은 무조건 만나게 되는 오류가 있다. 바로 클라이언트가 우리가 열심히 만든 API를 붙이려고 할 때 나타나는 Cross-Origin Resource Sharing, 즉 CORS 오류이다. 우선은 이 짜증나는 CORS 오류가 왜 있는지 알아야 할 필요가 있다.

CORS의 존재 이유

웹은 기본적으로 '동일 출처 정책(Same-Orgin Policy)'를 기본적인 보안 원칙으로 책정하고 있다. 해당 정책은 간단히 말해 출처 A에서 생산된 리소스들에 대해서 출처 B가 상호작용 하지 못하도록 막는 것인데 이를 통해 악의적인 사이트에서 사용자의 데이터를 도용하거나 조작하는 것을 방지하기 위함이다.

하지만 이럴 경우 다른 출처에 대해서 리소스가 필요할 때는 요청하지 못하게 되기 때문에 이를 지키면서 추가적으로 접근할 수 있도록 허용해주는 것이 CORS인 것이다. CORS 덕분에 안전하게 다른 사이트의 리소스를 가져올 수 있다고 생각하면 된다.

CORS 처리 방법

CORS는 우리가 진행하는 프로젝트 코드 단에서 잡을 수도 있고 최종적으로 우리가 배포할 인프라 단에서도 잡을 수 있다. 요즘 내가 주로 사용하는 프레임워크는 Spring Boot이므로 Spring Boot에서 CORS를 잡는 방법 중 하나와 배포 후 Nignx 자체에서 CORS를 잡는 방법을 소개하도록 하겠다.

Spring Boot

Spring Boot에서 CORS 처리를 하는 방법은 여러개가 있는데 간단하게 다음과 같이 정리할 수 있다.
1. Configuration을 이용해 글로벌로 처리하기
2. Controller 혹은 Method별 annotation 사용
3. 필터 사용

그 중 나는 1번 방법에 대해서만 소개하겠다.

글로벌로 CORS를 처리하려면 다음과 같이 처리할 수 있다.

@Configuration
public class MyConfiguration implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("https://example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*");
    }
}

addMapping은 내 프로젝트의 어떤 경로에 해당 처리를 할 것인지 정의하는 곳이다 "/*"는 모든 경로에 적용하겠다는 뜻이다.
allowdOrigins는 어떤 도메인에 적용할지 정의하는 곳이다. 이때 "
"를 이용하여 모든 출처에 대해서 허용하도록 설정할 수 있긴 하나 웬만해서는 허용할 곳만 적어두도록 하자.
allowedMethods는 허용할 메소드들을 정의하는 곳이다. 마찬가지로 ""를 이용하여 모든 Methods를 허용할 수 있다.
allowedHeaders는 허용할 Header들을 정의하는 곳이다. 위와 마찬가지로 "
"를 이용하여 모든 Header들을 허용할 수 있다.

근데 가끔 ""를 적용했는데도 불구하고 안되는 경우가 있었는데 그럴 때는 그냥 ""가 아니라 "GET", "POST", ... 처럼 직접 적어주니까 해결됐었다.

Nginx

이번에는 Nginx 설정 파일을 통해서 해결해보도록 하겠다. 인스턴스에 Nginx를 설치하고 진행하고 있다면 /etc/nginx/sites-avilable/default에 리버스 프로시 설정을 했을 것이고 그 위치에 아래와 같은 코드를 자신의 상황에 맞게 추가해주면 된다.

단일 도메인

location / {
    # 기타 설정... 리버스 프록시 설정 등

    # CORS 헤더 추가
    add_header 'Access-Control-Allow-Origin' '{허용할 도메인}' always;
    add_header 'Access-Control-Allow-Methods' '{허용할 Method들, *(전체 허용)}' always; 
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type' always;
    
    # OPTIONS 요청에 대한 처리
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '{허용할 도메인}';
        add_header 'Access-Control-Allow-Methods' '{허용할 Method들, *(전체 허용)}';
        add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type';
        add_header 'Content-Length' '0';
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Access-Control-Max-Age' 1728000;
        return 204;
    }
}

자신의 상황에 맞는 내용들을 {}에 넣어서 추가해주면된다. 허용할 Methods들의 경우에 명시해서 적을 때는 어떻게 적어야할지 고민이 될까봐 예시를 적어주자면

location / {
    # 기타 설정... 리버스 프록시 설정 등

    # CORS 헤더 추가
    add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, PUT' always; 
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type' always;
    
    # OPTIONS 요청에 대한 처리
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, PUT, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type';
        add_header 'Content-Length' '0';
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Access-Control-Max-Age' 1728000;
        return 204;
    }
}

위를 참고해서 설정해주면 되겠다.

여러개의 도메인

클라이언트가 여러 도메인에 대한 CORS 허용을 요청한 적이 있어 위의 단일 도메인과 같이 작성한 후 'Access-Control-Allow-Origin'을 여러번 추가했으나 nginx에서 여러개를 허용하지 못한다는 오류를 마주할 수 있었다.

그럼 단일 도메인이 아닐 때는 무조건 어플리케이션 레벨까지 내려가야하는가? 아니다. 다음과 같이 해결할 수 있다.

#server 블록 바깥에 위치
map $http_origin $cors_origin { 
        default "";
        "http://localhost:3000" $http_origin;
        "http://localhost:5173" $http_origin;
        "https://example.com" $http_origin;
}

map은 $http_origin의 값에 따라 $cors_origin 값을 설정한다. $http_origin은 클라이언트에서 요청을 보낼 때 포함된 origin 값을 담고 있는데 map과 $http_origin 덕분에 요청이 들어온 곳에 따라 $cors_origin을 바꿀 수 있다.

# 기존 location 위치에
location / {
	# 기타 설정... 리버스 프록시 설정 등
	
    if ($cors_origin ~* (http://localhost:5173|https://example.com|http://localhost:3000)) {
        add_header 'Access-Control-Allow-Origin' $cors_origin always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, PUT' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type' always;
    }

     if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' $cors_origin;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, PUT, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type';
        add_header 'Content-Length' '0';
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Access-Control-Max-Age' 1728000;
        return 204;
     }

이후에는 원래 if문이 없었으나 if문이 추가됐다. if문의 내용은 $cors_origin이 우측의 (http://localhost:5173|https://example.com|http://localhost:3000) 도메인들 중에 포함돼있는지를 확인하는 조건문이다.
$cors_origin이 도메인들 중 포함돼 있으면 'Access-Control-Allow-Origin' 헤더에 $cors_origin 값을 추가한다.

이렇게하면 nginx에서도 여러개의 도메인에 대해 cors를 허용할 수 있다.

참고로 nginx 설정을 수정했을 때는 restart하는 것을 잊지 말자!

만약 본인이 프로젝트 때 CI/CD를 이용해서 바로바로 배포를 하면서 프로젝트를 할 생각이라면 (로컬에서 본인이 직접 프론트, 백 돌려보지 않을 생각이라면) 코드에 CORS를 추가하지 않고 인프라단에 추가해서 불필요한 파일을 만들지 않을 수도 있겠다.

1개의 댓글

comment-user-thumbnail
2024년 4월 26일

goodddddddd!잘 배우고가요~

답글 달기