본문 바로가기
백엔드 개발

#030. CORS 에러 원인과 해결 (feat. 서버에서 CORS 테스트 하기)

by iamjoy 2023. 4. 8.

개념

[CORS 원인]
CORS 는 Cross-Origin-Resource-Sharing 의 약자로 "서로 다른 도메인" 간 자원을 공유하는 것을 금지하는 "브라우저의 정책"이다. 여기에서 핵심은 도메인브라우저이다.
먼저, 서로 다른 도메인이라면 서로 같은 지 비교할 도메인이 최소 두 가지 있어야 한다. 브라우저 정책이기 때문에 브라우저를 기준으로 두 가지 도메인이 필요함을 알 수 있다. 이를 이해하기 위한 그림은 아래와 같다. 

서로 다른 도메인은 아래의 (1)과 (2) 이다. 브라우저를 기준으로 이 (1)번 도메인과 (2)번 도메인을 비교해서 도메인이 같지 않으면 CORS 에러가 발생하는 것이다. 어제 경험한 CORS에러는 서버 코드가 abc.com에, 클라이언트 코드가 localhost에 있는 경우였다. 브라우저 화면에 띄우려고 하니 브라우저 입장에서는 (1)abc.com 과 (2)localhost가 서로 다르기 때문에 CORS에러가 발생한다.

대부분 개발환경에서 겪는 건 localhost에 서버와 클라이언트를 띄워놓은 경우인데 도메인은 port까지 보기 때문에 localhost:8080 과 localhost:3000을 서로 다른 origin으로 보고 에러를 발생시킨다.

 

[문제 해결 방법]
두 가지를 작업한다. 클라이언트 도메인 (그림의 (1))을 서버가 열어주는 도메인으로 바꾼다. 그리고 서버에서 허용할 도메인을 설정한다.(그림의 (2)) 구체적인 예시를 이용해서 실제로 CORS에러가 사라지는 테스트 환경을 만들어보려고 한다.

적용 (서버에서 CORS 테스트하기)

(1) 서버: CORS 허용 정책 설정

이건, 어떤 프레임워크를 사용하는 지에 따라 인터넷에 검색하면 자료가 굉장히 많다. 여기서는 Spring Security로 설정했기 때문에 다음과 같이 corsConfiguration 설정을 해준다.

서버는 보통 자신이 올라가는 곳의 도메인을 모르기 때문에 고정 도메인을 설정하고 서버가 올라가는 IP에 연결시키는 방식으로 브라우저가 fetching할 도메인을 고정할 수 있다.  예를 들면 여기에서는 "*.domain.com"을 허용했다. 이렇게 하면 (클라이언트)service.domain.com (서버)service.api.domain.com 과 같이 서브 도메인을 이용하면서도 서로 같은 도메인을 가지기 때문에 CORS 정책에 맞출 수 있다.

여기에서는 CORS 가 잘 설정되는 지를 확인하는 것이 목표이므로 허용 도메인을 전체로 열고 테스트 하려고 한다.

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import javax.servlet.http.HttpSession

@Configuration
class SecurityConfig(
    @Autowired private val httpSession: HttpSession
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http.csrf().disable()
            .formLogin().disable()
            .httpBasic().disable()
            .cors().configurationSource(corsConfigurationSource()) // cors configuration 설정 반영
            .and()
            .headers()
            .frameOptions().sameOrigin().and()
            .authorizeRequests()
            .antMatchers("/").authenticated()
            .antMatchers("/api/v1/auth/login").permitAll()
            .and()
        return http.build()
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.addAllowedOrigin("*.domain.com")
        configuration.addAllowedHeader("*")
        configuration.addAllowedMethod("*")
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", configuration)
        return source
    }
}

(2) 브라우저 테스트

CORS가 제대로 설정되어있는 지 확인하기 위해서 다음 두 가지를 할 수 있다.
1. ngrok을 이용해 도메인을 설정한다.
mac에서 ngrok을 설정하는 방법은 매우 간단하다: https://ngrok.com/download
그러면 이와 같은 도메인을 발급받을 수 있다:  https://4530-223-62-10-151.jp.ngrok.io

2. 브라우저를 통해 호출한다.
postman 과 같은 API Platform을 이용하면 브라우저에서 테스트 해볼 수 있는 코드를 쉽게 만들 수 있다.  예를 들면 아래와 같이 테스트 하려는 URL을 한 번 실행하면 오른쪽 Code snippet에서 원하는 fetch 형태로 바꿔주는 데, 이 중 JavaScript- Fetch 코드를 설정하고 복사한다. 

postman에서 javascript fetch 코드를 생성

그리고 아래와 같이 다른 도메인(위의 설명에서 1번에 해당하는 도메인)을 가진 브라우저에서 호출을 테스트 해보면 된다.

 

(3) 클라이언트 설정: 브라우저에서 fetch

보통 로컬에서 개발하기 때문에 도메인이 localhost인 경우가 많다. 이런 경우 localhost 와 서버가 올라가져있는 도메인(여기에서는 service.api.domain.com)의 도메인이 다르기 때문에 CORS에러가 발생한다. 따라서 클라이언트 코드를 -> 브라우저가 다운 받는 도메인 또한 이 도메인을 맞춰줘야한다. 이 설정은 /etc/hosts/에서 할 수 있다.

$ cd ~/
$ vi /etc/hosts


를 하면 다음과 같은 파일을 편집할 수 있다.

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1       localhost
255.255.255.255 broadcasthost
::1             localhost

127.0.0.1 service.domain.com //<----- localhost 와 service.domain.com 매핑
# End of section



preflight란?

위의 요청을 하면 Network의 all을 선택한 경우 아래와 같이 Post요청이 두 번 가는 것을 확인할 수 있다.

Post 요청을 하는 경우 이 요청이 유효한지 확인하는 prefilght 요청을 먼저 한다. 이 요청을 확인해보면 OPTIONS 매서드를 사용하고 있는 것을 확인할 수 있다.