jtwang7 / JavaScript-Note

JavaScript学习笔记
10 stars 2 forks source link

跨域、同源策略及跨域实现方式和原理 #54

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

参考文章: 九种跨域方式实现原理(完整版)

同源策略

定义

同源策略是一种约定,是浏览器端的实现,是浏览器最核心也最基本的安全功能。 同源 = 「协议 + 域名 + 端口」完全相同

两个不同的域名指向同一个ip地址也非同源。

http://(协议)www(子域名).abc.com(主域名):8080(端口号)/xxx

作用

减少浏览器遭受到 XSS、CSRF 等攻击的可能性。 本质上是因为同源策略限制了:

可跨域加载资源的标签

同源策略限制了浏览器端对资源的跨域获取,但是有三个标签是被允许跨域加载资源的:

  1. <img src="...">
  2. <link href="...">
  3. <script src="...">

常见的跨域场景

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。 对于跨域问题,需要注意几点:

  1. 如果是协议和端口造成的跨域问题,“前台”是无能为力的。
  2. 在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”。

请求跨域了,那么请求到底发出去没有?

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。 你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会? 因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

跨域解决方案

JSONP

原理

利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。 JSONP 请求一定需要对方的服务器做支持才可以。

JSONP 能跨域获取服务端资源,是因为服务端知道这类请求是 JSONP 跨域请求资源的形式,只需要将 JSON 数据以某种形式拼接组织,返回给前端即可。但是假若服务端不按照这种拼接形式返回,那么前端即便使用 JSONP 请求,也无法获得资源,所以说 JSONP 请求必须要服务端的支持。

优缺点

优点:简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。 缺点:仅支持 get 方法,具有局限性;不安全,可能会遭受 XSS 攻击。

建议了解 XSS 和 CSRF 攻击

实现流程

  1. 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
  2. 创建一个 <script> 标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
  3. 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是 show('我不爱你')。
  4. 最后服务器把准备的数据通过 HTTP 协议返回给客户端,因为服务端返回的字符串在客户端解析后,是一个立即执行的函数语句,因此客户端会自动执行之前声明的回调函数(show),对返回的数据进行操作。

前端代码实现

function jsonp({ url, params, callback }) {
  // JSONP 都是 GET 和异步请求的,不存在其他的请求方式和同步请求,因此 jsonp 函数将返回一个 Promise 对象包裹异步逻辑。
  return new Promise((resolve, reject) => {
    // 创建 script 标签,作为跨域的主体
    let script = document.createElement('script');

    // 全局挂载一个回调函数,函数参数为要跨域请求的目标数据(服务端返回的 JSON 数据)
    window[callback] = function(data) {
      // 调用该函数,会将服务器的数据传递出去,并移除 script 标签 (因为跨域请求结束,script 标签使命完成)。
      resolve(data);
      document.body.removeChild(script);
    }

    // 拼接 url 地址,作为 script 标签的 src 属性
    // 除了正常传递的 query 参数外,我们还需要在请求参数中,拼接 callback=xxx
    params = { ...params, callback }
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`

    // 将 script 标签添加到 DOM 树中,浏览器端解析 DOM 时会自动执行 JSONP 请求。
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})

后端代码实现

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // Iloveyou
  console.log(callback) // show

  // 重点:服务端将数据拼接到 callback 所带函数的参数中返回。
  // 本例拼接结果为:show('我不爱你'),返回到前端,前端解析这段字符串,会立即执行 show('我不爱你') 这个函数执行语句,然后将内容传递出去。
  res.end(`${callback}('我不爱你')`)
})
app.listen(3000)

JSONP & AJAX

JSONP 和 AJAX 相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但 AJAX 属于同源策略,JSONP 属于非同源策略(跨域请求)。此外,AJAX 支持多种数据请求的方式,而 JSONP 只支持 get 请求。

CORS

参考:跨域资源共享 CORS 详解

概念

CORS 是一个 W3C 标准,全称"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求(处理复杂请求的"预检"请求),但用户不会有感觉。 因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。 虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。

简单请求

只要同时满足以下两大条件,就属于简单请求

  1. 使用下列请求方法之一:

    • GET
    • HEAD
    • POST

      都是 HTTP/1.0 所支持的 HTTP 请求方法

  2. HTTP的头信息不超出以下几种字段:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type

其中 Content-Type 的值仅限于下列三者之一:

对于简单请求,浏览器检测到请求跨域时,直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段:

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

请求头信息中的 Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

服务端接收到 CORS 请求,会在服务端域名白名单中查找该 Origin,如果 Origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

响应 CORS 的头信息之中,有三个与 CORS 请求相关的字段,都以 Access-Control- 开头(都可在服务器端进行配置):

  1. Access-Control-Allow-Origin 该字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个 *,表示接受任意域名的请求。
  2. Access-Control-Allow-Credentials 该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。若服务端 Access-Control-Allow-Credentials 设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可。
  3. 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 字段的值。

如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,就知道出错了,从而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。

注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

关于跨域场景下 Cookie 的传输,还有必要进一步的说明:CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials 字段为 true:Access-Control-Allow-Credentials: true,另一方面,开发者必须在 AJAX 请求中打开 withCredentials 属性:

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

有些浏览器对于 withCredentials 的默认行为可能不太一致,因此最好显式设置 withCredentials 开关

需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie 也无法读取服务器域名下的Cookie。

复杂请求

简单请求之外的都算作复杂请求。 复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求。 浏览器会先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。 预检请求的 HTTP 头信息如下:

OPTIONS /cors HTTP/1.1
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...

"预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求来自哪个源。 除了Origin字段,"预检"请求的头信息包括两个特殊字段。

  1. Access-Control-Request-Method 该字段是必须的,用来列出浏览器的 CORS 复杂请求会用到哪些HTTP方法,上例是 PUT。
  2. Access-Control-Request-Headers 该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header。

服务器收到"预检"请求以后,在配置中检查 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,确认允许跨源请求,就可以做出回应。如果服务器否定了"预检"请求,会返回一个正常的 HTTP 回应,不包含任何的 CORS 相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest 对象的 onerror 回调函数捕获。 服务器正确响应 CORS 预检请求时,会返回如下响应头信息:

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
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

其中,Access-Control-Allow-Origin字段最关键,它告诉浏览器其字段值下的域名可以请求数据,若字段值为 * 表示同意任意跨源请求。 此外,关于复杂请求还有一些其他的 CORS 响应字段:

  1. Access-Control-Allow-Methods 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
  2. Access-Control-Allow-Headers 如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
  3. Access-Control-Max-Age 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

一旦服务器通过了"预检"请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。详细头信息请参考简单请求。

CORS & JSONP

CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。 JSONP 只支持 GET 请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。

postMessage

参考:postMessage可太有用了

postMessage 是 html5 引入的 API, postMessage() 方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档,多窗口,跨域消息传递.多用于窗口间数据通信,这也使它成为跨域通信的一种有效的解决方案.

postMessage API

发送数据:调用 postMessage API

otherWindow.postMessage(message, targetOrigin, [transfer]);
  1. otherWindow 窗口的一个引用。比如 iframe 的 contentWindow 属性;执行 window.open 返回的窗口对象;或者是命名过的或数值索引的 window.frames
  2. message 要发送到其他窗口的数据,它会被自动序列化.这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化.
  3. targetOrigin 通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,指定后只有对应 origin 下的窗口才可以接收到消息,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。 targetOrigin 设置为通配符"*"表示可以发送到任何窗口,但通常处于安全性考虑不建议这么做.如果想要发送到与当前窗口同源的窗口,可设置为"/"
  4. transfer | 可选属性 是一串和message同时传递的Transferable对象,这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权

接收数据:监听 message 事件的发生

window.addEventListener("message", receiveMessage, false) ;
function receiveMessage(event) {
     var origin= event.origin;
     console.log(event);
}

message 事件包含的 event 对象有以下四个属性:

  1. data:指的是从其他窗口发送过来的消息对象;
  2. type:指的是发送消息的类型;
  3. source:指的是发送消息的窗口对象;
  4. origin:指的是发送消息的窗口的源;

举例

假设 http://localhost:3000/a.html 页面需要向 http://localhost:4000/b.html 传递数据,并接收 b 页面返回的数据,需要跨域操作。 在 a 页面中,我们内嵌一个 iframe 窗口,去主动加载 b 页面,并设置一个 onload 事件,当 iframe 窗口加载 b 页面完成后,触发注册的事件回调函数,里面可以设置向 b 页面的数据发送以及对 b 页面传回数据的监听响应。 同样的,在 b 页面中,我们需要监听 message 事件,做一些响应和传递数据的操作。

<!-- a.html -->
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> <!-- 等待 iframe 加载完成触发 onload 事件 -->
<script>
  function load() {
    let frame = document.getElementById('frame')
    frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
    window.onmessage = function(e) { //接受返回数据
      console.log(e.data) //我不爱你
    }
  }
</script>
<!-- b.html -->
<script>
  window.onmessage = function(e) {
    console.log(e.data) //我爱你
    e.source.postMessage('我不爱你', e.origin)
 }
</script>

WebSocket

Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信。由于 WebSocket 本身不存在跨域问题,所以我们可以利用 WebSocket 来进行非同源之间的通信。

原理

WebSocket 和 HTTP 都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。

WebSocket 在建立连接时虽然需要借助 HTTP 协议,但是连接本身并没有数据传递,不存在跨域,并且连接建立好了之后,连接就会自动更换协议,升级为 WebSocket,此时 client 与 server 之间的双向通信就与 HTTP 无关了。由于数据的传输不再是通过发送和监听响应这种单向的方式实现,既然不存在响应,那么同源策略中浏览器拦截响应数据的方式也不再存在了,自然就不存在跨域问题。

Node 中间件代理

原理

同源策略是浏览器需要遵循的标准,但服务器向服务器请求就无需遵循同源策略。因此我们可以设置代理服务器来转发浏览端向服务端的请求。 代理服务器的工作职责:

浏览器和代理服务器之间的数据交互要遵循同源策略,因此代理服务器要和浏览器同源,但服务器之间不需要遵循同源策略,因此代理服务器可以自由地向其他服务器进行数据请求和响应。代理服务器充当一个中间人角色,专门处理浏览器和服务器间不能直接进行的操作。

nginx 反向代理

实现原理类似于 Node 中间件代理, nginx 会将同源的本地请求收集起来,并分发到各个服务器中,并将服务器响应的结果收集并返回。与 node 中间件代理的区别在于,nginx 更偏向底层服务端的逻辑处理,因此它实际上与服务端处于统一内网中,通过设置前端服务的域名,获得一个跳板机保证同源访问,将访问请求代理到设置的后端服务域名中。 利用 nginx 解决跨域问题,需要你搭建一个中转 nginx 服务器,用于转发请求。

优势

使用 nginx 反向代理实现跨域,是最简单的跨域方式

  1. 实现方式简单:只需要修改 nginx 的配置即可解决跨域问题,不需要修改任何代码,并且不会影响服务器性能。
  2. 兼容性好:支持所有浏览器,支持 session。

正向代理

正向代理会把访问某网站服务器 server 的网页请求,代理到一个可以访问该网站的代理服务器 proxy,这个代理服务器 proxy 把该网站服务器 server 上的网页内容获取,再转发给客户。

在正向代理中,客户端和代理服务器处于一个局域网中,可以直接互相访问,因此正向代理服务器可以接受多个客户端的请求;接受请求后,代理服务器通过代理用户端的请求来向域外服务器请求响应内容。(多对一)

反向代理

在反向代理中,客户端向服务端发送的请求,会先被代理服务器proxy接收,这个代理服务器将把请求代理到和自己属于同一个LAN(局域网)下的内部服务器上。与正向代理不同的是,反向代理的对象不是客户端而是服务端。反向代理服务器向客户端提供了一个统一的代理入口,客户端的请求,都先经过这个proxy服务器,至于在内网真正访问哪台服务器内容,由这个proxy去控制,因此,客户端经过反向代理,是无法选择具体请求哪台服务器的。

代理服务器和真正server服务器可以直接互相访问,属于一个LAN(服务器内网);代理对用户是透明的,即无感知。不论加不加这个反向代理,用户都是通过相同的请求进行的,且不需要任何额外的操作;代理服务器通过代理内部服务器接受域外客户端的请求,并将请求发送到对应的内部服务器上。(一对多)

反向代理优势

  1. 安全及权限。使用反向代理后,客户端将无法直接通过请求访问真正的内容服务器,而必须首先通过Nginx。可以通过在Nginx层上将危险或者没有权限的请求内容过滤掉,从而保证了服务器的安全。
  2. 负载均衡。例如一个网站的内容被部署在若干台服务器上,可以把这些机子看成一个集群,那么Nginx可以将接收到的客户端请求“均匀地”分配到这个集群中所有的服务器上(内部模块提供了多种负载均衡算法),从而实现服务器压力的负载均衡。此外,nginx还带有健康检查功能(服务器心跳检查),会定期轮询向集群里的所有服务器发送健康检查请求,来检查集群中是否有服务器处于异常状态,一旦发现某台服务器异常,那么在以后代理进来的客户端请求都不会被发送到该服务器上(直到后面的健康检查发现该服务器恢复正常),从而保证客户端访问的稳定性。

window.name + iframe

原理

window.name 属性特点:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。 这就意味着我们可以用 window.name 作为不同域间数据的载体,通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

实例

假设我们需要从 a 页面跨域请求 c 页面中的数据,其中 a.html 和 b.html 是同域的,都是 http://localhost:3000; 而 c.html 是 http://localhost:4000。 b.html 为中间代理页,与 a.html 同域,内容为空。

<!-- a.html(http://localhost:3000/a.html) -->

<!-- 利用 iframe 访问跨域的页面,跨域页面需要将传输的数据赋值到 window.name 属性上,并在完成加载时执行 load 函数 -->
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
  let first = true
  // onload 事件会触发2次:
  function load() {
    if(first){
      // 第1次加载跨域页完成后触发,此时跨域页将数据留存于 window.name,同时切换到同域代理页面
      let iframe = document.getElementById('iframe');
      iframe.src = 'http://localhost:3000/b.html';
      first = false;
    }else{
      // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
      console.log(iframe.contentWindow.name);
    }
  }
</script>
<!-- c.html(http://localhost:4000/c.html) -->

<!-- c 页面需要支持这种跨域模式,将要传递的数据赋值给 window.name -->
<script>
  window.name = '我不爱你'  
</script>

我们可以将 b 页面看作跨域数据的一个载体,用一个空页面当作仓库,c 页面将数据通过 window.name 保存在这个 b 仓库中,a 页面则通过 window.name 从该仓库中取出数据。> 页面的请求访问实际是用 iframe 做了替代,而数据的流转则用了不受同源策略限制的 window.name 属性

location.hash + iframe

原理

通过在 url 的 hash 值传递数据,一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。

浏览器同源策略拦截的是服务端返回的响应请求,因此一开始是可以跨域发出请求的,a.html 到 c.html 的数据传递不需要用到中间代理页面 b.html

实例

<!-- a.html -->

<!-- a 页面通过 url-hash 传递数据给跨域页 c -->
<iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
<script>
  // 监听 hash 变化
  window.onhashchange = function () {
    console.log(location.hash);
  }
</script>
<!-- b.html -->
<script>
  // b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
  window.parent.parent.location.hash = location.hash 
</script>
<!-- c.html -->
<script>
  let iframe = document.createElement('iframe');
  iframe.src = 'http://localhost:3000/b.html#idontloveyou';
  document.body.appendChild(iframe);
</script>

document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

实现原理

两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。