React 10 :: Nginx + Docker-Compose 解决 CORS问题

1,682 阅读7分钟

前言

相信如果是做H5的小伙伴们应该对CORS都不陌生,我们今天这篇文章就来聊一聊如何通过两种简单的方法实现CORS

话不多说,先share一下今天的Repo: gitlab.com/yafeya/reac…

Repo的几点说明:

  • 技术栈
    • Asp.Net Core作为REST API Server
    • React作为Client
    • nginx作为反向代理服务器
    • DockerDocker-Compose作为运行环境
  • 需要提前安装的软件
    • git
    • dotnet core sdk
    • node & npm
    • create-react-app
    • docker & docker-compose

1. 什么是CORS

CORS = Cross Origin resource sharing, 叫做跨域资源共享。虽然原文直译应该叫跨源,但是按照流行的叫法一般都叫做跨域。

1.1 跨域 & 同域

为了理解什么叫做CORS,我们可以首先了解什么叫做同域或同源。

其实,同域是AJAX的一个标准,这是浏览器安全的一个保障。下面的内容摘自阮一峰老师的文章

最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"。

  • 协议相同
  • 域名相同
  • 端口相同

随着互联网的发展,"同源政策"越来越严格。目前,如果非同源,共有三种行为受到限制。

  • Cookie、LocalStorage 和 IndexDB 无法读取。
  • DOM 无法获得。
  • AJAX 请求不能发送。

1.2 跨域的场景

我们可以举例说明跨域的场景:假如我们的网站发布在http://www.yafeya.com这个网址上,那么我们可以得到我们的网站具有如下的信息

项目
协议http
域名www.yafeya.com
端口80

那么以下场景的跨域情况

  • http://www.yafeya.com/api/weatherforecast 同域
  • https://www.yafeya.com/api/wetherforecast 跨域, 协议 & 端口不同 (https默认443端口)
  • http://yafeya.com/api/weatherforecast 跨域,域名不同
  • http://www.yafeya.com:9981/api/weatherforecast 跨域端口不同

所以只有第一个网址下面的网页可以与http://www.yafeya.com下的网页可以实现资源共享。

1.3 AJAX的同源策略

相信很多小伙伴已经注意到了一个细节,这个同源策略是个AJAX的策略,换句话说,他只存在于javascript的调用上,对于后端的技术没有限制。

其实对于后端的限制,是通过其他的技术,类似于authenticationjwt来实现的。简单来说,就是将credentials存放在一个jsontoken中,在访问api时需要出示这个令牌,来获取资源。我们曾经在strapi的系列中简单提到过这个模式,有兴趣的小伙伴们可以去考考古。

文章地址:juejin.cn/post/696567…

2. Enable CORS的两种简单方法

一般大家处理CORS高效的方法基本上两种:

  • 在服务端Enable All Origin或添加whitelist
  • H5REST Api Server通过反向代理代理到同一个Domain

下面我们通过我们的Demo对这两种方法分别进行介绍。

3. ServerAllow Origins

3.1 方法

在我们的Demo中,可以通过下面的代码将Demo运行在Development环境下,在Development环境下,是通过Allow All Orgins实现的CORS

# in WebApiServer folder
dotnet run --launch-profile WebApiServer
# in web-api-client folder
yarn start

Server端,在Development环境下,允许所有Origin, Header, 与Method。其实设置Whitelist也差不多,主要就是设置一个CorsPolicy,并应用。我们这里就不再演示代码了。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        // Allow all origin, header & method in Development Env.
        app.UseCors(builder =>
            builder.AllowAnyOrigin()
                   .AllowAnyHeader()
                   .AllowAnyMethod());
    }
    // ...
}

Client端,其实就是在.env.development文件中设置好要链接的Development环境的Server Api的地址。

# .env.development
REACT_APP_API_URL=http://localhost:5000/WeatherForecast

由于在Server端设置了AllowAnyOrigin所以,我们在Client端调用API时,能够绕开同源Policy

3.2 问题

  • Allow Any Origin

    使用这种方法,会使得其他Unexpect Domain也可以访问我们的资源。这是我们不希望的,会造成一些安全性的问题。

  • 设置whitelist

    使用这种方法,也会有一些问题,因为如果我们更换部署的URL,那我们的代码会经常修改,即使我们写在configuration里面,总是修改也是非常头疼的。

    我个人认为,代码主要解决业务逻辑的事情,但是部署属于Devops的活,代码应该专注于解决业务逻辑,而不应该被Devops牵着鼻子走。

3.3 解决方案

所以,基于上面两点,我们引入了下面一种解决方案,如果我们把Api ServerClient部署在同一个域名下面,相当于就没有CORS的问题了,所以我们引入了Nginx来解决这个问题。

4. 使用反向代理部署同域网站

4.1 网络结构

下图为整个Demo的网络结构:

whole.png

其中,REST API Client的部分,沿用了上一篇文章中的部署方式。其实还有另一种部署方式,就是将React-Demo的编译结果过dist目录放到上面Nginx Gatewayroot目录中,但是,本文的重点在于简述CORS的解决方法,而不在于Nginx的部署方式。并且,现在这种部署方式隔离性更强,每个Docker Container更为独立,便于是说明问题。

client.png

4.2 请求过程

上面的网络结构图中,我用数字标出了用户从浏览器中请求网页时,RequestResponse的过程。假设我们的Nginx Gateway部署在 https://my-domain 这个域名上面。

    1. 用户在浏览器中访问这个URL, https://my-domain
    1. Nginx Gateway将这个Requst转发给React-Demo这个docker-container
    1. React-Demo在网页中,对Rest-Api-Server这个docker-container进行REST-API请求
      # .env文件中配置的api地址如下
      REACT_APP_API_URL=https://some-domain/api/WeatherForecast
      

      正如上面代码所示,在React-Demo网页中对https://some-domain/api/WeatherForecast的请求,会被Nginx-Gateway反向代理到http://web-api-server上,但是以React-Demo这个观察者的视角,其实访问的是 https://some-domain 的资源,所以,浏览器会认为我们访问的是同域的资源,这样就绕开了同源policy的检查。

    1. http://web-api-serverAPIresponse返回给React-Demo
    1. React-Demo将网页Dom作为Response回传给Nginx-Gateway
    1. Nginx-Gateway将网页DomResponse返回给浏览器,浏览器根据Dom Response渲染网页呈现给用户

4.3 docker-compose.yaml

下面我们来看看docker-compose.yaml的实现。我们的docker-compose中存在3container或者说service,docker-compose的作用是将这三个service组成一个局域网,每个service可以通过service-name来互相访问。

web-api-serverweb-api-client作为两个service可以被nginx-gateway访问,在gateway的设置中,将浏览器对 https://some-domainhttps://some-domain/api 的访问反向代理到这两个service中。下面我们来看看nginx-gateway的设置。

version: '3'
services:
  web-api-server:
    image: yafeya/web-api-server
    environment:
      ASPNETCORE_ENVIRONMENT: Production
    restart: always
    ports: 
      - '5000:80'

  web-api-client:
    image: yafeya/react-demo
    restart: always
    ports:
      - '9981:80'
    depends_on:
      - web-api-server

  nginx-gateway:
    image: nginx:stable-alpine
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - /etc/ssl:/etc/ssl
    ports:
      - 80:80
      - 443:443
    restart: always

4.4 Nginx.conf

server {
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name _;

    ssl_certificate           /etc/ssl/fullchain.pem;
    ssl_certificate_key       /etc/ssl/private.pem;

    ssl on;
    ssl_session_cache  builtin:1000  shared:SSL:10m;
    ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
    ssl_prefer_server_ciphers on;

    # 对 https://some-domain 的请求将被反向代理到 http://web-api-client
    location / {
        proxy_pass  http://web-api-client;
    }

    # 对 https://some-domain/api 的请求将被反向代理到 http://web-api-server
    location /api {
        proxy_pass  http://web-api-server;
    } 

}

对于 https://some-domain/api 的请求,nginx-gateway反向代理到了 http://web-api-server 上,注意,这里没有以 / 结尾,也就是 http://web-api-server/,这说明,我们要是用相对路径,也就是说,我们的Api要能通过http://web-api-server/api/*被访问到。

对于asp.net core来说,为了实现这一点,我们在代码中加入了下面代码。这样,相当于在整个网站的路由中加入了子目录api

app.UsePathBase($"/api");

4.5 启动生产环境

执行以下代码,将会启动docker-compose,并将所有container运行起来。

# repo folder
./build
docker-compose up -d