共计 5727 个字符,预计需要花费 15 分钟才能阅读完成。
前言
上接《再谈风骚的跨源 / 域计划(今日篇)》,本篇聊聊古代规范(HTML5 之后)的跨源计划。
根底概念都在今日篇中,初学者请务必先看完今日篇。
配套的演示案例传送门。
自己集体能力无限,欢送批评指正。
PostMessage
该计划应用了 HTML5 新的 window.postMessage
接口,该办法是专门为不同源页面通信设计的,是一个经典的“订阅 - 告诉”模型。
原理
该计划原理与今日篇的“子域代理”很类似,都是主页面用 iframe 内非同源子页面作为代理去跟服务端交互获取数据。不同之处在于,“子域代理”须要通过批改 document.domain
使主页面获取子页面 document 操作权限,而 window.postMessage
曾经原生提供了主页面与子页面通信的方法,故仅须要主页面通过 window.postMessage
向子页面下命令,子页面申请实现后再以此告诉主页面即可实现跨源通信,换句话说子页面变成了一个相似转发服务的存在。
不须要批改 document.domain
也意味着解脱了“子域代理”严格的域限度,能够更加自在的利用在第三方 API 上。window.postMessage
是少有的不受同源限度的浏览器 API,精确来说是没有调用权限的限度而已,它对发送和接管的指标还是有严格限度的,这也是它安全性的体现。举个例子:
// 假如在 iframe 内页面进行订阅。window.addEventListener('message', event => {
// 验证发送者,发送者不合乎是能够不理睬的。if (event.origin !== 'http://demo.com') return
// 这就是发送过去的信息。const data = event.data
// 这是发送者的 window 实例,能够调用下面的 postMessage 回传信息。const source = event.source
})
// 主页面告诉。// 第二个参数是接收者的源,须要源齐全匹配的页面才会接管到信息。(“源”的定义见今日篇)// 设置为 * 能够实现播送,不过个别不举荐。iframe.contentWindow.postMessage('hello there!', 'http://demo.com')
流程
- API 所在的域部署一个代理页,设置好对 message 事件的监听,蕴含发送 Ajax 并将响应后果 postMessage 回主页面的性能;
- 主页面也设置对 message 事件的监听,并进行内容散发;
- 主页面新建 iframe 标签链接到代理页;
- 当 iframe 内的代理页就绪时,主页面就能够应用
iframe.contentWindow.postMessage
发送申请给代理页; - 代理页接管到申请,后发动 Ajax 到服务端 API;
- 服务端解决并响应,代理接管到响应后再通过
event.source.postMessage
传递给主页面。
错误处理
- 通过 iframe 的 load 事件能够查看代理页是否被加载(非同源须要 hack 办法),以此间接判断是否有网络谬误,但并不可知具体的谬误起因,也就是说无奈获取到服务器的响应状态码;
- iframe 的 error 事件在大部分浏览器是有效的(默认),发送 Ajax 是在 iframe 中实现,如果产生谬误只能通过 postMessage 转发给主页面,因而倡议不要在 iframe 内处理错误,应对立交给主页面解决。
实际提醒
-
前端
- 加载代理页是须要耗时的,因而要留神发动申请的机会,免在代理页还未加载完的时候申请;
- 并不需要每次申请都加载新的代理页,强烈建议只保留一个,多个申请共享;
- 如果听从上一条的倡议,还需思考代理页加载失败的状况,防止一次失败后后续均不能够;
- 能够应用预加载的形式提前加载代理页,免得减少申请的工夫;
- 无论是接管方还是发送方,都应该设置和验证 postMessage 的指标(targetOrigin),以确保安全性;
- 没必要每次申请都去监听 message 事件,能够在初始化时设置一个对立事件处理器进行内容散发,用一个对象将每次申请的回调保存起来,调配惟一的 id,通过对立的事件处理器按 id 调用回调;
- 如果听从上一条的倡议,全局对象内回调函数须要及时清理。
-
服务端
- 代理页的域必须与 API 的域是统一的;
- 代理页个别无需常常更新,能够进行长期缓存;
- 代理页应尽量精简,Ajax 申请的后果无论胜利或失败都应 postMessage 给主页面。
共享 iframe 的设计思路请参考今日篇的“子域代理”。
前端“对立事件处理器”的设计思路:
function initMessageListener() {
// 保留回调对象的对象。const cbStore = {}
// 设置监听,只需一个。window.addEventListener('message', function (event) {
// 验证发送域。if (event.origin !== targetOrigin) {return}
// ...
try {
// 运行失败分支。if (...) {cbStore[msgId].reject(new Error(...))
return
}
// 运行胜利分支。cbStore[msgId].resolve(...)
} finally {
// 执行清理。delete cbStore[msgId]
}
})
// 这里造成了一个闭包,只能用特定办法操作 cbStore。return {
// 设置回调对象的办法。set: function (msgId, resolve, reject) {
// 回调对象蕴含胜利和失败两个分支函数。cbStore[msgId] = {
resolve,
reject
}
},
// 删除回调对象的办法。del: function (msgId) {delete cbStore[msgId]
}
}
}
// 初始化,每次申请都调用其 set 办法设置回调对象。const messageListener = initMessageListener()
配合下面的“对立事件处理器”,msgId 其实没必要传递到服务端,在代理页解决即可:
window.addEventListener('message', event => {
// 验证发送域。if (event.origin !== targetOrigin) {return}
// 这是主页面 postMessage 的数据。// 其中 msgId 与“对立事件处理器”无关,其余参数与 Ajax 无关,按理论须要传递即可。const {msgId, method, url, data} = event.data
// 发送 Ajax。xhr(...).then(res => {
// 将 msgId 退出回传数据,其余保留原样。res.response.data = {
...res.response.data,
msgId
}
// 回传给主页面。event.source.postMessage(res, targetOrigin)
})
})
具体代码请参考演示案例 PostMessage 局部源码。
CORS(跨源资源分享)
CORS 全称 Cross-origin resource sharing,是 W3C 组织制订的规范跨源计划(传送门),也能够说是跨源的官网终极解决方案,它让古代的 web 开发不便不少。
原理
简略来说 CORS 是一套服务端与浏览器的协商机制,通过报文头实现,浏览器告知服务端起源(origin)和心愿容许的办法,服务端返回“白名单”(也是一组报文头),浏览器根据“白名单”判断是否容许这次申请,可利用与 Ajax、canvas 等的跨源状况。
CORS 分为 简略申请(simple)和 简单申请(complex),他们最次要的区别就是需不需要预检(preflight)。
简略申请须要满足如下条件(只挑重点):
-
办法(method)为如下之一
- GET
- POST
- HEAD
-
只容许设置如下报文头(header)
- Accept
- Accept-Language
- Content-Language
-
Content-Type(只容许三个)
text/plain
multipart/form-data
application/x-www-form-urlencoded
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
不满足下面条件的都会被断定为简单申请,就理论应用而言 form 收回的申请根本都是容许的,如果要应用 json 格局传递数据(即 Content-Type: application/json
),那必然是简单申请。
简单申请会先收回预检申请,也就是先问问看服务端,如果返回的“白名单”符合要求再会发动正式的申请。
预检申请是办法(method)为 OPTION 的申请,它不须要携带任何业务数据,仅按照须要发送 CORS 相干申请报文头给服务端,服务端也不须要响应任何业务数据,仅返回“白名单”,实现协商即可。
-
CORS 相干申请报文头
- Origin:发动申请页面的源,由浏览器主动增加,不容许手动设置;
- Access-Control-Request-Method:心愿服务端容许的办法,浏览器预检时根据正式申请的须要主动增加,不容许手动设置;
- Access-Control-Request-Headers:心愿服务端容许的申请报文头,浏览器预检时根据正式申请的须要主动增加,不容许手动设置。
-
CORS 相干响应报文头(即“白名单”)
- Access-Control-Allow-Origin:容许拜访该资源的域,这是开启 CORS 必定会返回的响应报文头,填写为 则示意容许来自所有域的申请,如果指定了非 的源,须要将源作为缓存判断根据,因而增加
Vary: Origin
免得当 API 给不同源页面返回不同数据时,被缓存搞混; - Access-Control-Expose-Headers:在跨域的状况下,XMLHttpRequest 对象的 getResponseHeader() 办法只能拿到一些最根本的响应头,如果要获取而外头部,须要进行指定;
- Access-Control-Max-Age:本次预检的最长有效期(秒),在这段时间内浏览器将不须要再次预检,而是间接发送正式申请;
- Access-Control-Allow-Credentials:是否容许携带 cookie,默认为 false,当设置为 true 时,不容许 Access-Control-Allow-Origin 设为 *;
- Access-Control-Allow-Methods:容许应用的申请办法;
- Access-Control-Allow-Headers:容许应用的申请报文头,罕用于增加自定义报文头。
- Access-Control-Allow-Origin:容许拜访该资源的域,这是开启 CORS 必定会返回的响应报文头,填写为 则示意容许来自所有域的申请,如果指定了非 的源,须要将源作为缓存判断根据,因而增加
流程
简略申请与个别的 Ajax 流程完全相同,仅需浏览器发送 Origin 申请报文头,服务端返回 Access-Control-Allow-Origin 响应报文头即可。
上面详讲简单申请的状况。
假如当初网页源为 http://demo.com
,服务端 API 源为 http://api.demo.com
,需要申请的办法为 POST,数据类型是 json,自定义报文头 token。
- 浏览器查看到,将发动 Ajax 申请的 API 源与以后页面源不同,则进入 CORS 协商;
- 数据类型是 json,而已还要自定义报文头,断定本次是简单申请;
-
发送预检 OPTION 申请,无关 CORS 的报文头设置如下:
- 读取以后页面的源写入
Origin: http://demo.com
; - 因为须要 POST 申请,则
Access-Control-Request-Method: POST
; - 因为须要数据类型是 json,也就是默认三种 content-type 不符合要求,还有自定义报文头 token 则
Access-Control-Request-Headers: content-type, token
;
- 读取以后页面的源写入
-
服务器接管到预检申请进行响应,无关 CORS 的报文头设置如下:
- 写入容许的域
Access-Control-Allow-Origin: http://demo.com
; - 写入容许的办法
Access-Control-Allow-Methods: POST, GET, OPTIONS
; - 写入容许的报文头
Access-Control-Allow-Headers: Content-Type, token
; - 写入
Vary: Origin
(下面有阐明,它不属于 CORS 报文头,但必须)
- 写入容许的域
- 浏览器接管到响应,验证 CORS 响应报文头,验证通过则紧接着发送正式 POST 申请,仅需增加
Origin: http://demo.com
,其余与失常申请统一; - 服务器接管正式申请,解决后进行响应,仅需增加
Access-Control-Allow-Origin: http://demo.com
和Vary: Origin
,其余与失常响应统一; - 浏览器接管到响应,验证 CORS 响应报文头,验证通过则实现申请。
错误处理
- 服务器谬误能够像个别申请那样捕捉,取得精确的状态码;
- 当产生跨源相干的谬误时,可在 XMLHttpRequest 对象的 error 事件捕捉到;
-
跨源相干的谬误总体分两类。
- 拦挡响应的谬误:比方简略申请的时候,接管到响应数据,但响应报文头验证未通过,这时候尽管从抓包上看曾经实现申请,但浏览器仍然会报错;
- 限度申请的谬误:比方简单申请的时候,预检返回的响应报文头验证未通过,则浏览器不会发动正式的申请,而是间接报错,这时候抓包是看不到正式申请的。
实际提醒
-
前端
- 该计划对前端的影响是非常小的,简直是浏览器主动实现,像个别申请那样发动即可;
- 错误处理局部有提到两类跨源相干的谬误,这是在调试时须要留神的点。
-
服务端
-
不倡议无脑增加 CORS 相干响应报文头,要按需增加,免得造成头部冗余,参考下面的流程,能够大抵可分为两组。
- 简略申请头部:Access-Control-Allow-Origin 和 Vary 两个即可;
- 预检申请头部:按需抉择 CORS 的头部,外加 Vary。
- Access-Control-Max-Age 是一个无效的优化伎俩,它能够缩小频繁的预检申请,节约资源。
- 除非是公共的第三方 API,不倡议将 Access-Control-Allow-Origin 设为 * 号。
- 为了安全性,最好验证 Origin 申请报文头,而不是疏忽它,当不符合要求时,能够返回 403 状态码。
-
具体代码请参考演示案例 CORS 局部源码。