티스토리 뷰

728x90

같은 증상이 발생하면 해보기 바란다.

현재 스프링 부터 3.2.2 버전을 사용하고 있다. Security는 6.2.2

 

 

1. 이 문제는 고질적인 문제이다. Spring Cloud 환경에서 React와 Spring Boot 개발할 때 생기는 문제로 아무리 Spring Cloud Gateway 설정을 만져봤자 브라우저에서는 Origin이 없다고 호출을 차단해 버린다. 즉 아래 설정 같은 거 아무리 만줘 봤지 동작하지 않는다. gateway 서버가 그냥 헤더를 무성히하게 반환한다.

 

  sporing:
    cloud:
      gateway:
        default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials
        globalcors:
          cors-configurations:
            '[/**]':
              allowedOrigins:
                - 'http://localhost:3000'
              allow-credentials: true
              allowedHeaders: '*'
              allowedMethods:
                - PUT
                - GET
                - POST
                - DELETE
                - OPTIONS

 

2. 인터넷에 찾아봐도 그냥 설정만 붙이면 되는 식으로 답변이 되어 있어서 Spring Cloud로 React와 Spring Boot 조합으로 개발하기 싫어지는 하나의 원인이다.

 

3. 원인은 React 클라이언트가 Spring Cloud Gateway에 호출이 전달 될 때 필터가 제대로 실행이 되지 않는 부분이다. 그래서 해결을 하려면 Gateway에 필터설정을 수동으로 코딩해야 한다. 그리고 실제 호출을 받는 마이크로서비스의 Cors 설정값과 중복되는 부분은 제거해 주어야 한다.

 

4. 가장 쉬운 방법은 Gateway 의 CorsWebFilter 빈을 생성하여 등록하면 될 것으로 보인다. 그런데 아래처럼 하면 안된다. 대부분은 돌아가는 것 같은데 React는 안되는 것 같다. CorsWebFilter의 기본 filter 메소드가 문제가 있어 보인다.

@Configuration
open class CorsConfig {
    @Bean
    open fun corsWebFilter(): CorsWebFilter {
        val corsConfig = CorsConfiguration()
        corsConfig.allowedOrigins = listOf("http://localhost:3000")
        corsConfig.maxAge = 3600
        corsConfig.addAllowedMethod("GET, PUT, POST, DELETE, OPTIONS")
        corsConfig.addAllowedHeader("*")
        corsConfig.allowCredentials = true

        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", corsConfig)

        return CorsWebFilter(source)
    }
}

 

5. 그래서 CorsWebFilter를 상속해서 대충 구현해 보면 아래 처럼 될 것이다. 이렇게 하면 돌아간다. 물론 configSource로 받은 정보는 하나도 사용하지 않기 때문에 인자는 무시해도 된다. 아래서 중요한 부분은 if 절인데 클라이언트에서 OPTIONS으로 preflight를 받았을 경우에만 credentials과  Origin을 설정해주어야 한다. 

  5-1 그 이유는 OPTIONS은 prefight이기 때문에 MSA 단으로 전달되지가 않기 때문에 필요한 정보를 모두 기입해 주어야 한다.

  5-2 뒷단으로 전달되는 부분은 모두 빼주어야 한다.

    5-2-1 Credentials이 위에 있으면 credential이 불필요한 경우에도 헤더에 기입되기 때문에 Cors를 통과하지 못한다. 성공한다고 하더라도 로그인 후에 cookie로 jwt를 받지 못한다.

    5-2-2 origin을 빼주지 않으면 MSA 단에서 설정된 Cors와 중복이 되어 Origins 에 동일한 설정의 두개의 값이 들어간다. 이것 역시 cors에서 허용하지 않는다.

 

* 아래 코드에서 허용헤더를 *로 설정할 경우에 Content-Type 헤더가 허용되어 있지 않다고 cors를 통과하지 못하는 경우가 있다. 필요한 헤더를 모두 나열해주는 것이 안전하다.

 

class CorsCustomFilter() : CorsWebFilter(null!!) {
    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
        val request = exchange.request
        if (CorsUtils.isCorsRequest(request)) {
            val response = exchange.response
            val headers = response.headers
            headers.add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS")
            headers.add("Access-Control-Max-Age", "3600")
            if (request.method === HttpMethod.OPTIONS) {
                headers.add("Access-Control-Allow-Credentials", "true")
                headers.add("Access-Control-Allow-Origin", "http://localhost:3000")
                headers.add(
                    "Access-Control-Allow-Headers",
                    "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN"
                )                
                response.setStatusCode(HttpStatus.OK)
                return Mono.empty()
            }
        }
        return chain.filter(exchange);
    }
}

 

 

6. 위의 소스는 좀 보기가 그렇고, null도 강제하고 뭔가 보기 싫다. 그냥 아래처럼 config파일에서  lambda로 설정하면 더 보기 좋다. CorsWebFilter가 WebFilter를 상속하기 때문에 필요한 부분을 코딩하는 것으로 충분하다.

 

    @Bean
    open fun corsFilter(): WebFilter {
        return WebFilter { exchange, chain ->
            val request = exchange.request
            if (CorsUtils.isCorsRequest(request)) {
                val response = exchange.response
                val headers = response.headers
                headers.add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS")
                headers.add("Access-Control-Max-Age", "3600")
                if (request.method === HttpMethod.OPTIONS) {
                    headers.add("Access-Control-Allow-Credentials", "true")
                    headers.add("Access-Control-Allow-Origin", "http://localhost:3000")
                    headers.add(
                        "Access-Control-Allow-Headers",
                        "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN"
                    )                    
                    response.setStatusCode(HttpStatus.OK)
                    return@WebFilter Mono.empty()
                }
            }
            chain.filter(exchange);
        }
    }
728x90

'Client Technologies > React' 카테고리의 다른 글

NextJs 구글 폰트 사용하기  (0) 2024.03.26
CORS, Cookie: React, Spring Boot  (0) 2024.03.09
React : SCSS 사용하기  (0) 2020.11.22
React : CSS 사용하기  (0) 2020.11.22
React : React Modal 사용하기  (0) 2020.11.16
댓글