CORS 에러 정리 - boostcampwm-2022/web33-Mildo GitHub Wiki

1. 출처(Origin)란?

  • URL의 간단한 구조는 아래 그림과 같다.

    • HTTP 프로토콜에서 80번 포트는 생략 가능하다.
    • HTTPS 프로토콜에서 443번 포트는 생략 가능하다.

    url.png

  • 출처(Origin)란 URL의 구조에서 프로토콜, 호스트, 포트를 합친 것을 말한다.

    • 같은 출처의 예시는 아래의 표와 같다.

2. 동일 출처 정책(Same-Origin Policy)

  • 같은 출처(Same-Origin)란 프로토콜, 호스트, 포트가 같은 서버를 의미하며, 동일 출처 정책은 다른 출처(Origin)에서 가져온 리소스와 상호작용 하는 것을 제한하는 보안 방식이다.
  • XSS나 XSRF 등의 보안 취약점을 노린 공격을 방어할 수 있는 장점이 있다.
  • 동일-출처 정책에서 다른 출처의 리소스를 사용하기 위한 예외 조항이 CORS이다.

3. 교차 출처 리소스 공유(Cross-origin resource sharing, CORS)

  • 웹 페이지 상의 제한된 리소스를 최초 자원이 서비스된 도메인 밖의 다른 도메인으로부터 요청할 수 있게 허용하는 구조이다.
  • CORS는 크게 두 가지 방법이 존재한다.

3.1. 단순 요청 방법(Simple request)

cors_simle_request.png

  • 서버에게 바로 요청을 보내는 방법으로, 브라우저는 서버에 API를 요청하고 서버는 Access-Control-Allow-Origin헤더를 포함한 응답을 브라우저에 보낸다.
  • 브라우저는 이 헤더를 확인해서 CORS 동작을 수행할지 판단한다.
  • 단순 요청 방법은 아래의 3가지 조건을 만족해야 작동한다.
    • 요청 메서드(Request method)GET, HEAD, POST만 사용 가능하다.
    • 요청 헤더(Request header)Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width만 가능하다.
      • 그러나 사용자 인증에 주로 사용되는 Authorization 헤더가 포함되지 않는다.
    • Content-Type 헤더는 application/x-www-form-urlencoded, multipart/form-data, text/plain 만 가능하다.
      • 그러나 많은 REST API가 Content-Type 헤더에 application/json을 많이 사용한다.

3.2. 예비 요청 방법(Preflight request)

cors_preflight_request.png

  • 서버에 예비 요청을 보내서 안전한 서버인지 판단한 후 실제 요청을 보내는 방법이다.
    • 예비 요청은 OPTIONS라는 메서드를 사용한다.
  • 서버는 브라우저의 예비 요청에 대해 Access-Control-Allow-Origin 헤더를 포함한 응답을 보내고, 브라우저는 이 헤더를 확인해서 CORS 동작을 수행할지 판단한다.

4. CORS 에러 해결 방법

4.1. CORS 관련 헤더 포함 시키기

  • 요청 헤더
    • Origin : 요청하는 대상의 출처를 나타내며, API를 호출하는 페이지의 출처 값이 저장된다.
    • Access-Control-Request-Method : 실제 요청이 어떤 HTTP 메서드를 사용하는지 서버에 알려준다.
    • Access-Control-Request-Headers : 브라우저에서 보내는 요청의 커스텀 헤더 이름을 서버에 알려준다.
  • 응답 헤더
    • Access-Control-Allow-Origin : 이 헤더에 작성된 출처의 리소스 접근을 허용한다.
    • Access-Control-Allow-Credentials : 요청의 credentials가 include일 때 응답할지 나타낸다.
    • Access-Control-Expose-Headers : 브라우저의 자바스크립트에서 응답의 특정 헤더에 직접 접근할 수 있게 허용한다.
    • Access-Control-Max-Age : 예비 요청 결과를 캐시 할 수 있는 시간을 나타낸다.
    • Access-Control-Allow-Methods : 요청 헤더에 포함된 Access-Control-Request-Method 헤더에 대한 응답 결과로, 리소스 접근을 허용하는 HTTP 메서드를 지정한다.
    • Access-Control-Allow-Headers 요청 헤더에 포함된 Access-Control-Request-Headers 헤더에 대한 응답 결과이다.

4.2. 기타 해결 방법

  • cors 미들웨어 사용

    • CORS 응답 헤더를 알아서 추가해주기 때문에, 개발자가 별도로 COSR 응답 헤더를 추가해주지 않아도 된다.
  • JSONP(JSON with Padding)

    • <script> 태그가 외부 출처 리소스를 가져올 수 있는 특징을 사용하는 방법이다.

      <!-- Frontend -->
      <!DOCTYPE html>
      <html>
        <script>
          function jsonpFn (data) {
            console.log(data) // beomy
          }
        </script>
        <script
          type="application/javascript"
          src="<http://localhost:3001/cors?callback=jsonpFn>"
        >
        </script>
      </html>
      
      // Backend
      router.get('/cors', (req, res, next) => {
        res.send(`${req.query.callback}('beomy')`)
      })
      
  • 프록시 서버(Proxy server)

    350px-Open_proxy_h2g2bob.svg.png

    • 클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해 주는 컴퓨터 시스템이나 응용 프로그램을 가리킨다.
    • 서버와 클라이언트 사이에 중계기로서 대리로 통신을 수행하는 것을 프록시, 그 중계 기능을 하는 것을 프록시 서버라고 부른다.

5. Mildo 프로젝트에서

5.1. 기능

  • 기능 구현을 위해 브라우저에서 사용자의 거주 지역을 확인해야 하는데, 브라우저가 직접 확인할 수 있는 사용자 위치 정보는 위·경도 뿐이다.
  • 네이버 maps API의 Reverse Geocoding을 사용하면 위·경도 정보로 구체적인 주소 정보를 확인할 수 있다.
  • 그러나 네이버 maps API는 보안 문제로 인해 브라우저에서 직접 요청할 수 없고, server를 통해서 우회적으로 요청을 해야한다.
    • 공식 사용법에 브라우저에서 요청 시 의도적으로 cors 에러를 띄운다고 설명이 되어 있으며, API secret key를 브라우저에서 관리하면 노출되기 때문으로 보인다.

5.2. 코드

  • 우리는 CORS 에러를 해결하기 위해 다음과 같은 코드를 작성하였는데, 매번 이 에러가 발생할 때마다 해결 방법을 찾는 데만 급급해서 이것저것 방법을 다 가져다가 붙여 놓은 모습이다.

    • 가장 먼저 브라우저의 요청 헤더에 이것저것 넣어봤는데, 이건 코드가 현재는 없다.

    • 서버의 응답 헤더에 Access-Control-Allow-origin을 설정하였다.

      // server\\src\\apis\\controllers\\naver.controller.ts
      try {
            const data = await getAxiosFromNaverApi(url);
            if (process.env.CLIENT_URL) {
              res.setHeader('Access-Control-Allow-origin', process.env.CLIENT_URL);
            }
            res.status(200).send(data);
          } catch (error) {
            console.log(error);
            res.status(500).send({
              ok: false,
              message: '좌표값을 기반으로 주소를 가져오지 못했습니다! :('
            });
          }
      
    • axios 요청 헤더에 credentials를 사용하였다.

      // client\\src\\apis\\axios.ts
      axios.defaults.withCredentials = true;
      // 나중에 .env로 빼내어야 함
      const serverURL = '<http://localhost:3001>';
      

      const fetchGeocodeFromCoords = async (lat: number, lng: number) => { let data;

      try { if (lat && lng) { data = await axios.get(${serverURL}/api/naver?lng=${lng}&amp;lat=${lat}); } } catch (error) { console.log(error); }

      return data?.data.results[0].region.area1.name; };

    • 다 안되다가 cors 미들웨어에 이런 저런 옵션을 다 설정해주니 잘 되었다.

      // server\\src\\app.ts
      app.use(cors({ origin: process.env.CLIENT_URL, credentials: true }));
      
  • 그러나 최초 에러 메시지는 No ‘Access-Control-Allow-Origin’ header is present…였는데 너무 많이 잡다한 코드를 넣었다.

    오리진.PNG

    • 지금도 확인해보니 서버의 응답 헤더에 Access-Control-Allow-origin만 넣어주면 되는데 COSR 에러를 제대로 이해하지 못하니 이것 저것 건드리니까 더 헤맨 것으로 보인다.
  • axios에서 withCredentials를 사용하면 서버 응답 헤더에 Access-Control-Allow-Headers를 넣어주어야 하는데 개념 자체를 잘 모르니까 아래 에러도 떴었다.

    크레덴셜.PNG

    • cors 미들웨어의 옵션으로 credential을 추가하니까 해결됐는데 이게 위 방법이랑 똑같은 것이다.

5.3. 결론

  • 개별 서버 응답 헤더에 Access-Control-Allow-origin만 설정해도 잘 돌아간다.
  • 아니면 cors 미들웨어를 사용해서 전체 응답 헤더에 위 헤더를 넣어주면 된다.
  • credential은 무조건 지우고 위 두 방법 중 하나만 살려두자!
  • 해결 방법만 급급하게 찾으려고 하지 말고 개념을 좀 잘 이해하자~

5.4. 출처

⚠️ **GitHub.com Fallback** ⚠️