네트워크

CORS와 해결법

무무11 2024. 12. 26. 16:46

프로젝트 기간에 CORS 문제를 해결했던 과정과 CORS가 무엇인지에 대해 정리해보았다.

 

1. CORS(Cross-Origin-Resource-Sharing)란?

CORS란 그대로 한국어로 직역하면 ‘교차 출처 리소스(자원) 공유’라는 뜻이다. 간단히 말해 서로 다른 두 출처(Origin)가 자원을 공유한다는 뜻이다.

 

CORS 에러라고 악명높게 알려져 있어서 CORS가 문제 발생의 원인이라고 생각하는 경우가 많은데 사실 CORS는 해결책이다. CORS에 대해 알기 위해서는 먼저 SOP에 대해 알아야한다.

SOP(Same-Origin-Policy)

SOP는 '동일 출처 정책'이라는 뜻이다. 이 동일 출처 정책 때문에 에러가 발생하는 것이고 CORS는 그에 대한 해결책이다.

 

'동일 출처 정책'이란 말 그대로 서로 다른 두 출처 간에 자원을 공유할 수 없음을 말한다. 여기서 출처(Origin)란 프로토콜, 호스트, 포트번호를 의미한다. URL에서 이 셋이 같으면 뒤에 어떠한 것이 붙어도 상관없이 같은 출처로 판단한다.

 

서로 다른 출처끼리왜 리소스 공유를 막는 이유는 무엇일까? 그 전에 이 동일 출처 정책이 적용되어 있는 곳이 바로 웹 브라우저라는 점을 알야아한다. 이 웹 브라우저에서 보안상의 이유로 서로 다른 출처에서 자원이 공유되지 못하도록 막는 것이다.

 

브라우저에서는 특정한 사이트들에 접속할 때 인증에 필요한 토큰과 같은 중요한 정보들이 저장되어 있다. 어떤 의도가 불순한 악의적인 사이트에서 이 인증에 필요한 정보를 가지고 특정 사이트에 가서 개인정보를 가져오게 만들 수도 있을 것이다. 이런 문제를 방지하기 위해서 서로 다른 출처끼리의 리소스 공유를 기본적으로 막는 것이다.

 

하지만 서로 다른 서버끼리 리소스를 공유하는 일은 현재는 매우 흔한 일이 되어버렸다. 그렇지만 여전히 모든 서버끼리 리소스 공유가 가능하면 안될 것이다. 따라서 합의된 출처들 간에 어떤 기준을 충족해야지만 리소스가 공유되도록 만들어진 매커니즘이 바로 CORS이다. 

 

이 CORS 설정을 해주게되면 서로 다른 출처끼리 리소스를 공유할 수 있게 된다.

 

2. CORS 문제 해결하기

개발을 진행하다 보면 위와 같은 CORS 정책에 의해 차단되었다는 메시지를 한 번은 마주하게 될 것이다. CORS가 무엇인지 알았으니 해결하는 법을 정리해보았다.

 

먼저 '동일 출처 정책'이 적용되어 요청을 막는 주체가 브라우저라는 것을 쉽게 확인해볼 수 있다. 포스트맨 같은 것으로 백엔드 서버에 요청을 날리면 정상적으로 응답을 받을 수 있을 것이다. 하지만 프론트와 백엔드 서버를 모두 띄우고 웹 브라우저의 프론트 페이지에서 요청을 보내면 CORS 문제가 발생한다.

 

CORS 문제를 해결하려면 백엔드에서 요청을 허락할 출처들을 미리 명시해두면된다. (다른 방법으로는 브라우저에서 SOP 설정을 끄는 것일텐데 당연히 그러면 안된다.)

 

진행했던 프로젝트는 Spring Security를 이용중이었기 때문에 설정 파일에 있는 filterChain에 아래 예시 코드와 같이 CORS 설정을 해줌으로써 문제를 해결할 수 있었다. (사용하는 언어, 프레임워크마다 방법은 다르겠지만 원리는 다 똑같다.)

 

코드는 아래와 같다.

@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
    return http
        .cors { cors ->
            cors.configurationSource {
                val configuration = CorsConfiguration()
                configuration.allowedOrigins =
                    listOf("http://localhost:5173", "https://bean-space-front.vercel.app/")
                configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
                configuration.allowedHeaders = listOf(
                    "X-Requested-With",
                    "Content-Type",
                    "Authorization",
                    "Origin",
                    "Accept",
                    "Access-Control-Request-Method",
                    "Access-Control-Request-Headers"
                )
                configuration.allowCredentials = true
                configuration
            }
        }
        .httpBasic { it.disable() }
        // ... 중략

여기서 허용할 출처(Origin, 현재 작업 중인 로컬 주소와 배포된 주소), 허용할 Method, 허용할 헤더 등을 설정할 수 있다.

 

허용할 출처에 와일드카드(*)를 적어놓아도 당장의 문제는 해결되는데, 모든 출처를 허용하는 것은 보안상 위험하기 때문에 반드시 정확한 주소를 적어주어야한다. (그리고 인증된 요청을 이용하려면 와일드카드를 사용할 수 없다.)

 

또한 인증을 위해서 인증된 요청을 이용해야 했기 때문에 allowCredentials에 true를 넣어주었다.

 

Access-Control-Allow-Credentials를 true로 설정하면 인증된 요청을 이용할 수 있는데 일반적인 요청과 다르게 처리해주어야한다. 출처, 메소드, 헤더 모두 와일드카드(*)를 사용할 수 없으며, 정확하게 지정해주어야한다.

 

시험삼아 allowedOrigins에 와일드카드를 넣어서 요청을 날려보았는데 아래와 같은 메시지가 나오면서 에러가 발생했다.

‘When allowCredentials is true, allowedOrigins cannot contain the special value "" since that cannot be set on the "Access-Control-Allow-Origin" response header.’
‘allowCredentials가 true인 경우 “Access-Control-Allow-Origin” 응답 헤더에는 와일드카드 “”를 설정할 수 없으므로 allowedOrigins에는 특수 값 “”를 포함할 수 없습니다.’

 

그리고 인증된 요청의 경우는 요청을 보내는 쪽에서도 withCredentials: true를 넣어서 보내주어야한다.

 

프론트는 리액트를 사용중이어서 아래와 같이 withCredentials: true를 설정해주고 요청을 보내도록 바꿔주었다.

import axios from "axios";

export const authClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true,
});

authClient.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers["Authorization"] = `Bearer ${token}`;
  }
  return config;
});

위와 같이 설정한 뒤에 모든 요청이 정상적으로 응답받는 것을 확인할 수 있었다.

 

이제 Chrome에 있는 개발자 도구를 열어서 확인해보았다.

Request Header에 있는 출처(http://localhost:5173)과 Response Header에 있는 Access-Control-Allow-Origin(http://localhost:5173)이 같은 것을 확인할 수 있다. 이렇게 잘 설정되면 요청과 응답을 정상적으로 잘 받을 수 있다.

 

그리고 프로젝트를 진행하던 중에 다시 한 번 CORS 문제와 마주하였다. AWS S3 저장소를 이용할 때였는데 PresingedUrl을 이용해 이미지를 업로드하려고 할 때 CORS 문제가 발생했다.

 

문제가 발생한 즉시 어떻게 해결해야할지 어렵지 않게 알 수 있었다. 찾아보니 S3의 버킷 설정에서 CORS 설정을 하는 곳이 있었고 설정을 해주니 바로 문제를 해결할 수 있었다.

 

CORS 문제를 해결하는 법은 검색하면 바로 나오고 간단하다. 하지만 문제의 원인을 정확히 알아야지만 같은 문제가 발생했을 때 해결 방법을 쉽게 떠올릴 수 있을 것이다. 문제를 해결하고 글을 정리하면서, 급하게 문제를 해결해야할 경우도 있겠지만 문제를 해결한 이후라도 원인에 대해 정확히 파악하고 정리해두는 것의 중요성을 알 수 있었다. 앞으로도 이런 식으로 꼭 모르는 부분을 잘 이해하고 넘어갈 수 있도록 노력해야겠다.