前言
相信如果是做H5
的小伙伴们应该对CORS
都不陌生,我们今天这篇文章就来聊一聊如何通过两种简单的方法实现CORS
。
话不多说,先share
一下今天的Repo
:
gitlab.com/yafeya/reac…
对Repo
的几点说明:
- 技术栈
Asp.Net Core
作为REST API Server
React
作为Client
nginx
作为反向代理服务器Docker
和Docker-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
的调用上,对于后端的技术没有限制。
其实对于后端的限制,是通过其他的技术,类似于authentication
和jwt
来实现的。简单来说,就是将credentials
存放在一个json
的token
中,在访问api
时需要出示这个令牌,来获取资源。我们曾经在strapi
的系列中简单提到过这个模式,有兴趣的小伙伴们可以去考考古。
2. Enable CORS的两种简单方法
一般大家处理CORS
高效的方法基本上两种:
- 在服务端
Enable All Origin
或添加whitelist
- 将
H5
和REST Api Server
通过反向代理代理到同一个Domain
下
下面我们通过我们的Demo对这两种方法分别进行介绍。
3. Server
端Allow 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
也差不多,主要就是设置一个Cors
的Policy
,并应用。我们这里就不再演示代码了。
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 Server
和Client
部署在同一个域名下面,相当于就没有CORS
的问题了,所以我们引入了Nginx
来解决这个问题。
4. 使用反向代理部署同域网站
4.1 网络结构
下图为整个Demo
的网络结构:
其中,REST API Client
的部分,沿用了上一篇文章中的部署方式。其实还有另一种部署方式,就是将React-Demo
的编译结果过dist
目录放到上面Nginx Gateway
的root
目录中,但是,本文的重点在于简述CORS
的解决方法,而不在于Nginx
的部署方式。并且,现在这种部署方式隔离性更强,每个Docker Container
更为独立,便于是说明问题。
4.2 请求过程
上面的网络结构图中,我用数字标出了用户从浏览器中请求网页时,Request
和Response
的过程。假设我们的Nginx Gateway
部署在 https://my-domain 这个域名上面。
-
- 用户在浏览器中访问这个
URL
, https://my-domain
- 用户在浏览器中访问这个
-
Nginx Gateway
将这个Requst
转发给React-Demo
这个docker-container
-
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
的检查。
-
http://web-api-server
将API
的response
返回给React-Demo
-
React-Demo
将网页Dom
作为Response
回传给Nginx-Gateway
-
Nginx-Gateway
将网页Dom
的Response
返回给浏览器,浏览器根据Dom Response
渲染网页呈现给用户
4.3 docker-compose.yaml
下面我们来看看docker-compose.yaml
的实现。我们的docker-compose
中存在3个container
或者说service
,docker-compose的作用是将这三个service
组成一个局域网,每个service
可以通过service-name
来互相访问。
web-api-server
和web-api-client
作为两个service
可以被nginx-gateway
访问,在gateway
的设置中,将浏览器对 https://some-domain 与 https://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