跨域问题的存在是因为浏览器都遵循同源策略
同源策略
1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"。
- 协议相同
- 域名相同
- 端口相同
举例来说,http://www.netease.com/a.html
这个网址,协议是http://
,域名是www.netease.com
,端口是80
(默认端口可以省略)。它的同源情况如下。
跨域
针对浏览器的Ajax请求跨域的主要解决方案有:JSONP、CORS。
AJAX
首先演示下正常情况下,通过Ajax进行跨域请求的情景:
-
通过koa启了两个本地server,一个port为3200,一个为3201
app.js
const koa = require('koa'); const app = new koa(); const Router = require('koa-router'); const router = new Router(); const serve = require('koa-static'); const path = require('path'); const staticPath = path.resolve(__dirname, 'static'); // 设置静态服务 const staticServe = serve(staticPath, { setHeaders: (res, path, stats) => { if (path.indexOf('jpg') > -1) { res.setHeader('Cache-Control', ['private', 'max-age=60']); } } }); app.use(staticServe); router.get('/ajax', async (ctx, next) => { console.log('get request', ctx.request.header.referer); ctx.body = 'received'; }); app.use(router.routes()); app.listen(3200); console.log('koa server is listening port 3200');
app2.js
const koa = require('koa'); const app = new koa(); const Router = require('koa-router'); const router = new Router(); router.get('/ajax', async (ctx, next) => { console.log('get request', ctx.request.header.referer); ctx.body = 'received'; }); app.use(router.routes()); app.listen(3200); console.log('app2 server is listening port 3200');
两个server都定义了一个GET请求接口
/ajax
。除监听port不同外,app.js还设置了静态服务。origin.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>cross-origin test</title> </head> <body style="width: 600px; margin: 200px auto; text-align: center"> <button onclick="getAjax()">AJAX</button> <button onclick="getJsonP()">JSONP</button> </body> <script type="text/javascript"> var baseUrl = 'http://localhost:3201'; function getAjax() { var xhr = new XMLHttpRequest(); xhr.open('GET', baseUrl + '/ajax', true); xhr.onreadystatechange = function() { // readyState == 4说明请求已完成 if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) { // 从服务器获得数据 alert(xhr.responseText); } else { console.log(xhr.status); } }; xhr.send(); } </script> </html>
origin.html
放在app.js server对应的静态服务目录下,通过XMLHTTPRequest简单实现了一个Ajax Get方法。 -
修改请求地址
-
http://localhost:3200/ajax
服务器返200,且拿到返回值。
-
http://localhost:3201/ajax
当ajax发送跨域请求时,控制台报错:
Failed to load http://localhost:3201/ajax: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3200' is therefore not allowed access.
这里有个奇怪的点,虽然控制台有报错,但AJAX请求收到了200的应答:
这其实涉及到浏览器的CORS机制,具体在后面展开。
-
JSONP
-
原理
虽然浏览器同源策略限制了XMLHttpRequest请求不同域上的数据。但是,在页面上引入不同域的js脚本是可以的,而且script元素请求的脚本会被浏览器直接运行
-
测试
在
origin.html
的脚本中添加:function getJsonP() { var script = document.createElement('script'); script.src = baseUrl + '/jsonp?type=json&callback=onBack'; document.head.appendChild(script); } function onBack(res) { alert('JSONP CALLBACK: ', JSON.stringify(res)); }
getJsonP
方法会在当前页面添加一个script,src属性指向跨域的GET请求:http://localhost:3201/jsonp?type=json&callback=onBack
,通过query格式带上请求的参数。callback是关键,用于定义跨域请求回调的函数名称,这个值必须后台和脚本保持一致。在
app2.js
中添加jsonp请求的路由:router.get('/jsonp', async (ctx, next) => { const req = ctx.request.query; console.log(req); const data = { data: req.type } ctx.body = req.callback + '('+ JSON.stringify(data) +')'; }) app.use(router.routes());
针对jsonp请求,后台要做的是:
- 获取请求参数中的callback值,如本例中的
onBack
- 将callback的值以function(args)的格式作为response。
重启服务后,触发页面的
getJsonP
事件,效果: - 获取请求参数中的callback值,如本例中的
-
补充
jquery
,zepto
这些js第三方库,其提供的ajax 方法都有对jsonp请求进行封装,如jquery发jsonp的ajax请求:function getJsonPByJquery() { $.ajax({ url: baseUrl + '/jsonp', type: 'get', dataType: 'jsonp', // 请求方式为jsonp jsonpCallback: "onBack", // 自定义回调函数名 data: { type: 'json' } }); }
执行的效果一致。
-
优点
JSONP方案的兼容性好,IE浏览器也支持。
-
缺点
- 因为是利用的
<script>
元素,所以只支持GET请求。 - 缺乏错误处理机制
- 因为是利用的
CORS
CORS即跨域资源分享(Cross-Origin Resource Sharing),是W3C制定的标准。
-
特性
CORS需要浏览器和服务器同时支持。
- 大多主流浏览器都支持,IE 10以下不支持。
- 只要服务器端实现了CORS接口,浏览器就能自动实现基于CORS的跨域请求。
-
两种请求
浏览器将CORS请求分成两类:简单请求和非简单请求。
简单请求需要满足两个条件:
- 请求类型为
HEAD
,GET
,POST
之一; - 请求头信息不超出以下几种:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
- 请求类型为
简单请求
对于简单请求,浏览器会直接发出,同时在请求头中添加Origin
字段。
Origin
用来说明请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
回顾下直接Ajax测试跨域的请求报文:
浏览器为这个简单的GET请求添加了Origin
,而响应头信息中没有Access-Control-Allow-Origin
,浏览器判断请求跨域,给出错误提示。
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
在origin.html
中添加一个post请求:
function corsWithJson() {
$.ajax({
url: baseUrl + '/cors',
type: 'post',
contentType: 'application/json',
data: {
type: 'json',
},
success: function(data) {
console.log(data);
}
})
}
通过设置Content-Type
为appliaction/json
使其成为非简单请求:
"预检"请求的方法为OPTIONS
,服务器判断Origin
为跨域,所以返回404。
除了Origin
字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是content-type
。
服务端设置CORS
CORS允许服务端在响应头中添加一些头信息来响应跨域请求。
在app2.js
引入koa2-cors
:
app.use(cors({
origin: function (ctx) {
if (ctx.url === '/cors') {
return "*"; // 允许来自所有域名请求
}
return 'http://localhost:3201';
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5,
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'], //设置允许的HTTP请求类型
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));
重启服务后,浏览器重新发送POST请求。可以看到浏览器发送了两次请求。
OPTIONS
请求报文:
OPTIONS
的响应头表示服务端设置了Access-Control-Allow-Origin:*
,于是发送POST请求,得到服务器返回值。
在OPTIONS的请求响应报文中,头信息里有一些CORS提供的其他字段:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type,Authorization,Accept
Access-Control-Allow-Methods: GET,POST,DELETE
Access-Control-Max-Age: 5
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true
,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie,删除该字段即可。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
CORS与JSONP的区别
CORS更为标准,功能也更强大。而JSONP只支持GET请求。CORS唯一不足在于IE10以下不支持。