본문 바로가기
백엔드 개발/백엔드 일기

#021. nginx의 reverse proxy로 cors 에러 해결하기

by iamjoy 2022. 9. 18.

github 코드: https://github.com/erie0210/cors-error

요약

nginx의 reverse proxy로 cors 에러 해결한다

goal(목표)

cors 에러가 나는 이유를 이해한다.
네트워크 레벨에서 cors 에러를 해결한다.
proxy, forward proxy, reverse proxy를 이해한다.

Non-goal

cors에러를 코드 레벨에서 해결

plan

 

플로우 다이어그램

서버 내부 구현

1.  cors 에러 발생시키기
CORS(Cross-Origin Resource Sharing)는 서버가 자기 자신, origin에 지정되어있는 게 아닌 다른 출처(domain or port)에서 브라우저가 리소스 로딩을 하지 않도록 하는 HTTP 헤더 기반 매커니즘이다. 쉽게 말해 response의 header를 열어봐서 허용되어있는 origin이 아니면 브라우저 차원에서 차단하는 기능이다.
브라우저는 OSI 7계층의 5계층인 session layer까지 본다. port는 4계층(transport layer)에 속하기 때문에 같은 localhost라고 하더라도 port가 다르면 브라우저는 서로 다른 origin이라고 이해한다. 그래서 아래와 같이 localhost: 3000에 react, localhost:5000에 nest가 작동하면 브라우저는 서로 다른 origin이라고 생각해 CORS에러를 낸다.

// App.js

import logo from './logo.svg';
import './App.css';
import axios from "axios";
import { useEffect,useState } from "react";

function App() {
  const [msg,setMsg] = useState("")

  useEffect(()=>{
    getHello()
  },[])

  const getHello = async () => {
    const res = await axios.get('http://localhost:5000/api/v1')
    setMsg(res.data)
  }

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          {msg}
        </p>
      </header>
    </div>
  );
}

export default App;

이와 같이 5000 번 서버를 바라보게 하면 아래 같이 에러가 난다.


2. nginx를 사용해 proxy 설정하기 (reverse-proxy)
cors 에러를 해결하는 데에 코드적 접근네트워크 적인 접근 크게 두 가지 방법이 있다.

코드적 접근 방법: 요청을 보내는 쪽의 header에 서용할 Origin 정보를 입력하는 것이다.
네트워트적인 접근: proxy를 설치해 router를 보고 load-balancing을 하는 방법이다.
여기에서는 네트워크적인 접근을 해보려고 하고, config를 작성해 그 config를 사용한 docker nginx를 사용하려고 한다.

먼저 proxy로 nginx를 설치하고 nginx config를 다음과 같이 작성한다.

// ./nginx/nginx.conf

user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server {
        listen 80; -------------------------------> nginx를 80 포트에 연다
        server_name localhost; -------------------> nginx의 domain 이름을 지정한다.

        location /api/v1 { -------------------------------> 이런 router로 시작하면
            proxy_pass http://host.docker.internal:5000; ---> 이 도커를 띄운 컴퓨터 localhost 의 5000번으로 가게 한다.
        }

        location / { -------------------------------> 그 외 router는 3000번으로 가게 한다.
            proxy_pass         http://host.docker.internal:3000;
        }
    }
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}

다시 말해 /api/v1 으로 시작하는 router는 nest서버로 보내고 그 외는 ract를 보게 한다.
nginx는 80포트를 보고 있기 때문에 http://localhost + ~~~ 와 같이 port를 열지 않은 http 요청은 모두 nginx로 들어오게 된다.
nginx는 이렇게 들어온 값들의 router를 보고서 정해진대로 맞는 port에 전달(load balancing)하게 된다.
이를 위해 react의 데이터를 불러오는 코드를 다음과 같이 고친다. 5000번 nest서버를 바라보던 것을 nginx를 바라보게 수정하는 것이다.

  const getHello = async () => {
    const res = await axios.get('http://localhost/api/v1')
    setMsg(res.data)
  }

이렇게 수정한 후 위에서 작성한 nginx.conf를 config로 사용하는 docker를 띄우기 위해 루트에 다음과 같이 docker-compose.yml을 작성한다. 그리고 $ docker-compose up -d를 통해 서버를 띄운다.

// docker-compose.yml

version: '3.7'

services:
  local-dev-proxy:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro

그리고서 다시 react를 켜보는데 이번에는 localhost:3000이 아니라 localhost로 들어간다. router가 '/'이므로 react를 바라보게 되고, react에서 서버를 바라보는 요청은 http://localhost/api/v1이므로 다시 nginx가 nest 서버로 load balancing한다.



3. proxy: forward proxy 와 reverse proxy
위의 매커니즘을 보면 nginx가 router를 보고 port를 다시 지정해서 요청을 전달한다. 유저 입장에서는 localhost:80만 바라보게 되므로 보낸 응답에 대한 response 값이 서버의 응답이 어디에서 오는 지 (port 3000인지, 5000인지) 알 수 없게된다.

이와 같이 서버를 가리는 경우를 reverse proxy라고 한다. router path(OSI application layer(7))을 보고 port(OSI application layer(4))의 header 정보를 붙인다. 대표적인 예시로 Load Balancing이 있다.

여기서 reverse라는 개념은 forward proxy 와 대비되는 개념으로 이름이 붙여진 것인데, forward proxy는 클라이언트를 가린다. 대표적인 예시로 CDN이 있는데 유저가 클라이언트에게 요청을 보내면 클라이언트는 CDN에 올려진 static code를 받아서 사용하기 때문에 클라이언트 서버와 직접 통신하는 것이 아니라 CDN에 캐싱된 데이터를 내려받는다. 이 과정에서 CDN이 proxy로 사용된다고 할 수 있다.


+

코드로 cors 에러를 해결하는 방법:
nest에서는 cors를 express-cors를 기반으로 코드 레벨에서 해결할 수 있도록 지원하고 있다.
아래와 같이 CorsOption을 사용해서 어떤 header의 내용을 믿을 수 있는 지 설정할 수 있다.

export interface CorsOptions {
    /**
     * Configures the `Access-Control-Allow-Origins` CORS header.  See [here for more detail.](https://github.com/expressjs/cors#configuration-options)
     */
    origin?: StaticOrigin | CustomOrigin;
    /**
     * Configures the Access-Control-Allow-Methods CORS header.
     */
    methods?: string | string[];
    /**
     * Configures the Access-Control-Allow-Headers CORS header.
     */
    allowedHeaders?: string | string[];
    /**
     * Configures the Access-Control-Expose-Headers CORS header.
     */
    exposedHeaders?: string | string[];
    /**
     * Configures the Access-Control-Allow-Credentials CORS header.
     */
    credentials?: boolean;
    /**
     * Configures the Access-Control-Max-Age CORS header.
     */
    maxAge?: number;
    /**
     * Whether to pass the CORS preflight response to the next handler.
     */
    preflightContinue?: boolean;
    /**
     * Provides a status code to use for successful OPTIONS requests.
     */
    optionsSuccessStatus?: number;
}



TBD

x-forwarede-for

Reference

https://github.com/expressjs/cors#configuration-options

 

GitHub - expressjs/cors: Node.js CORS middleware

Node.js CORS middleware. Contribute to expressjs/cors development by creating an account on GitHub.

github.com

https://github.com/erie0210/cors-error

 

GitHub - erie0210/cors-error: cors 에러 해결하는 과정 예시 코드

cors 에러 해결하는 과정 예시 코드. Contribute to erie0210/cors-error development by creating an account on GitHub.

github.com