CORS 跨域资源共享,这个话题大家一定不陌生了,吃久了大转转公众号的深度技术好文,也该吃点儿小米粥溜溜胃里的缝儿了,今天咱们就再好好屡屡 CORS 跨域资源共享这个话题,大牛怡情小牛巩固,把这碗前端经久不凉的大碗茶,再细细的品一品。
"JSONP 直接了当很豪爽,CORS 细吮慢品大补汤"
在咱们前端的日常工作中,跨域比较常用的方式就是 JSONP,JSONP 呢就是通过 script 标签无同源限制的特点,在获取到需要的资源后自动执行回调方法的方式,而我们浏览器原生的 CORS 跨域,是通过 "正当手段" 得到服务器小姐姐首肯,大摇大摆获取跨域资源的方式,相比 JSONP 只能实现 GET 请求,CORS 大法支持所有的请求类型,同时 CORS 是通过普通的 XMLHttpRequest 发起请求和获得数据,比起 JSONP 有更好的错误处理,接下来我们就来说下这个 CORS 大法。
"整体概述,先摆个谱"
官方简略的: CORS(Cross-Origin Resource Sharing)跨域资源共享,主要思想就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定响应是成功还是失败,它允许了浏览器向跨源服务器发送请求,从而克服了同源的限制。
私下露骨的: 其实就是向服务器发送跨域请求时,浏览器自动针对普通请求和非普通请求进行区别对待,在请求头中加个 Origin 字段告诉服务器这个请求的源,通过服务器返回的响应头中 Access-Control-Allow-Origin 字段的值是不是请求中的 Origin,来看服务器让不让咱请求到这资源。
"我是一些工作中不怎么用得到的基本知识,可我也是一条小生命啊"
CORS 浏览器的支持情况 :
浏览器端已经获得了良好的支持,所以实现 CORS 的关键就是服务器,只要实现了 CORS 的接口,就可以实现跨域通信。
IE 对 CORS 的实现 :
IE8 中引入了 XDR(XDomainRequest),注意:
XDR 对象的使用方法用户 XHR 对象非常类似,如下:
- 1 var xdr = new XDomainRequest();
- 3 xdr.onload = function() {
- 5 alert(xdr.responseText);
- 7 }
- 9 xdr.onerror = function() {
- 11 alert("error");
- 13 }
- 15 xdr.open("get", "http://www.xxx.com/yyy/");
- 17 xdr.send(null);
其他浏览器对 CORS 的实现
Firefox3.5+,Safari4+,Chorme,IOS 版的 Safari 和 Android 平台下的 webKit 都通过 XmlHttpRequest 实现了对 CORS 的支持。
- 1 var xhr = new XMLHttpRequest();
- 3 xhr.onreadystatechange = function () {
- 5 if(xhr.readyState == 4){
- 7 if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
- 9 console.log(xhr.responseText)
- 11 }else {
- 13 console.log('err' + xhr.status);
- 15 }
- 17 }
- 19 };
- 21 xhr.open('get','http://www.xxx.com/zzz/',true);
- 23 xhr.send(null);
跨域 XHR 一些安全限制:
"请求没有那么简单,每种都有她的习惯"
浏览器将 CORS 请求分成两类。
1. 简单请求:
高大上版定义:
Simple requests
A simple cross-site request is one that meets all the following conditions:
The only allowed methods are: GET HEAD POST
Apart from the headers set automatically by the user agent (e.g. Connection,User-Agent, etc.), the only headers which are allowed to be manually set are:
Accept
Accept-Language
Content-Language
Content-TypeThe only allowed values for the Content-Type header are:
application/x-www-form-urlencoded multipart/form-data text/plain
大意就是说:
2. 非简单请求:
非简单请求是那种对服务器有特殊要求的请求,除以上条件之外的都是非简单请求,条件如下:
"跨域请求分两队,差别对待也是醉"
在讨论 "CORS 对不同请求的处理" 这部分内容时,我们同步跑起来一个 nodejs 的项目对照着理解,纸上谈兵终觉浅,要干大事还得码啊!
为了更加直观,我们为我们接下来要占用的两个端口配下代理:
- 127.0.0.1:8081 m.zhuanzhuan.com
- 127.0.0.1:8082 u.58.com
客户端 nodejs 脚本 client.js:
- 1 var http = require('http');
- 3 var fs = require('fs');
- 5 var url = require('url');
- 8
- 9 // 创建服务器
- 11 http.createServer( function (request, response) {
- 13 // 解析请求,包括文件名
- 15 var pathname = url.parse(request.url).pathname;
- 17 // 输出请求的文件名
- 19 console.log("Request for " + pathname + " received.");
- 21 // 读取请求的文件内容
- 23 fs.readFile(pathname.substr(1), function (err, data) {
- 25 if (err) {
- 27 console.log(err);
- 29 // HTTP 状态码: 404 : NOT FOUND Content Type: text/plain
- 31 response.writeHead(404, {'Content-Type': 'text/html'});
- 33 }else{
- 35 // HTTP 状态码: 200 : OK
- 37 response.writeHead(200, {'Content-Type': 'text/html'});
- 39 // 响应文件内容
- 41 response.write(data.toString());
- 43 }
- 45 // 发送响应数据
- 47 response.end();
- 49 });
- 51 }).listen(8082);
- 53 // 控制台会输出以下信息
- 55 console.log('Server running at http://u.58.com/');
服务端 nodejs 脚本 server.js:
- 1 var express = require('express');
- 3 var app = express();
- 5 var router = express.Router();
- 9 router.all('/getData', function(req, res, next) {
- 11 //设置允许跨域请求
- 13 var reqOrigin = req.header("origin");
- 15 console.log(reqOrigin);
- 17 if(reqOrigin !=undefined && reqOrigin.indexOf("http://u.58.com") > -1){
- 21 //设置允许 http://u.58.com 这个域响应
- 23 res.header("Access-Control-Allow-Origin", "http://u.58.com");
- 25 res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
- 27 res.header("Authorization",'zhuanzhuanFe')
- 29 res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
- 31 }
- 33 res.json(200, {name:"转转熊",property:"Cute"});
- 35 });
- 39 app.use('/', router);
- 43 var server = app.listen(8081, function () {
- 45 console.log("应用实例,访问地址为http://127.0.0.1:8081/");
- 47 });
- 51 console.log('Server running at http://m.zhuanzhuan.com/');
创建一个 index.html
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>test cors</title>
- </head>
- <body >
- <button id="btn" onclick="getData()">跨域获取数据</button>
- </body>
- <script>
- function getData(){
- var xhr = new XMLHttpRequest();
- xhr.onreadystatechange = function () {
- if(xhr.readyState == 4){
- if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
- console.log(xhr.responseText)
- }else {
- console.log('err' + xhr.status);
- }
- }
- };
- xhr.open('get','http://m.zhuanzhuan.com/getData',true);
- xhr.send(null);
- }
- </script>
- </html>
我们在控制台执行我们创建的 client.js
在浏览器访问 http://u.58.com/index.html(也就是我们的 127.0.0.1:8082),控制台会看到如下输出:
同时,我们把我们的服务端也跑起来,注意上一个服务不要停掉哈
有了代码心里就有底了,接下来我们来看下简单和非简单请求,CORS 到底 do 了 what!
1. 简单请求:
按照上面的 getData 方法的配置:
- xhr.open('get','http://m.zhuanzhuan.com/getData',true);
我们点击一下 http://u.58.com/index.html 页面的按钮,浏览器就会从 http://u.58.com 下发向 http://m.zhuanzhuan.com/getData 发送一个普通的 GET 请求
对于简单的跨域请求,浏览器自动的发出 CORS 请求,在请求头中,增加一个 Origin 字段,如下请求头:
请求头 RequestHeaders 中有一个 Origin 字段,这个字段表示本次请求来自哪个源(协议 + 域名 + 端口)
服务器接收到请求后,如果不为这个跨域请求做任何的操作,如下改写下 server.js:
- router.all('/getData', function(req, res, next) {
- //不做任何的处理
- });
服务器接收到请求后,如果 Origin 指定的源不在许可范围内,服务器会返回一个正常的 HTTP 回应,浏览器接收到的回应头信息中没有包含 Access-Control-Allow-Origin 字段,那么浏览器就会抛出一个错误,被 XHR 的 onerror 函数捕捉,这种情况无法通过状态码判断,状态码可能会返回 200。
如果请求头中的 Origin 字段是服务器允许的来源,那么服务器会在请求的返回头中添加 Access-Control-Allow-Origin 字段,并赋值为请求头中的 Origin,表示允许该源请求资源。接下来我们恢复 server.js 中之前对 getData 请求的处理,再次点击按钮发起跨域请求,模拟一下上述情景:
- 1 router.all('/getData', function(req, res, next) {
- 2
- 3 //设置允许跨域请求
- 5 var reqOrigin = req.header("origin");
- 7 console.log(reqOrigin);
- 9 if(reqOrigin !=undefined && reqOrigin.indexOf("http://u.58.com") > -1){
- 11 //设置允许 http://u.58.com 这个域响应
- 13 res.header("Access-Control-Allow-Origin", "http://u.58.com");
- 15 res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
- 17 res.header("Authorization",'zhuanzhuanFe');
- 19 res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization");
- 21 }
- 23 res.json(200, {name:"转转熊",property:"Cute"});
- 25 });
如果 Origin 指定的域名在许可的范围内的话,服务器返回的响应,会多出来几个头信息字段:
响应头 Response Headers 中以 Access-Control - 开头的字段都是与 CORS 相关的字段, 其中 Access-Control-Allow-Origin 该字段是允许跨域响应头中必不可少的一个字段,值一般都是请求中 Origin 的值,表示允许当前源的跨域请求,也可以设置成 *,表示接受任意源的请求。其余相关字段下面会详解。
2. 非简单请求 Preflighted Request: 当浏览器发送的请求为非简单请求时,浏览器必须首先使用 OPTIONS 方法向服务器发起一个预检请求(Rreflighted Request),从而获知服务端是否允许该跨域请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
我们将请求从简单请求 GET 改成非简单请求 PUT,将 index.html 中 getData 方法稍加改动:
- xhr.open('put','http://m.zhuanzhuan.com/getData',true);
当然,我们还是要先看看对跨域请求不做任何处理的时候的状态,继续将 server.js 中对 Access-Control-Allow-Origin 字段的设置注释掉,发送下请求:
响应头中 Access-Control-Allow-Origin 字段如果不设置为 Origin 字段的值或者 "*",就表示该预检请求不被同意,会返回一个正常的 HTTP 回应,但是没有任何的 CORS 相关头信息字段,这时浏览器接收到响应,会被 XMLHttpRequest 对象的 onerror() 回调函数捕获,控制台会打出如上的报错信息。
我们接下来看看正常的复杂请求的处理逻辑。恢复 server.js 中对 Access-Control-Allow-Origin 字段的设置,刷新页面,点击按钮再次发送请求,我们看到比起我们要发送的 PUT 请求,页面多了一次类型为 OPTIONS 的请求:
这个就是预检请求。对于预检请求,响应头中有一个必须的字段:Access-Control-Request-Method,表示请求允许使用的方法,如果没有这个字段,预检请求无法通过,我们来实践一下,注释掉之前在响应中对 Access-Control-Request-Method 字段的设置,改写下 server.js:
- 1 router.all('/getData', function(req, res, next) {
- 2
- 3 //设置允许跨域请求
- 5 var reqOrigin = req.header("origin");
- 7 if(reqOrigin !=undefined && reqOrigin.indexOf("http://u.58.com") > -1){
- 9 //设置允许 http://u.58.com 这个域响应
- 11 res.header("Access-Control-Allow-Origin", "http://u.58.com");
- 13 }
- 15 res.json(200, {name:"转转熊",property:"Cute"});
- 17 });
点击页面按钮发送 put 请求,可以看到页面控制台报错:
顾名思义,我们发送的 put 方法并没有在响应头中设置被允许,所以,预检请求失败,真正的请求也就被扼杀在摇篮里了...
来来来让我们动气程序员证就世界的手指把摇篮里的巨婴救活吧!恢复 server.js 到最开始的状态,重新发送请求:
服务器一旦通过了预检请求,以后每次浏览器正常的 CORS 请求都会跟简单请求一样,妥了,救活,万岁!
"凭证不是你想带,想带就能带"
默认情况下跨域请求不提供凭据(Cookie,HTTP 认证以及 SSL 证明等),但是通过将 xhr 的 withCredentials 属性设置为 true,就可以指定某个请求发送凭据。如果服务器接受带凭据的请求,会在响应头中用 Access-Control-Allow-Credentials:true 来响应。
首先我们改写下 index.html 中的 getData 方法:
- 1 function getData(){
- 3 var xhr = new XMLHttpRequest();
- 5 //允许跨域携带请求
- 7 xhr.withCredentials = true;
- 9 xhr.onreadystatechange = function () {
- 11 if(xhr.readyState == 4){
- 13 if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
- 15 console.log(xhr.responseText)
- 17 }else {
- 19 console.log('err' + xhr.status);
- 21 }
- 23 }
- 25 };
- 27 xhr.open('put','http://m.zhuanzhuan.com/getData',true);
- 29 xhr.send(null);
- 31 }
我们刷新页面发送请求:
一厢情愿了吧,意思就是在预检请求的响应头中如果不同时为 Access-Control-Allow-Credentials 设置为 true,那么浏览器就不会把响应交给 javascript,responseText 为空字符串,status 为 0,触发 onerror(),跨域携带凭证的请求是不被允许滴。
我们乖乖在 server.js 响应中加上下面的代码:
- res.header("Access-Control-Allow-Credentials",'true');
再次请求,可以看到:
拖家带口闯关东成功!
对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为 "*",值必须为 Origin 首部字段所指明的域名即允许附带凭证的源,实践一下看看,将 server.js 中稍加修改:
- res.header("Access-Control-Allow-Origin", "*");
发送请求,可以看到:
祖国河山一片红,不说应该也懂了吧,就是不行哈不行,NOOO!
"... 我编不出来了... 反正就是介绍下双方出场队员 = ="
最后我们将跨域涉及到的一些响应头中 Access-Control-Allow 家族常见的字段罗列下,给我们的知识系统来几下 80 块的扎实大锤:
HTTP 响应首部字段
HTTP 请求首部字段
差不多就这么多了,鞠躬感谢,前端小新人的一些拙劣总结,有问题的地方还希望各位前辈们多多指教,一起携手在前端的草原策马奔腾吧!嘚驾~~~~
如果你喜欢我们的文章,关注我们的公众号和我们互动吧。
来源: http://www.cnblogs.com/zhuanzhuanfe/p/7430199.html