最近学习node,写了一个简单的demo:后端使用node的http模块来监听http请求,主要负责数据输出,而前端则负责请求后台api得到数据,最终渲染到页面。
所以当浏览器加载一个页面时,会有两次http请求,一次请求页面,即index.html,另外一次为请求api数据,数据为一个json对象。最终通过原生js将数据渲染到页面中。
问题出现
看着非常简单的一个案例,却发生了一个开发过程中非常经典的问题,即浏览器的同源策略阻止了跨域请求资源。
服务器运行在http://121.249.216.190:8030
,而前端js运行在http://127.0.0.1:8020
,所以当在前端js使用ajax方法来请求服务器数据时,浏览器就会抛出阻止跨源请求的错误。
解决跨域问题的方式有很多种,比如架设代理服务器、jsonp等。这次使用了CORS来解决这个问题。最终demo的服务器代码在我的github上,点击这里可以访问到。
什么是跨域
一张图来解释什么是跨域:
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。
不同域间相互请求资源,就算做跨域。
出于安全考虑,浏览器会限制脚本中发起的跨域请求。比如,使用XMLHttpRequest对象发起http请求就必须遵守同源策略。
注意:浏览器并非限制了http请求的发起,跨域请求可以正常发起,但是返回结果会被浏览器拦截。
CORS,跨域资源共享,即Cross-Origin Resource Sharing,是W3C推荐的一种新的机制来让Web应用服务器能支持跨域访问控制,它的侧重点在服务器,换言之,CORS的核心就是让服务器来确定是否允许跨域访问。
跨源资源共享标准使得以下应用场景可以使用跨域http请求:
- 使用XMLHttpRequest发起的跨域http请求
- Web字体(CSS中通过@font-face使用跨域字体资源)
典型访问控制场景
简单请求
所谓的简单请求,需要同时满足以下两个条件:
- 只使用GET、HEAD或者POST请求方法。如果使用POST向服务器传送数据,则数据类型(Content-Type)只能是application/x-www-form-urlencoded、multipart/form-data或者text/plain一种。
- 不使用自定义请求头(比如X-TOKEN)。
Server代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 'use strict'; var http = require('http'); var xwj = { name: 'xwj', age: 21, school: 'OUC', love: 'play basketball' }; http.createServer (function (request, response) { response.setHeader('Access-Control-Allow-Origin', '*'); response.write(JSON.stringify(xwj)); response.end(); }).listen(8030, function () { console.log('Server has stared...') });
|
Clinet代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(responseText); } } } xhr.open('GET', 'http://121.249.216.190:8030', true); xhr.send(null); xhr.open('POST', 'http://121.249.190:8030', true); xhr.setRequestHeader('Content-Type', 'text/plain') xhr.send(null); xhr.open('DELETE', 'http://121.249.216.190:8030', true); xhr.send(null); xhr.open('POST', 'http://121.249.216.190:8030', true); xhr.setRequestHeader('X-TOKEN', 'abc'); xhr.send(null);
|
预请求
下面我们来探究场景3和场景4跨域请求失败的原因,比如场景4,因为我们加了一个自定义的请求头,所以导致跨域请求失败,而浏览器也告知了我们原因。
但我们打开网络面板时,可以看到多了一次OPTIONS请求。
OPTIONS请求是HTTP/1.1中的方法,用来获取更多的服务器信息,是一个不应该对服务器数据造成影响的方法。
并且在OPTIONS请求的请求头中,有以下两个字段:
1 2
| Access-Control-Request-Method: POST Access-Control-Request-Headers: X-TOKEN
|
OPTIONS请求就是预请求发送的。
不同于上面的简单请求,预请求要求必须先发送一个OPTIONS请求给目的站点,来查明这个跨域请求对于目的站点是否安全可接受。
当具备以下任何一个条件,这个请求就会被当做预请求处理:
- 请求以GET、HEAD或者POST以外的方式发起请求,比如DELETE。
- 使用POST,但是请求数据类型为 application/x-www-form-urlencoded,multipart/form-data或者text/plain以外的数据类型,比如application/json。
- 使用自定以请求头,比如X-TOKEN。
所以,场景4中的请求由于加了自定义请求头,所有该请求是一个预请求形式的跨域请求。我们可以改动服务器端的代码,使其允许这次跨域请求。
1 2 3 4 5 6 7 8 9 10 11
| http.createServer (function (request, response) { response.setHeader('Access-Control-Allow-Origin', '*'); response.setHeader(('Access-Control-Allow-Methods:', 'GET, POST, DELETE'); response.setHeader(('Access-Control-Allow-Headers:', 'X-TOKEN'); response.write(JSON.stringify(xwj)); response.end(); }).listen(8030, function () { console.log('Server has stared...') });
|
改动之后,场景3和场景4都可以完成跨域请求。
并且我们可以看到这次OPTIONS请求的响应头中包含以下字段:
1 2 3
| Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET,POST,DELETE Access-Control-Allow-Headers: X-TOKEN
|
附带凭证信息的请求
一般而言,对于跨域请求,浏览器是不会发送凭证信息(HTTP Cookie和验证信息)的。
但如果将XMLHttpRequest的一个特殊标志位withCredentials
设置为true,浏览器就将允许该请求的发送。
在带凭证的请求中,后端的响应头必须包含Access-Control-Allow-Credentials
,并且Access-control-Allow-Origin
不能再用*匹配,而必须明确指明域名。
我们对clinet.js做出以下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| document.cookie = 'name=xwj'; document.cookie = 'age=20'; var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(responseText); } } } xhr.open('GET', 'http://121.249.216.190:8030', true); xhr.withCredentials = true; xhr.send(null);
|
现在浏览器就会发送带凭证的请求,假如我们只是将server.js
中Access-control-Allow-Origin
的值改为http://127.0.0.1:8020
,那么会出现以下错误。
我们还需对server.js做出以下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| http.createServer (function (request, response) { response.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8020'); response.setHeader(('Access-Control-Allow-Methods:', 'GET, POST, DELETE'); response.setHeader(('Access-Control-Allow-Headers:', 'X-TOKEN'); response.setHeader('Access-Control-Allow-Credentials', true); response.write(JSON.stringify(xwj)); response.end(); }).listen(8030, function () { console.log('Server has stared...') });
|
http响应头
这些为服务器响应客户端http请求时候的响应头:
1 2 3 4 5
| Access-control-Allow-Origin Access-Control-Allow-Headers Access-Control-Allow-Credentials Access-Control-Allow-Methods Access-Control-Allow-Headers
|
http请求头
以下为发送http请求时的请求头:
1 2 3 4
| origin Access-Control-Request-Method Access-Control-Request-Headers
|
参考资料
参考资料主要有以下:
以上就是对CORS解决跨域问题一些总结。