본문 바로가기

현재/Issues

Cors와 Preflight 이슈 해결

❗작업 진행하면서 만났던 에러를 공유합니다.

이슈

api 전반적으로 path 수정 및 인증 인터셉터를 추가 한 후 프론트엔지니어분께 api들을 공유드렸고, CORS 문제가 발생한다는 문의를 받게되었습니다.

 

첫번째. WebConfig 설정을 통해 CORS 설정을 허용할 수 있도록 수정해보자

적용한 인증 인터셉터는 제대로 등록이 되었는지, 변경한 api 자체 문제가 없는지 확인을 한 뒤(postman, swagger에서 모두 동작되는 것 확인) 특이사항이 없어 cross origin 요청을 허용해주기 위해 들어오는 도메인, 포트 모두 다시 확인 후 아래와 같이 설정했습니다.

 

registry.addMapping("/**")
            .allowedOrigins(
                "<http://localhost:8080>",
                ...(요청이 들어오는 도메인들)
            )
            .allowedMethods("*")

 

?? : “다시 한번 호출 부탁드립니다!!”

FE : “똑같은 에러가 발생합니다!! ㅠㅠ”

두번째. 요청 로그를 샅샅이 확인해보자

시니어 분과 곰곰이 생각해 본 후, 요청이 제대로 들어오고 있는지 확인을 위해 로그를 추가로 더 심고 요청을 다시 받아보기로 했습니다. 루트(’/’)와 ‘favicon.ico’로 들어오는 요청이 많아 해당 path는 제외하고 요청 필터링 전에 어떻게 요청이 들어오는지 추가해서 볼 수 있도록 합니다.

 

if ("/" != path && "/favicon.ico" != path) {
  val ip = if (req.getHeader("X-Forwarded-For") == null) req.remoteAddr else req.getHeader("X-Forwarded-For")
              val transactionId = req.getHeader(Xheader.TRANSACTION_ID.headerName)?:"NO-TRANSACTION-ID"

  request.headerNames.toList().map {
      logger.info("header {} : {}", it, request.getHeader(it))
  }
  logger.info(">>>>>>>>>>>> Transaction-id : {} Path : {} Method : {} Ip : {}", transactionId, path, method, ip)
}

chain.doFilter(req, res)

 

로그를 확인해보니 아래와 같이 찍혔고, 성공적으로 호출되었을 때의 로그와 비교를 해봤습니다.

(CORS 날 때의 로그)

 

ai.lemontree.content.common.filter.LoggerFilter - header access-control-request-headers : x-transaction-id,x-user-uid
Jul 26 14:25:19 ip-10-10-89-87 web: 2022-07-26 14:25:19.301 [http-nio-8083-exec-4] INFO

 

(호출 성공 시 로그)

 

2022-07-26 14:27:16.285 [http-nio-8083-exec-5] INFO  ai.lemontree.content.common.filter.LoggerFilter - header x-user-uid : KIB9RsTXI60zRRdV5pdNV
  • (실패 시) header access-control-request-headers : x-transaction-id,x-user-uid
  • (성공 시) header x-user-uid : KIB9RsTXI60zRRdV5pdNV

위 헤더 로그를 살펴보면, 성공 시엔 ‘값’이 들어있는 반면 실패 시엔 값이 아닌 헤더 내용이 들어있는것을 확인할 수 있었습니다. POSTMAN, SWAGGER로 호출 시엔 문제가 되지 않다가 브라우저에서만 실패가 뜨는 상황이었고, 브라우저에서 호출할 때만 요청이 다르게 오고 있는 상황이 이해가 가지 않아 직접 확인을 해보기 위해 프론트 서버를 하나 띄웠습니다.

프론트 서버를 하나 띄우고, 아래와 같이 api를 호출 할 수 있도록 작성했습니다. (급히 작성한 내용이라.. 보기 불편하셔도 참아주세요..)

 

<script type="text/javascript">
    $(document).ready(function(){
        $("#submit").click(function(){
            $.ajax({
                type: 'GET',
                url: "<http://localhost:8081/api/place/v1/places/area>",
                dataType: "json",
                beforeSend: function (xhr) {
                    xhr.setRequestHeader("x-user-uid","KIB9RsTXI60zRRdV5pdNV");
                },
                success:function(data){
                    console.log(data.body[0])
                    $.each(data.body[0], function (index, item) {
                        $("#d").append(JSON.stringify(index));
                        $("#d").append(JSON.stringify(item)+"</br>");
                    });
                    alert("success");
                }
            });
            return false;
        });
    });
</script>
</head>

<body>
<input type="button" id="submit" value="submit" />
<p id="d">
</p>

 

서브밋 버튼을 누르자 위와 같이 CORS 에러가 난 것을 확인할 수 있었습니다! 그런데 그 밑에 preflight라는 것도 같이 찍혔고, 해당 내용에 대해 아는바가 없어 찾아보았습니다.

 

세번째. Preflight을 파헤쳐보자

브라우저에서는 cross-origin actual request(본 요청)을 전송하기 전에, OPTIONS 메소드로 Preflight request(사전 요청)를 전송하고 요청받은 서버에서는 어떤 origin과 method에 대해 접근을 허용하는지 브라우저로 알려줍니다.

이때, 브라우저에서 요청한 orgin과 method가 혀용된 것이 확인되면, 본 요청을 하게됩니다.

아래 코드는 구글 크롬(chromium)에서 preflight 요청을 생성하는 일부 소스입니다. (해당 소스에 대해 자세히 알 필요는 없지만, 앞선 요청 로그에서 보았던 ‘access-control-request-headers’ 와 OPTIONS 메소드로 보내고 있는 부분을 확인할 수 있습니다.)

 

std::unique_ptr CreatePreflightRequest(
    const ResourceRequest& request,
    bool tainted,
    const base::Optional& devtools_request_id) {
  DCHECK(!request.url.has_username());
  DCHECK(!request.url.has_password());

  std::unique_ptr preflight_request =
      std::make_unique();

  // Algorithm step 1 through 5 of the CORS-preflight fetch,
  // <https://fetch.spec.whatwg.org/#cors-preflight-fetch>.
  preflight_request->url = request.url;
  preflight_request->method = net::HttpRequestHeaders::**kOptionsMethod;**
  preflight_request->priority = request.priority;
  preflight_request->destination = request.destination;
  preflight_request->referrer = request.referrer;
  preflight_request->referrer_policy = request.referrer_policy;
  preflight_request->mode = mojom::RequestMode::kCors;

  preflight_request->credentials_mode = mojom::CredentialsMode::kOmit;
  preflight_request->load_flags = RetrieveCacheFlags(request.load_flags);
  preflight_request->resource_type = request.resource_type;
  preflight_request->fetch_window_id = request.fetch_window_id;
  preflight_request->render_frame_id = request.render_frame_id;

  preflight_request->headers.SetHeader(net::HttpRequestHeaders::kAccept,
                                       kDefaultAcceptHeaderValue);

  preflight_request->headers.SetHeader(
      header_names::kAccessControlRequestMethod, request.method);

  std::string request_headers = CreateAccessControlRequestHeadersHeader(
      request.headers, request.is_revalidating);
  if (!request_headers.empty()) {
    preflight_request->headers.SetHeader(
        header_names::**kAccessControlRequestHeaders**, request_headers);
  }

  ...
  return preflight_request;
}

 

결국, 브라우저가 preflighted request후 리다이렉트를 지원하지 않아 오류가 발생하게 되었습니다. (참고)

(❗allowedMethods 를 ‘*’ 전체로 허용했는데, 왜 발생을 한것일까? 라는 의문점이 생겨 찾아보았더니, Access-Control-Allow-Methods: GET, PUT, POST, DELETE, HEAD만 지원을하고 있다고 합니다.)

해결

preflight request로 인한 문제임을 파악했고, 이를 해결하기 위한 방법에도 여러가지가 존재했습니다만(webconfig에서 maxAge 설정, CrossOrigin 어노테이션 설정 등..), 제가 해결한 방식을 공유하고 마무리 해보겠습니다.

인증을 하는 인터셉터에서 요청 request의 메소드가 OPTIONS로 들어올 경우, 해당 값을 true로 리턴을 시켜줌으로 에러가 떨어지지 않도록 수정하였습니다.

 

override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
      val method = request.method
      if ("OPTIONS" == method) {
          return true
      }
			...
}

 

위와 같이 설정을 한 후, 테스트를 다시 진행해보니 호출에 성공하는 것을 볼 수 있었습니다.

(혹시나 나중에 저와 같은 상황을 마주하여 당황하실 분들을 위해 부족한 내용이지만 공유 드려봅니다. 읽어주셔서 감사합니다. 🙇)