跨域请求
一个域上的文档或者脚本试图请求另一个域下的资源,就是跨域。比如
- 资源跳转:a 链接、重定向、表单提交;
- 资源引入:link、script、img、iframe 等标签,css background:url()、font-face;
- 脚本请求:XMLHttpRequest、fetch;
同源限制--狭义的跨域
同源策略(same origin policy)是一种浏览器安全策略,即只允许浏览器向同源的服务器请求资源(请求还是会发送,服务器也会响应,只是响应被浏览器拦截了),可避免浏览器遭到 xss、csfr 等攻击。同源:协议+域名+端口三者相同,即使不同的域指向同一个 ip,也非同源。
非同源限制了几种行为:
- Cookie、LocalStorage 和 IndexDB 无法获取;
- DOM 无法获得;
- AJAX 返回的数据不能获取。
容易混淆的跨域:
域名和域名对应相同 ip 不允许
主域相同,子域不同 不允许
跨域解决方案
- JSONP
- CORS
- WebSocket
- node 中间件代理
还有其他一些方法,本文主要介绍 JSONP 和 CORS。
jsonp 实现跨域
原理:具有 src 属性的标签在请求资源时,不受同源策略限制(历史遗留问题),可以通过这些标签(img、script 等)加载外域的脚本,在本域执行,实际是加载一个函数调用。
例如:
<!-- 在 http://127.0.0.1:8016/ 的页面内有一下标签 -->
<script>
function localHandler(data) {
alert(data.name)
}
</script>
<!-- jsonp.js 脚本里有 localHandler 函数,该函数在本域有定义,加载该脚本后就会执行本域的 localHandler 处理数据 -->
<script src="http://localhost:3001/jsonp.js"></script>
//http://localhost:3001 域下有如下脚本 jsonp.js
localHandler({
name: 'jack',
age: 24
})
关键
:外域上的脚本是一个执行函数,函数参数是一个对象或者 JSON , 且与本域的处理函数同名。
问题:外域的脚本是写死了,我们如何统一本域和外域的函数名呢?
改进 1:在 src 传递过去就好,其实还可传递其他参数。
<script>
function localHandler(data) {
alert(data.name)
}
</script>
<!-- 在本域传递参数 -->
<script src="http://localhost:3001/jsonp.js?callback=localHandler&id=abc8848"></script>
外域服务端处理:
// node 代码
let query = Url.parse(req.url, true).query
let callback = query.callback // 本地传递过来的本地函数名
let id = query.id // 假设根据id,查询数据库,获取用户信息 userInfo
let userInfo = {
id,
name: 'jack',
age: 24
}
let response = callback + '(' + JSON.stringify(userInfo) + ')' // 将处理函数和传递的数据组合
res.write(response)
res.end()
问题:在本域 script 标签写死了,动态插入,会更加好。
改进 2:动态插入 script 标签。
//本域动态插入 script 标签
//处理返回数据
var localHandler = function(data) {
console.log(data)
}
// 提供 jsonp 服务的 url 地址(生成的返回值都是一段javascript代码)
// 将处理函数传递给服务端,这里查询 id 为 abc8848 的用户信息
var url = 'http://localhost:3001/jsonp?callback=localHandler&id=abc8848'
// 创建script标签,设置其属性
var script = document.createElement('script')
script.setAttribute('src', url)
// 把script标签加入 head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script)
外域服务端处理:
// node 代码
let query = Url.parse(req.url, true).query
let callback = query.callback //本地传递过来的本地函数名
let id = query.id // 假设根据 id,查询数据库,获取用户信息 userInfo
let userInfo = {
id,
name: 'jack',
age: 24
}
let response = callback + '(' + JSON.stringify(userInfo) + ')' //将处理函数和传递的数据组合
res.write(response)
res.end()
jquery AJAX 跨域
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
$.ajax({
type: 'get',
async: false,
url: 'http://localhost:3001/jsonp?id=abc8848',
dataType: 'jsonp',
jsonp: 'callback', //传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback)
jsonpCallback: 'localHandler', //自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
})
.done(json => {
console.log(json)
})
.fail(err => {
console.log(err)
})
})
</script>
外域服务处理同上。
jsonp 的优缺点:
优点
- 所有浏览器都支持。
- 简单。
缺点
- 由于返回的数据当成脚本执行,会存在脚本注入的安全问题。
- 只能发一次请求。
- 只能使用 GET 方法。
- 不好处理请求错误。
cors 跨域
CORS 需要浏览器和服务器同时支持。目前主流浏览器都都支持 cors,cors 通信过程浏览器自动完成,和同源通信无差别。cors 关键是服务器,只要服务器设置了 CORS,就能实现跨域请求。
两种请求
满足以下两个条件的是简单请求,否则为非简单请求。
- 请求方法为:GET|POST|HEAD
- 请求头不超出以下字段:
- Accept|Accept-Language|Content-Language|Last-Event-ID
- Content-Type 的值限制在
application/x-www-form-urlencoded
|multipart/form-data
|text/plain
。
简单请求和非简单请求,浏览器处理不同。
简单请求
简单请求,浏览器器直接发出 CORS 请求,会自动在请求头中增加 Origin
字段,告诉服务器,本次请求从哪个源(协议+域名+端口号)发起,服务器根据该源,决定是否同意本次请求。
服务响应头信息 Access-Control-Allow-Origin
包含源,则同意请求,否则出错,触发 XHR 的 error 事件。注意,这种错误无法根据状态码识别,因为可能为 200。
简单请求,响应头信息除了 Access-Control-Allow-Origin
字段必须设置外,可选地设置以下字段:
Access-Control-Allow-Credentials
:布尔值,是否允许发起者获取响应内容。CORS 默认发送 Cookie,但是响应不携带 Access-Control-Allow-Credential:true
,浏览器不会把响应内容返回给请求的发起者(即该请求拿不到响应)。不需要携带认证信息,可设置请求 xhr.withCredentials = false;
。附带认证的请求, Access-Control-Allow-Origin
必须设置一个具体的值,否则请求将失败。响应中携带 Set-Cookie
字段,尝试对 Cookie 进行修改,失败会抛出异常。
Access-Control-Expose-Headers
:CORS 请求时,XHR 的 getResponseHeader()
只能拿到 6 个头信息: Cache-Control
| Content-Language
| Content-Type
| Expires
| Last-Midified
| Pragma
。想获取到其他字段,必须在这个响应头里指定。
非简单请求
非简单请求,在发送实际请求之前,会发送一个预请求(preflight),以确定跨域信息。跨域信息包括:**允许跨域的域 + 方法 + 请求 URL + 特殊的请求头字段 + credentials **。跨域信息符合实际请求,才会发起实际请求,否则报错。通过预检请求会,在跨域缓存时间内,不会再发起预检请求。
实际请求:
var url = 'http://api.alice.com/cors'
var xhr = new XMLHttpRequest()
xhr.open('PUT', url, true) // PUT
xhr.setRequestHeader('X-Custom-Header', 'value') //自定义请求头
xhr.send()
浏览器检测到非简单请求,会发情预检请求:
OPTIONS /cors HTTP/1.1 // OPTIONS 方法,询问跨域信息
Origin: http://api.bob.com // 将要跨域的源
Access-Control-Request-Method: PUT // 将要跨域的方法
Access-Control-Request-Headers: X-Custom-Header // 跨域自定义字段,用逗号分隔的字符串。
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
预请求响应:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com // 允许跨域的源 必需的
Access-Control-Allow-Methods: GET, POST, PUT // 允许跨域的请求方法 必需的
Access-Control-Allow-Headers: X-Custom-Header // 允许跨域的请求头字段 必需的
Access-Control-Max-Age: 1728000 // 缓存跨域请求的时间,单位为秒,非必需
Access-Control-Allow-Credentials: false // 响应内容控制,非必需
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
实际请求满足预请求响应,会自动发送实际请求,否则触发 error 事件。通过了预检请求,以后只要在跨域缓存时间内(注意,URL 变了,跨域缓存信息也会失效),就和简单请求一样,携带 Origin
字段,服务器响应,有 Access-Control-Allow-Origin
字段。
实际请求:
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
实际响应(部分字段):
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
CORS 优劣
优点:
- 支持的方法多;
- 更加安全。
缺点:
- 老旧浏览器不支持;
- 会多发请求。
相比 CORS 的优点,缺点可忽略。