在web开发中,有时会遇到如下错误:
Access to XMLHttpRequest has been blocked by cors policy
这个错误经常出现在新项目刚开始的时候,处理的方式一般有两种:
在浏览器上装个跨域插件或跑个本地代理,适用于本地调试。
调整服务端配置,从根本上解决。
这是个老生常谈的跨域问题,今天我们来重新认识一下。
一个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是一种机制,该机制使用附加的 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请求到服务器,确认是否可以发送实际请求,确认允许后再发送实际请求。
在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
从字面上就能看出各字段的作用,这里就不多赘述了。
用户发起请求,web服务端响应请求。
图中的web服务指用PHP,Node Js,Java,Python,Go等语言开发的应用。在生产环境中一般会在这些服务前挂一个反向代理,比如Nginx。用户发起请求,先到反向代理,之后到后端的web服务。
在响应的HTTP头部加上CORS字段即可实现跨域。以上图为例,可以在Nginx或后端web服务上配置。
Nginx使用add_header指令添加,后端语言也都有相应的添加方式,比如PHP,可以直接使用header函数添加。
比如IE7,IE8。不过现在基本不会用这些浏览器了,这个问题可以忽略。
现在的后端框架组件都很齐全,基本都带了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上配置。
比如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有简单请求和预检请求,在HTTP头部加上CORS的相关字段即可实现。
在介绍CORS时提到了反向代理,跨域的解决方案中有一个方案也是代理。这两个代理是不同的,需注意下。