前言javascript
先后端处理数据交互时每每会遇到跨域的问题,那么什么是跨域? 有哪些跨域方式? 出现跨域又该如何解决呢?html
1、什么是跨域?前端
理解跨域首先要理解同源策略,它是浏览器对js施加的一种安全限制,若是缺乏了同源策略,浏览器很容易受到XSS、CSFR、SQL注入等攻击。所谓同源是指协议、域名、端口必须相同。浏览器在请求数据时都要遵循同源策略,那么凡是发送请求的URL中协议、域名、端口三者之中的一点不一样时,就叫作跨域。
java
而在同源策略当中,它所限制的内容就有如下几个:node
可是如下三个标签是容许跨域加载资源的:web
常见的跨域场景上面的图表已经说了,在协议,域名,端口号三者中,假若其中之一不相同都会致使跨域的现象.ajax
在此特别说明的两点:express
第一:若是是协议和端口形成的跨域问题“前台”是无能为力的。json
第二:在跨域问题上,仅仅是经过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”能够理解为“协议, 域名和端口必须匹配”。后端
这里你或许有个疑问:请求跨域了,那么请求到底发出去没有?
跨域并非请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明经过表单的方式能够发起跨域请求,为何 Ajax 就不会?由于归根结底,跨域是为了阻止用户读取到另外一个域名下的内容,Ajax 能够获取响应,浏览器认为这不安全,因此拦截了响应。可是表单并不会获取新的内容,因此能够发起跨域请求。同时也说明了跨域并不能彻底阻止 CSRF,由于请求毕竟是发出去了。
一. CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它容许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
基本上目前全部的浏览器都实现了CORS标准,其实目前几乎全部的浏览器ajax请求都是基于CORS机制的,只不过可能平时前端开发人员并不关心而已(因此说其实如今CORS解决方案主要是考虑后台该如何实现的问题)。
CORS 须要浏览器和后端同时支持目前,全部浏览器都支持该功能,IE浏览器不能低于IE10, IE 8 和 9 须要经过 XDomainRequest 来实现。
对于开发者来讲,CORS通讯与同源的AJAX通讯没有差异,代码彻底同样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感受。
浏览器会自动进行 CORS 通讯,实现 CORS 通讯的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin 就能够开启 CORS。 该属性表示哪些域名能够访问资源,若是设置通配符则表示全部网站均可以访问资源。
经过这种方式解决跨域会出现两种请求: 分别是简单请求和复杂请求.
(1) 请求方法是如下三种方法之一:
(2)HTTP的头信息不超出如下几种字段:
|
凡是不一样时知足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不同的。
下面就来仔细谈谈简单请求的基本流程:
1.1基本流程
对于简单请求,浏览器直接发出CORS请求。具体来讲,就是在头信息之中,增长一个Origin
字段。
下面是一个例子,浏览器发现此次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin
字段。
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0... |
上面的头信息中,Origin
字段用来讲明,本次请求来自哪一个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否赞成此次请求。
若是Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。注意,这种错误没法经过状态码识别,由于HTTP回应的状态码有多是200。
若是Origin
指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0... |
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-
开头。
该字段是必须的。它的值要么是请求时Origin
字段的值,要么是一个 *
,表示接受任意域名的请求。
该字段可选。它的值是一个布尔值,表示是否容许发送Cookie。默认状况下,Cookie不包括在CORS请求之中。设为true
,即表示服务器明确许可,Cookie能够包含在请求中,一块儿发给服务器。这个值也只能设为true
,若是服务器不要浏览器发送Cookie,删除该字段便可。
该字段可选。CORS请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。若是想拿到其余字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定,getResponseHeader('FooBar')
能够返回FooBar
字段的值。
1.2 withCredentials 属性
上面说到,CORS请求默认不发送Cookie和HTTP认证信息。若是要把Cookie发到服务器,一方面要服务器赞成,指定Access-Control-Allow-Credentials
字段。
Access-Control-Allow-Credentials: true
另外一方面,开发者必须在AJAX请求中打开withCredentials
属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
不然,即便服务器赞成发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
可是,若是省略withCredentials
设置,有的浏览器仍是会一块儿发送Cookie。这时,能够显式关闭withCredentials
。
xhr.withCredentials = false;
须要注意的是,若是要发送Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其余域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie
也没法读取服务器域名下的Cookie。
不符合以上条件的请求就确定是复杂请求了。
复杂请求的CORS请求,会在正式通讯以前,增长一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,经过该请求来知道服务端是否容许跨域请求。
咱们用PUT向后台请求时,属于复杂请求,后台需作以下配置:
// 容许哪一个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不作任何处理
if (req.method === 'OPTIONS') {
res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('小学生多多指教,我是小芳妞和小帆仔!')
})
接下来咱们看下一个完整复杂请求的例子,而且介绍下CORS请求相关的字段
// index.html let xhr = new XMLHttpRequest() document.cookie = 'name=xiamen' // cookie不能跨域 xhr.withCredentials = true // 前端设置是否带cookie xhr.open('PUT', 'http://localhost:4000/getData', true) xhr.setRequestHeader('name', 'xiamen') xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(xhr.response) //获得响应头,后台需设置Access-Control-Expose-Headers console.log(xhr.getResponseHeader('name')) } } } xhr.send() //server1.js let express = require('express'); let app = express(); app.use(express.static(__dirname)); app.listen(3000); //server2.js let express = require('express') let app = express() let whitList = ['http://localhost:3000'] //设置白名单 app.use(function(req, res, next) { let origin = req.headers.origin if (whitList.includes(origin)) { // 设置哪一个源能够访问我 res.setHeader('Access-Control-Allow-Origin', origin) // 容许携带哪一个头访问我 res.setHeader('Access-Control-Allow-Headers', 'name') // 容许哪一个方法访问我 res.setHeader('Access-Control-Allow-Methods', 'PUT') // 容许携带cookie res.setHeader('Access-Control-Allow-Credentials', true) // 预检的存活时间 res.setHeader('Access-Control-Max-Age', 6) // 容许返回的头 res.setHeader('Access-Control-Expose-Headers', 'name') if (req.method === 'OPTIONS') { res.end() // OPTIONS请求不作任何处理 } } next() }) app.put('/getData', function(req, res) { console.log(req.headers) res.setHeader('name', 'jw') //返回一个响应头,后台需设置 res.end('哈喽,我是小芳妞!') }) app.get('/getData', function(req, res) { console.log(req.headers) res.end('哈喽,我是小帆仔!') }) app.use(express.static(__dirname)) app.listen(4000)
上述代码由http://127.0.0.1:3000/index.html
向http://127.0.0.1:4000/
跨域请求,正如咱们上面所说的,后端是实现 CORS 通讯的关键。
二.JSONP方式解决跨域问题
jsonp解决跨域问题是一个比较古老的方案(实际中不推荐使用),这里作简单介绍(实际项目中若是要使用JSONP,通常会使用JQ等对JSONP进行了封装的类库来进行ajax请求)
实现原理:
JSONP之因此可以用来解决跨域方案,主要是由于Web页面上调用js文件时则不受是否跨域的影响(不只如此,咱们还发现凡是拥有”src”这个属性的标签都拥有跨域的能力,好比<\script>、<\img>、<\iframe>)。
若是想经过纯web端跨域访问数据,能够在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理。
实现流程:
1.简单实现
假设远程服务器http://remoteserver.com根目录下有个remote.js文件代码以下:
alert('我是远程文件');
本地服务器http://localserver.com下有个jsonp.html页面代码以下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript" src="http://remoteserver.com/remote.js"></script> </head> <body> </body> </html>
毫无疑问,页面将会弹出一个提示窗体,显示跨域调用成功。
2.如今咱们在jsonp.html页面定义一个函数,而后在远程remote.js中传入数据进行调用。
jsonp.html页面代码以下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript"> var localHandler = function(data){ alert('我是本地函数,能够被跨域的remote.js文件调用,远程js带来的数据是:' + data.result); }; </script> <script type="text/javascript" src="http://remoteserver.com/remote.js"></script> </head> <body> </body> </html>
remote.js文件代码以下:
localHandler({"result":"我是远程js带来的数据"});
运行以后查看结果,页面成功弹出提示窗口,显示本地函数被跨域的远程js调用成功,而且还接收到了远程js带来的数据。
很欣喜,跨域远程获取数据的目的基本实现了,可是又一个问题出现了,我怎么让远程js知道它应该调用的本地函数叫什么名字呢?毕竟是jsonp的服务者都要面对不少服务对象,而这些服务对象各自的本地函数都不相同啊?咱们接着往下看。
3.聪明的开发者很容易想到,只要服务端提供的js脚本是动态生成的就好了呗,这样调用者能够传一个参数过去告诉服务端 “我想要一段调用XXX函数的js代码,请你返回给我”,因而服务器就能够按照客户端的需求来生成js脚本并响应了。
看jsonp.html页面的代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript"> // 获得航班信息查询结果后的回调函数 var flightHandler = function(data){ alert('你查询的航班结果是:票价 ' + data.price + ' 元,' + '余票 ' + data.tickets + ' 张。'); }; // 提供jsonp服务的url地址(无论是什么类型的地址,最终生成的返回值都是一段javascript代码) var url = "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998&callback=flightHandler"; // 建立script标签,设置其属性 var script = document.createElement('script'); script.setAttribute('src', url); // 把script标签加入head,此时调用开始 document.getElementsByTagName('head')[0].appendChild(script); </script> </head> <body> </body> </html>
此次的代码变化比较大,再也不直接把远程js文件写死,而是编码实现动态查询,而这也正是jsonp客户端实现的核心部分,本例中的重点也就在于如何完成jsonp调用的全过程。
咱们看到调用的url中传递了一个code参数,告诉服务器我要查的是CA1998次航班的信息,而callback参数则告诉服务器,个人本地回调函数叫作flightHandler,因此请把查询结果传入这个函数中进行调用。
OK,服务器很聪明,这个叫作flightResult.aspx的页面生成了一段这样的代码提供给jsonp.html
(服务端的实现这里就不演示了,与你选用的语言无关,说到底就是拼接字符串):
flightHandler({
"code": "CA1998",
"price": 1780,
"tickets": 5
});
使用注意
基于JSONP的实现原理,因此JSONP只能是“GET”请求,不能进行较为复杂的POST和其它请求,因此遇到那种状况,就得参考下面的CORS解决跨域了(因此现在它也基本被淘汰了)
注意,因为接口代理是有代价的,因此这个仅是开发过程当中进行的。
与前面的方法不一样,前面CORS是后端解决,而这个主要是前端对接口进行代理,也就是:
关于如何实现代理,这里就不重点描述了,方法不少,也不难,基本都是基于node.js的。
搜索关键字node.js
,代理请求
便可找到一大票的方案。
Access-Control-Max-Age:
这个头部加上后,能够缓存这次请求的秒数。
在这个时间范围内,全部同类型的请求都将再也不发送预检请求而是直接使用这次返回的头做为判断依据。
很是有用,能够大幅优化请求次数