关于前端:Pride再谈风骚的跨源域方案今日篇

6次阅读

共计 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')

流程

  1. API 所在的域部署一个代理页,设置好对 message 事件的监听,蕴含发送 Ajax 并将响应后果 postMessage 回主页面的性能;
  2. 主页面也设置对 message 事件的监听,并进行内容散发;
  3. 主页面新建 iframe 标签链接到代理页;
  4. 当 iframe 内的代理页就绪时,主页面就能够应用 iframe.contentWindow.postMessage 发送申请给代理页;
  5. 代理页接管到申请,后发动 Ajax 到服务端 API;
  6. 服务端解决并响应,代理接管到响应后再通过 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:容许应用的申请报文头,罕用于增加自定义报文头。

流程

简略申请与个别的 Ajax 流程完全相同,仅需浏览器发送 Origin 申请报文头,服务端返回 Access-Control-Allow-Origin 响应报文头即可。
上面详讲简单申请的状况。
假如当初网页源为 http://demo.com,服务端 API 源为 http://api.demo.com,需要申请的办法为 POST,数据类型是 json,自定义报文头 token。

  1. 浏览器查看到,将发动 Ajax 申请的 API 源与以后页面源不同,则进入 CORS 协商;
  2. 数据类型是 json,而已还要自定义报文头,断定本次是简单申请;
  3. 发送预检 OPTION 申请,无关 CORS 的报文头设置如下:

    • 读取以后页面的源写入 Origin: http://demo.com
    • 因为须要 POST 申请,则 Access-Control-Request-Method: POST
    • 因为须要数据类型是 json,也就是默认三种 content-type 不符合要求,还有自定义报文头 token 则 Access-Control-Request-Headers: content-type, token
  4. 服务器接管到预检申请进行响应,无关 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 报文头,但必须)
  5. 浏览器接管到响应,验证 CORS 响应报文头,验证通过则紧接着发送正式 POST 申请,仅需增加 Origin: http://demo.com,其余与失常申请统一;
  6. 服务器接管正式申请,解决后进行响应,仅需增加 Access-Control-Allow-Origin: http://demo.comVary: Origin,其余与失常响应统一;
  7. 浏览器接管到响应,验证 CORS 响应报文头,验证通过则实现申请。

错误处理

  • 服务器谬误能够像个别申请那样捕捉,取得精确的状态码;
  • 当产生跨源相干的谬误时,可在 XMLHttpRequest 对象的 error 事件捕捉到;
  • 跨源相干的谬误总体分两类。

    • 拦挡响应的谬误:比方简略申请的时候,接管到响应数据,但响应报文头验证未通过,这时候尽管从抓包上看曾经实现申请,但浏览器仍然会报错;
    • 限度申请的谬误:比方简单申请的时候,预检返回的响应报文头验证未通过,则浏览器不会发动正式的申请,而是间接报错,这时候抓包是看不到正式申请的。

实际提醒

  • 前端

    • 该计划对前端的影响是非常小的,简直是浏览器主动实现,像个别申请那样发动即可;
    • 错误处理局部有提到两类跨源相干的谬误,这是在调试时须要留神的点。
  • 服务端

    • 不倡议无脑增加 CORS 相干响应报文头,要按需增加,免得造成头部冗余,参考下面的流程,能够大抵可分为两组。

      • 简略申请头部:Access-Control-Allow-Origin 和 Vary 两个即可;
      • 预检申请头部:按需抉择 CORS 的头部,外加 Vary。
    • Access-Control-Max-Age 是一个无效的优化伎俩,它能够缩小频繁的预检申请,节约资源。
    • 除非是公共的第三方 API,不倡议将 Access-Control-Allow-Origin 设为 * 号。
    • 为了安全性,最好验证 Origin 申请报文头,而不是疏忽它,当不符合要求时,能够返回 403 状态码。

具体代码请参考演示案例 CORS 局部源码。

正文完
 0