[문제 해결] CORS 에러 해결하기

2025. 11. 26. 22:32문제 해결

 

이번 포스팅에서는 CORS Error 해결 과정에 대해서 알아보겠습니다.

 

문제 상황

기존에 만들어 둔 백엔드 서버와 프론트 엔드 코드를 연결해 사용자 화면을 구성하는 작업을 하는 중 이었습니다.

 

입력 폼에서 사용자 이메일과 비밀번호를 입력받고 로그인을 실행하는 부분입니다.

로그인을 실행했으니 프론트에서 서버의 signin API를 호출하게 됩니다.

하지만 에러와 함께 로그인이 작동하지 않았습니다.

네트워크 탭으로 어떤 오류가 발생하는지 살펴보니, CORS error와 403 Forbidden 에러가 발생하고 있었습니다.

 

403 Error? 사용자가 로그인을 하는데, 잘못된 입력으로 인한 Bad Request도 아니고 Forbidden 에러가 나는 이유를 찾아야 했습니다. 왜냐하면 제가 구축한 서버는 JWT 기반 인증을 사용하며 "/signin" API는 WHITE_LIST에 포함되어 JWT Filter에서 토큰 검사를 진행하지 않기 때문입니다.

 

 

403 Error가 발생하는 요청의 Request Header를 다시 살펴보니 Request Method가 OPTIONS로 되어있습니다.

 

HTTP Method OPTIONS

OPTIONS method가 무엇인지 알기 전에 왜 이 method를 사용하는지에 대해서 먼저 알면 더 좋을 듯 합니다.

 

CORS(Access-Control-Allow-Origin)란 다음과 같습니다.

웹 브라우저가 외부 도메인의 서버와 통신하기 위해 HTTP 헤더 기반 메커니즘을 사용한 것을 CORS라 한다. 그렇다면 왜 안전하게 1개의 도메인이 아닌 다른 도메인에서 데이터를 가져오는 것일까? 그것은 웹 기술의 발전에 따라 다른 서버로 요청을 보내거나 페이지내 자원을 분리해야할 필요성이 생겨나서 이를 다른 도메인에서 가져와 쓰는 것이다.

 

필자는 CORS에 대해 이해하기 위해 위 글을 보던 중에 "백엔드 코드도 localhost로, 프론트 코드도 localhost로 띄웠는데도 오류가 발생한거면, 어떤 부분에서 다른 도메인이길래 오류가 발생한거지?"라는 생각을 하게 되었습니다.

 

여기서 말하는 도메인이란 정확히 말하면 출처(Origin)을 뜻합니다.

 

흔히 naver.com과 google.com 처럼 주소 이름이 달라야 도메인이라고 생각하지만 브라우저는 아래 3가지가 모두 일치해야 같은 곳(Same Origin)이라 인식합니다.

  1. 프로토콜 : http or https
  2. 호스트 : localhost, naver, google ...
  3. 포트 : 8080, 3000 ...

저의 경우은 백엔드 코드는 8080 포트로, 프론트 코드는 5173 포트로 실행했기 때문에 다른 Origin으로 인식하게 된 것 입니다.

 

만약 브라우저가 포트 번호를 무시하고 호스트만 같을 경우에도 데이터를 가져오는 것을 허용해준다면

다른 포트에서 "8080 포트에 관리자 권한으로 유저 다 삭제해줘"라는 요청을 보내도 브라우저는 호스트가 같기 때문에 같은 서버 안에서의 요청이라는 판단을 내리고 허용해 줄 것입니다.

 

이러한 사고를 막기 위해 포트 번호가 달라도 브라우저는 이를 외부 출처라 간주하고 CORS 정책을 발동시킵니다.

그래서 어떠한 요청을 할것인지 미리 보내 승인을 받으면 데이터를 가져옵니다.

이때 데이터를 가져오기 위해서 Access-Control-Allow-Origin 헤더 속성을 이용하여 접속 가능 여부를 확인합니다. 또한 그 때 사용되는 요청이 Preflight Request입니다. 

 

Preflight Request

권한 및 해당 도메인에 대한 안전을 확인하기 위한 사전요청과 같습니다.

클라이언트가 요청하는 URL이 외부 도메인일 경우 웹브라우저 자체적으로 실행되는데 OPTIONS 메서드로 사전 요청을 보내고 무슨 요청을 사용할 수 있는지 권한이 출력됩니다.

 

터미널에 다음과 같이 입력하면 OPTIONS 요청을 보낼 수 있습니다. (요청을 보낼 서버에 별도의 SSL 설정이 없다면 http로 보내야 합니다.)

curl -X OPTIONS http://API서버 -i

 

 

그럼 위와 같이 해당 URL이 어떤 Method를 수행할 수 있는 지 Allow 부분에서 확인할 수 있습니다.

 

그럼 지금까지의 과정을 정리해보자면, 클라이언트에서 요청하는 서버의 포트가 달라 브라우저는 서로 다른 Origin에서 요청을 보내는 것이라 판단하고 CORS 정책을 발동시켜 OPTIONS Method로 Preflight Request를 보냈습니다. 

그런데 요청의 결과는 403 Forbidden이었습니다. 

 

왜 403이 발생했을까?

백엔드 서버의 JwtFilter 코드에서 걸려서 발생한 것입니다.

OPTIONS 요청에는 Authorization 헤더가 없습니다. 따라서 유효하지 않은 요청이라 판단하고 403 에러를 내립니다.

그렇기에 CorsConfiguration이라는 클래스를 만들어 JwtFilter보다 먼저 검사하며 응답 헤더에 내용을 포함시켜 줍니다.

 

@Configuration
public class CorsConfig {
    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);

        config.setAllowedOriginPatterns(Arrays.asList(
                "http://localhost:5173",
                "http://127.0.0.1:5173"
        ));

        config.addAllowedHeader("*");

        config.addAllowedMethod("*");

        source.registerCorsConfiguration("/**", config);

        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));

        bean.setOrder(0);
        return bean;
    }
}

 

코드를 라인별로 살펴보자면,

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();

 

CORS 설정을 담을 객체를 만들어 줍니다.

 

그 다음 부턴 만든 config 객체에 CORS 설정을 set 해줍니다.

config.setAllowCredentials(true);

 

 

브라우저가 서버에 요청을 보낼 때 쿠키(Cookie)나 인증 헤더를 같이 실어 보내도 되는지 허락하는 부분입니다.

서버에서 허용하는 Origin이 특정될 때는 그냥 true로 해도 무방하지만, 특정되지 않았을 때 true일 경우는 보안에 취약합니다.

 

config.setAllowedOriginPatterns(Arrays.asList(
                "http://localhost:5173",
                "http://127.0.0.1:5173"
        ));

 

서버에서 허용하는 Origin을 특정합니다. Array List로 여러개를 설정할 수 있습니다.

저의 경우 아직 배포가 이루어지는 과정이 아니고 local에서 아직 테스트로 개발을 진행하였기에 위와 같이 작성해주었습니다.

 

config.addAllowedHeader("*");

 

클라이언트가 Content-Type, Authorization 외에 X-Custom-Header 같은 임의의 헤더를 보내는 것을 허용한다는 의미입니다.

 

config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));

 

설정된 Method만 허용하겠다는 의미입니다.

"*" 로 모든 것을 허용하겠다 설정하여도 무방할 수 있지만 자주 사용하지 않는 마이너한 Method까지 포함한다면 보안에 취약할 수 있으므로 서버에서 허용하는 Method만 명시적으로 설정해 주는 것이 낫습니다. 

 

source.registerCorsConfiguration("/**", config);

 

위 코드는 지금까지 설정한 규칙(config)을 서버의 모든 URL에 적용하겠다는 의미입니다.

 

FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(0);

 

필터를 등록하고 순서를 변경하는 부분입니다.

순서를 0번으로 설정하여 JwtFilter보다 먼저 동작하도록 설정했습니다. 

 

완성된 코드는 다음과 같습니다.

 

 

코드를 변경하고 다시 로그인 요청을 시도해보면,

 

이제 signin 요청이 성공적으로 작동 되는 것을 볼 수 있습니다.

 

 

참고 블로그

https://tyrell96.tistory.com/43

 

OPTIONS method 란?

HTTP OPTIONS 메소드를 쓰는 이유를 알기 위해서는 CORS란 개념을 우선 숙지해야한다. CORS란 웹 브라우저가 외부 도메인의 서버와 통신하기 위해 HTTP 헤더 기반 메커니즘을 사용한 것을 CORS라 한다.

tyrell96.tistory.com