前言:很多人都知道浏览器的跨域问题,以为这是前端工程师的事情,但其实跨域问题不仅是前端工程师需要关注的问题,后端工程师也需要对其有一定的了解,并知道其原因和解决方法。
什么是跨域问题
-
同源策略
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能。此策略限制了浏览器对不同源对不同源的脚本或文本的访问方式进行了限制。所以其是对浏览器的限制。
如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
受到同源限制:
1)无法读取不同源的 Cookie、LocalStorage 和 IndexDB 。
2)无法获得不同源的DOM 。
3)不能向不同源的服务器发送ajax请求。
这些都是基于浏览器安全考虑的限制。其实Ajax 跨域请求,在服务器端不会有任何问题,只是服务端响应数据返回给浏览器的时候,浏览器根据响应头的Access-Control-Allow-Origin字段的值来判断是否有权限获取数据。
在浏览器中"script、img、iframe、link"等标签都可以跨域加载资源,而不受同源策略的限制。所以就有人通过这些标签请求来绕过同源策略的限制,JSONP就是如此,当然这也是不安全的。
-
跨域问题
前面说了只要不满足同源策略的请求,都属于跨域,即不满足“协议,域名,端口”中的一者或多者。举例说明的话,会有以下这么多情况:
URL | 说明 | 是否允许通信 |
---|---|---|
http://www.domain.com/a.js http://www.domain.com/b.js | 同一域名,不同文件或路径 | 允许 |
http://www.domain.com/b.js http://www.domain.com:8000/a.js | 同一域名,不同端口 | 不允许 |
http://www.domain.com/a.js https://www.domain.com/b.js | 同一域名,不同协议 | 不允许 |
http://www.domain1.com/a.js http://www.domain2.com/a.js | 域名1和域名2不同,不管是否指向同一个IP | 不允许 |
http://a.domain.com/a.js http://b.domain.com/b.js | 主域相同,子域不同 | 不允许 |
跨域的几种解决方法
既然跨域问题是因为浏览器的同源策略,那么解决跨域就需要从同源策略的几个限制条件着手。包括改变域名,改变请求头,改变请求方法等。
-
jsonp:只支持 GET,不支持 POST 请求,不安全 XSS
-
cors:需要后台配合进行相关的设置
-
postMessage:配合使用 iframe,需要兼容 IE6、7、8、9
-
document.domain:仅限于同一域名下的子域
-
websocket:需要后台配合修改协议,不兼容,需要使用 http://socket.io
-
proxy:使用代理去避开跨域请求,需要修改 nginx、apache 等的配置
常见的跨域解决方法一般就是上面几种,古老的jsonp就是通过同源策略有序的scrip标签请求来避过跨域;websock通过服务端和前端建立通信链路来解决跨域问题;proxy代理方法则是通过代理转发功能来将不同的域名转发到相同的应用上来实现。
本文主要说的是后端需要掌握的CORS,通过改变请求头和方法来实现跨域,实现CORS通信的关键是服务器端。只要服务器实现了CORS接口,就可以跨源通信。
CORS机制
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下三大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
-
HEAD
-
GET
-
POST
(2)HTTP的头信息不超出以下几种字段:
除了被用户代理自动设置的首部字段(例如 Connection ,User-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:
-
Accept
-
Accept-Language
-
Content-Language
-
Content-Type (需要注意额外的限制)
-
DPR
-
Downlink
-
Save-Data
-
Viewport-Width
-
Width
(3)Content-Type 的值仅限于下列三者之一:
-
text/plain
-
multipart/form-data
-
application/x-www-form-urlencoded
凡是不同时满足上面三个条件,就属于非简单请求。为什么要区分简单请求和非简单请求呢?因为浏览器对这两种请求的处理是不一样的。
1,简单请求
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段,用来说明请求来自哪个源。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。
注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.test.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
这里我们主要关注Access-Control-Allow-Origin这个字段,只有源请求的域名在此集合中才能访问正常返回数据。通过此集合的设置可以在满足跨域请求的同时一定程度上保证网站的安全性,比如只让test.com下的二级域名访问。
2,非简单请求
如果不满足我们上文提到的三个条件,那么这个请求就是非简单请求。对于非简单请求,浏览器需要在正式请求前做一次预检请求。
“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
比如下面这个请求:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
function callOtherDomain(){
if(invocation)
{
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
......
因为这是一个发送application/xml的post请求,而且该请求包含了一个自定义的请求首部字段(X-PINGOTHER: pingpong。所以该非简单请求,需要先执行一次预检请求,看我们的服务端是否允许这次请求。
预检请求通过后,浏览器才会发送真正的请求,如果没通过则不会发送真正的请求。
预检请求中同时携带了下面两个首部字段:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。
首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。
服务器收到options的预检请求后,会在access-control-allow-*字段返回允许的一些操作:
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
首部字段 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求。该字段与 HTTP/1.1 Allow: response header 类似,但仅限于在需要访问控制的场景中使用。
首部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type。与 Access-Control-Allow-Methods 一样,Access-Control-Allow-Headers 的值为逗号分割的列表。
此返回说明预检请求通过,那么就可以发送真正的post请求了。至此跨域问题就解决了。
我们也可以看一下segmentfault.com也是通过这种方式来解决跨域的,segmentfault.com的域名下却可以获取到gateway.segmentfault.com下的数据。
至此,你知道后端如何通过CORS来解决跨域问题了吗?
举个栗子:
现在我要通过domaina.com请求到domainb.com的数据。所以肯定要在allow-origin中增加domaina,而且因为需要预检请求,所以需要将options方法设为可通过。如果还需要http请求中带上cookie,需要前后端都设置credentials.
ctx.set('Access-Control-Allow-Origin', 'http://domaina.com')
ctx.set('Access-Control-Allow-Credentials', true)
ctx.set('Access-Control-Request-Method', 'PUT,POST,GET,DELETE,OPTIONS')
ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, t')
至此,跨域问题就很简单的解决啦
往期推荐