跨域的几种实现方式
首先,浏览器为了保护你的安全,禁止一个网站向和它不同域名的网站发送ajax请求。比如如果浏览器当前处于http://localhost:3001
网站下,那么它无法发送请求到http://localhost:3002
。
下面我们在localhost:3001
访问3002
中的api。
JSONP实现跨域
JSONP是一个比较投机的方式,它利用的是浏览器对script
标签的跨域没有限制这一特性来实现发送请求。
<html>
<head>
<script type="text/javascript" src="jquery.js"></script>
</head>
<body>
<script>
// 这里是api的回调
function handleResult(resp) {
alert(resp.name);
}
</script>
<!-- 这里访问不同域下的api -->
<script type="text/javascript" src="http://localhost:3002/controller"></script>
</body>
</html>
而API也不能返回单纯的json了,因为json是纯数据,script
标签加载之后并不会产生任何动作。API应该返回包装了回调函数调用的数据,这个函数的参数就是实际JSON数据。如下:
handleResult({
"name": "app2"
})
这样,当浏览器加载了localhost:3002/controller
,它会把响应数据当作js脚本来处理,然后就是调用回调函数handleResult
,并把实际的响应传回去。下面是调用成功的一个例子:
然而,这用到一个api就要一个script标签,这让人无法忍受,Jquery封装了jsonp的调用:
<html>
<head>
<script type="text/javascript" src="jquery.js"></script>
</head>
<body>
<script>
$(document).ready(function(){
$.ajax({
type : "get",
async: false,
url : "http://localhost:3002/controller",
dataType: "jsonp",
jsonp:"callback",
jsonpCallback: "jsonhandle",
success : function(data) {
alert("name:" + data.name);
}
});
});
</script>
</body>
</html>
这里的jsonp
代表请求中参数的名字,jsonCallback
代表回调函数的名字,这会在url中组合成localhost:3002?${jsonp}=${jsonpCallback}
的形式
Jsonp实现跨域的一个缺点就是,它无法发布post
请求,只能发布get
请求。
CORS实现跨域
CORS是现代浏览器为了支持跨域请求而造的一个新规范,简单来说就是浏览器会检测到跨域请求,并在其中加一个Origin
字段用于告诉服务器请求者当前所在的域。这需要服务器的支持,但是对于前端开发者来说,跨域请求完全由浏览器实现,前端开发者并不需要为之多做什么。
服务器返回后,浏览器会检测服务器返回的头中是否有Access-Control-Allow-Origin
字段,如果没有,代表服务器尚未支持跨域,那么不管这次请求是否成功都直接宣告失败。
我们不妨做一个实验,现在,我使用express
在3002端口开放两个api:
const express = require('express')
const app = express()
const port = 3002
// 直接返回,响应头中没有任何字段
app.get('/nocors', (req, resp) => {
res.send("OK");
})
// 响应头中加上Access-Control-Allow-Origin
app.get('/cors', (req, resp) => {
res.set('Access-Control-Allow-Origin', '*');
res.send("OK");
})
app.listen(port, () => {
console.log(`Express app listening on port ${port}`)
})
然后,我在3001端口运行一个服务,并使用ajax分别请求这两个API:
cors.html
:
$(document).ready(function () {
$.ajax({
type: 'get',
async: false,
url: 'http://localhost:3002/cors',
success: function (data) {
alert(data);
}
});
});
nocors.html
:
$(document).ready(function () {
$.ajax({
type: 'get',
async: false,
url: 'http://localhost:3002/nocors',
success: function (data) {
alert(data);
}
});
});
cors.html
得到了如下响应:
nocors.html
并未得到响应,控制台打印了如下数据:
并且网络界面显示请求失败,并且显示无法加载响应数据,尽管服务器端返回的状态码是200。
我们用脚丫子想想都知道了,服务器端的nocors
逻辑已经被执行,并且返回数据也发回了客户端,只是浏览器没在返回数据中看到Access-Control-Allow-Origin
,它就认为请求失败了,但实际上该请求的效果已经产生,如果服务器端的代码有什么副作用,那副作用也已经产生了。
CORS响应头字段
Access-Control-Allow-Origin
是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。Access-Control-Allow-Credentials
可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。Access-Control-Expose-Headers
可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
关于Cookie的问题,只携带服务器域名设置的Cookie,其它Cookie不会携带。
非简单请求
上面所说的CORS的所有内容都基于简单请求,简单请求的定义如下:
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
假设现在我们来修改cors.html
,来发一个非简单请求:
$(document).ready(function () {
$.ajax({
type: 'get',
async: false,
url: 'http://localhost:3002/cors',
// [+] 添加下面这行,不再是简单请求
contentType: 'application/json',
success: function (data) {
alert(data);
}
});
});
再次请求,我们发现请求也失败了,而且网络中出现了两个到cors
的ajax请求,其中的一个是OPTION
请求:
是这样的,对于复杂请求,浏览器会先发送一个OPTION
请求预检服务器是否支持CORS,当前我们的服务器显然还不支持OPTION请求,所以这次跨域请求会失败。
在复杂请求中,浏览器会携带两个头,一个代表它将要以什么方法请求跨域API,一个代表它请求头中携带的多余Header。下面是我们的请求头中的这两个参数:
服务器端的任务是校验它是否支持这个域的访问,如果支持,检测它是否支持对应的Header和请求方法,修改服务器端的代码:
app.get('/cors', (req, resp) => {
resp.set('Access-Control-Allow-Origin', '*');
resp.send("OK");
})
app.options('/cors', (req, resp) => {
resp.set('Access-Control-Allow-Origin', '*');
// 告诉浏览器,GET方法被支持
resp.set('Access-Control-Allow-Methods', 'GET')
// 告诉浏览器Content-Type头被支持
resp.set('Access-Control-Allow-Headers', 'Content-Type')
resp.send("OK");
})
再次请求
如果服务器端返回的头中没有后面那两行,浏览器也不会发起真正的请求,它会认为服务器表明它不支持我的请求。
WebSocket
WebSocket是HTML5中新增的,给浏览器与服务器进行套接字通信的接口,它是允许跨域的。
代理
代理就是,比如你通过域A访问域B,那么在域A中架设一台代理服务器,把请求转发到域B,然后浏览器访问域A即可,这其中不涉及到跨域访问了。
前端后端分离开发时经常会用到这个特性,在本地架设一个代理服务器,以免浏览器无法跨域访问api。
其它跨域方式
- window.postMessage()
- window.name+iframe
- 修改document.domain跨子域