# 前言

在web开发中,有时会遇到如下错误:

Access to XMLHttpRequest has been blocked by cors policy

这个错误经常出现在新项目刚开始的时候,处理的方式一般有两种:

1. 在浏览器上装个跨域插件或跑个本地代理,适用于本地调试。
2. 调整服务端配置,从根本上解决。

这是个老生常谈的跨域问题,今天我们来重新认识一下。

# 什么是跨域

一个URL的组成可以简化为四部分:协议,域名,端口号,路径。

格式为:协议://域名:端口号/路径。

比如地址: https://www.a.cn/tools/gitlab-runner.html

协议是https,域名是www.a.cn,端口号是443(浏览器默认不显示443端口号),路径是tools/gitlab-runner.html。

浏览器有个同源策略:对于两个URL,如果协议,域名,端口号都相同时,则这两个URL是同源的,如果有不同的则是不同源。两个不同源的URL之间的访问叫跨源访问,也就是我们说的跨域访问,简称跨域。

跨域时,使用XMLHttpRequest,Fetch API等会受到约束,出现类似"Access to xmlhttprequest at has been blocked by cors policy"的错误,这个问题是我们说的跨域问题了。

# 为什么要处理跨域问题

浏览器为了更安全,好心好意设计了同源策略,为什么我们还要突破这个限制?因为在实际使用中存在必须跨域的情况,举两个常见的例子:

# 本地开发跨域

在本地的开发环境中,前后端的地址一般是不一致的。

比如前端地址为http://192.168.103.5:8080

后端接口地址为http://192.168.10.6/api

这两个地址的ip和端口不一样,前端请求后端接口时就会出现跨域问题。

# 线上接口跨域

比如有个页面地址为http://a.company.com

页面需调用A接口http://api.company.com/a

这两个地址的完整域名不一致,在页面中调用A接口时会出现跨域问题。

# 如何解决跨域问题

目前的方案可归类为四种:

使用支持跨域的API,如window.postMessage
使用JSONP
使用代理
使用CORS机制

由于方案1和方案2的限制较多,在前后端分离的项目中基本不会采用。

以JSONP为例,它只支持GET请求,需要接口输出JavaScript代码。正经的API会使用RESTful规范或类似的标准,JSONP显然不适合。

# 使用代理

跨域问题是浏览器搞出来的,在服务端访问则没这个问题,所以我们可以在服务端加一层代理,让前端请求代理地址。

假设应用A地址为 http://192.168.103.5:808

接口地址为为 http://192.168.10.6/api

则可以在应用A上加一层代理,前端接口地址改为http://192.168.103.5:8080/api,如图:

代理可以用Nginx,Apache,Node Js等,挑个自己喜欢的即可。比如用Nginx可配置如下:

server {
        listen 8080;
        server_name 192.168.103.5;
        location /api {
            proxy_pass http://192.168.10.6/api;
        }
}

# CORS机制

有没有一种对架构没有入侵性,更通用的方案呢?答案就是CORS。

CORS是一种机制,该机制使用附加的 HTTP 头来告诉浏览器,准许运行在一个源上的Web应用访问位于另一不同源选定的资源。即我们可以在HTTP头部添加CORS字段,让浏览器允许跨域。

当请求的HTTP头部包含CORS字段时,该请求就是CORS请求。CORS有简单请求和预检请求。

# 简单请求

简单请求指客户端发送请求给服务器后,服务器直接返回内容。

简单请求需满足如下条件:

请求方法只能是GET、HEAD、POST中的一个

人为设置的header字段只能是Accept、Accept-Language、Content-Language 、Content-Type、DPR、Downlink、Save-Data、Viewport-Width、Width

Content-Type字段的值只能是text/plain、multipart/form-data、application/x-www-form-urlencoded中的一个

请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器

请求中没有使用 ReadableStream 对象

# 预检请求

不满足简单请求的条件时,会触发预检请求:浏览器会先发一个option请求到服务器,确认是否可以发送实际请求,确认允许后再发送实际请求。

# CORS字段

在HTTP的请求和响应中都有CORS字段。

CORS的请求头部(Request Hearders)字段如下:

Origin

Access-Control-Request-Method

Access-Control-Request-Headers

这三个字段是自动加的,在option请求中就可以看到,如图:

CORS的响应头部(Response Hearders)字段如下:

Access-Control-Allow-Origin
Access-Control-Expose-Headers
Access-Control-Max-Age
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Access-Control-Allow-Headers

从字面上就能看出各字段的作用,这里就不多赘述了。

# CORS配置

一个简化的web服务架构如下:

用户发起请求,web服务端响应请求。

图中的web服务指用PHP,Node Js,Java,Python,Go等语言开发的应用。在生产环境中一般会在这些服务前挂一个反向代理,比如Nginx。用户发起请求,先到反向代理,之后到后端的web服务。

此时请求的响应链路是由后端web服务到反向代理,再到用户。

在响应的HTTP头部加上CORS字段即可实现跨域。以上图为例,可以在Nginx或后端web服务上配置。

Nginx使用add_header指令添加,后端语言也都有相应的添加方式,比如PHP,可以直接使用header函数添加。

# CORS实践

# 1. 一些老旧的浏览器不支持CORS

比如IE7,IE8。不过现在基本不会用这些浏览器了,这个问题可以忽略。

# 2. 在配置CORS跨域时,只在一处配置

现在的后端框架组件都很齐全,基本都带了CORS组件,一行代码就能使用CORS了。但需注意的是如果整个链路有多处节点,则只在一处配置即可,避免多处配置,造成冲突。

以上图Nginx反向代理为例,假如在Nginx上配置了CORS:

add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
    return 204;
}

且后端web服务上也加了CORS的响应头:

header("Access-Control-Allow-Origin:*")

则最终响应头部的Access-Control-Allow-Origin字段值可能会变成*.*,导致无法达到想要的跨域效果。

正确的处理方式是只在一处配置,比如只在Nginx上配置。

# 3. 成功状态码有CORS字段,其他状态码没有

比如Nginx,当响应状态码是4XX,5XX时,即使配置了CORS,头部也没有CORS相关字段,此时可以加一个always参数:

add_header Access-Control-Allow-Origin '*' always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
if ($request_method = 'OPTIONS') {
    return 204;
}

添加该参数后,其他的状态码下也有CORS字段了。

# 总结

跨域是浏览器的问题,在其他端不存在这个问题。

处理这个问题最常用的方式是CORS机制。CORS有简单请求和预检请求,在HTTP头部加上CORS的相关字段即可实现。

在介绍CORS时提到了反向代理,跨域的解决方案中有一个方案也是代理。这两个代理是不同的,需注意下。