> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/frontend/01-ji-chu-03-tan-tan-qian-duan-kua-yu-wen-ti-ji-jie-jue-fang-fa.md).

# 基础 03 谈谈前端跨域问题及解决方法

所谓跨域，是指一个域下的文档或脚本试图去请求另一个域下的资源。

广义的跨域：

* 资源跳转：`a` 标签、重定向、表单提交；
* 资源嵌入：`<link>`、`<script>`、`<img>`、`<frame>` 等标签，`background:url()`、`@font-face()` 等文件外链；
* 脚本请求：Js 发起的 Ajax 请求、`Dom` 和 `Js` 对象的跨域操作（如`iframe`）等；

狭义的跨域：也就是我们常说的跨域问题，是指由浏览器同源策略限制的一类请求场景。典型的是 Ajax 跨域请求。

## 同源和同站

域名级数是指一个域名由多少级组成，域名的各个级别被 `.` 分开，简而言之，有多少个点就是几级域名。

一般来说就是比如 `www.baidu.com`：

* 顶级域名： .com；
* 一级域名（也有称顶级域名）： baidu .com；
* 二级域名： [www.baidu](http://www.baidu) .com、aaa.baidu .com、bbb.baidu .com 二级域名；

### 同源的定义

两个 URL 的协议、域名、端口都相同的话，则这两个 URL 是同源。

同源的例子：

| <p><http://example.com/app1/index.html><br><http://example.com/app2/index.html></p> | 同源。协议、域名都相同                 |
| ----------------------------------------------------------------------------------- | --------------------------- |
| <p><http://example.com:80><br><http://example.com></p>                              | 同源。协议、域名都相同，服务器默认的http端口是80 |

不同源的例子：

| <p><http://example.com/app1><br><https://example.com/app2></p>                        | 不同源。协议不同 |
| ------------------------------------------------------------------------------------- | -------- |
| <p><http://example.com><br><http://www.example.com><br><http://myapp.example.com></p> | 不同源。域名不同 |
| <p><http://example.com><br><http://example.com:8080></p>                              | 不同源。端口不同 |

**注意：** IE 浏览器中两个相互之间高度互信的域名，如公司域名，不受同源策略限制。

**注意：** IE 浏览器未将端口号纳入到同源策略的检查中。因此，同协议、同域名，但是端口不同的链接不受同源策略限制。如：

```shell
https://company.com:80/index.html
https://company.com:81/index.html
```

### 同站的定义

同站的判断宽松：只要两个 URL 的 eTLD+1 相同即可，**不需要考虑协议和端口**。

其中，eTLD 表示**有效顶级域名**，注册于 Mozilla 维护的公共后缀列表（Public Suffix List）中，例如，`.com、.co、.uk、.github.io` 等。而 eTLD+1 则表示，有效顶级域名+二级域名，例如 `taobao.com` 等。

比如：`www.taobao.com` 和 `www.baidu.com` 是跨站，`www.a.taobao.com` 和 `www.b.taobao.com` 是同站，`a.github.io` 和`b.github.io` 是跨站。

同源策略作为浏览器的安全基石，其同源判断是比较严格的，而相对而言，Cookie 中的同站判断就比较宽松。

## 什么是同源策略?

\*\*同源策略（same-origin policy）\*\*是一个重要的安全策略，它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。它的目的，是为了保证用户信息的安全，防止恶意的网站窃取数据。如：CSRF（跨站请求伪造，Cross-site request forgery）攻击。

1995年，同源政策由 Netscape 公司引入浏览器。目前，所有浏览器都实行这个政策。随着互联网的发展，同源政策越来越严格。

如果非同源，共有三种行为受到限制：

* Cookie、LocalStorage 和 IndexDB 无法读取；
* DOM 无法获得；
* Ajax 请求不能发送。

但是以下标签通常是允许跨域加载资源的：

* `<img src=''>`
* `<video src=''>`
* `<audio src=''>`
* `<link href=''>`
* `<script src=''>`
* `<iframe src=''>`

另外，还有一些注意点：

* `<script src='' />` 标签嵌入跨域脚本，语法错误信息无法被当前页面脚本捕捉。
* `@font-face` 引入的字体，在不同浏览器有不同的限制。
* JavaScript API 访问跨域 window 对象时，Window、Location 对象的方法和属性受限制。具体如下：
  * Window 可跨域访问：window\.blur、window\.close、window\.focus、window\.postMessage
  * Window 只读属性：window\.closed、window\.frames、window\.length、window\.opener、window\.parent、window\.self、window\.top、window\.window
  * Window 读/写属性：window\.location
  * Location 可跨域访问：location.replace、location.href = ''（可写，不可读）

## 常见的跨域场景

### Cookie跨域

Cookie 是服务器写入浏览器的一小段信息，只有同站的网页才能共享。

注意：\*\*浏览器本身是允许 cookie 同站通信的。\*\*即，只要两个 URL 的 eTLD+1 相同即可，**不需要考虑协议和端口**。

两个网页一级域名相同，只是二级域名不同，浏览器允许通过设置 `document.domain` 共享 Cookie。

如：A 网页是 <http://w1.example.com/a.html，B> 网页是 <http://w2.example.com/b.html，那么只要设置相同的> `document.domain`，两个网页就可以共享 Cookie。

```javascript
document.domain = 'example.com'
```

另外，服务器在设置 Cookie 的时候，指定 Cookie 的所属域名为一级域名+二级域名。这样的话，其子域名不用做任何设置，都可以读取这个 Cookie。

```java
Set-Cookie: key=value; domain=.example.com; path=/
```

### iframe跨域

iframe 是在当前页面嵌入其他页面，可以是同源，也可以是非同源页面。每个 iframe 有自己的窗口，即有自己的 window 对象。iframe 的脚本可以获得父窗口和子窗口。但是，只有在同源的情况下，父窗口和子窗口才能通信；如果跨域，就无法拿到对方的 DOM。

比如，父窗口运行下面的命令，如果 iframe 内嵌的页面非同源，就会报错：

```js
document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
```

反之亦然，子窗口获取主窗口的 DOM 也会报错。

```js
window.parent.document.body
// Uncaught DOMException: Blocked a frame with origin "http://xxx.xxx.x.xxx:xxxx" from accessing a cross-origin frame.
```

这种情况不仅出现在 iframe 窗口，还出现在 `window.open` 方法打开的窗口，只要跨域，父窗口与子窗口之间就无法通信。

如果两个窗口一级域名相同，只是二级域名不同，那么可以上述修改 `document.domain` 的方法规避同源政策。

### LocalStorage跨域

LocalStorage 和 IndexedDB 都是受同源政策限制的，无法跨域访问。

### AJAX跨域

浏览器的同源政策规定，AJAX 请求只能发给同源的网址，否则就报错。

## 1.CORS（跨域资源共享）

CORS，跨域资源共享（Cross-origin resource sharing）， 是一个 W3C 标准。它允许浏览器向非同源的服务器，发出 `XMLHttpRequest` 请求，从而克服了 Ajax 只能同源使用的限制。

**这是解决跨域 Ajax 请求的根本方法。**

**CORS 需要浏览器和服务器同时支持**。目前，所有浏览器都支持该功能，IE 浏览器不能低于IE10。

整个 CORS 通信过程都是浏览器自动完成，不需要用户参与。对于开发者来说，CORS 通信与同源的 Ajax 通信没有差别，代码完全一样。浏览器一旦发现 Ajax 请求跨域，就会自动添加一些附加的头信息，有时还会多出一次附加的请求，但用户不会有感觉。

因此，**实现 CORS 通信的关键是服务器**。只要服务器实现了 CORS 接口，就可以跨域通信。

CORS 允许在下列场景中使用跨域请求：

* 由 XMLHttpRequest 或 Fetch 发起的跨域请求；
* @font-face 引用的跨域字体资源；
* WebGL 贴图；
* 使用 drawImage 将 Images/video 资源绘制到 canvas。

### CORS 实现

客户端无需处理，**浏览器会自动在请求头中增加一些字段。** 如果要发送跨域 Cookie，请求头要设置 `withCredentials` 属性。

服务端响应头要设置 `Access-Control-Allow-Origin`、`Access-Control-Allow-Headers`、`Access-Control-Allow-Credentials`属性。

### 请求头字段

当浏览器发起跨域请求时，会自动添加如下请求头字段。**请注意，这些字段无须开发者手动添加。**

* Origin：表明预检请求或实际请求的源站，值为源站 URI。它不包含任何路径信息，只是服务器名称。
* Access-Control-Request-Method：用于预检请求。其作用是，将实际请求所使用的 HTTP 方法告诉服务器。

  ```js
  Access-Control-Request-Method: <method>
  ```
* Access-Control-Request-Headers：用于预检请求。其作用是，将实际请求所携带的首部字段告诉服务器。

  ```js
  Access-Control-Request-Headers: <field-name>[, <field-name>]*
  ```

### 响应头字段

* Access-Control-Allow-Origin：origin | \*，设置允许跨域的源，origin 指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求，服务器可以指定该字段的值为通配符 `*`，表示允许来自所有域的请求。
* Access-Control-Expose-Headers：让服务器把允许浏览器请求头中的字段放入白名单。

  在跨域访问时，XMLHttpRequest 对象的 `getResponseHeader()` 方法只能拿到一些最基本的响应头字段，`Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma`，如果要访问其他字段，则需要服务器设置本响应头。
* Access-Control-Allow-Credentials：true | false：指定了当浏览器的 `credentials` 设置为 true 时是否允许浏览器读取 response 的内容。当用在对 preflight 预检测请求的响应中时，它指定了实际的请求是否可以使用 `credentials`。

  请注意：简单 GET 请求不会被预检；如果对此类请求的响应中不包含该字段，这个响应将被忽略掉，并且浏览器也不会将相应内容返回给网页。

### withCredentials 属性

CORS 请求默认不包含 Cookie 信息（以及 HTTP 认证信息等）。如果需要包含 Cookie 信息，客户端和服务器都要加配置：

* 服务端响应头设置 `Access-Control-Allow-Credentials: true`；
* 客户端请求头设置 `withCredentials = true`。

否则，即使服务器同意发送 Cookie，浏览器也不会发送。或者，服务器要求设置 Cookie，浏览器也不会处理但是，如果省略`withCredentials` 设置，有的浏览器还是会一起发送 Cookie。这时，可以指定 `withCredentials = false`。

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

### 两种请求

CORS 请求分成两类：简单请求（simple request）和非简单请求（not-so-simple request）。

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

* 请求方法是这三种方法之一：HEAD、GET、POST
* HTTP 的头信息不超出以下几种字段。
  * Accept
  * Accept-Language
  * Content-Language
  * Last-Event-ID
  * Content-Type：只限于三个值`application/x-www-form-urlencoded`、`multipart/form-data`、`text/plain`

凡是不同时满足上面两个条件，就属于非简单请求。**一句话，简单请求就是简单的 HTTP 方法与简单的 HTTP 头信息的结合。**

这样划分的原因是，表单在历史上一直可以跨域发出请求。简单请求就是表单请求，浏览器沿袭了传统的处理方式，不把行为复杂化，否则开发者可能转而使用表单，规避 CORS 的限制。对于非简单请求，浏览器会采用新的处理方式。

### 简单请求的跨域流程

对于简单请求，浏览器直接发出 CORS 请求。具体来说，就是在头信息之中，增加一个`Origin`字段（包含协议 + 域名 + 端口）。服务器根据这个值，决定是否同意这次请求。

如果 `Origin` 指定的源，不在许可范围内，服务器会返回一个正常的 HTTP 回应。浏览器发现这个回应的头信息没有包含 `Access-Control-Allow-Origin` 字段，就知道出错了，从而抛出一个错误，被 `XMLHttpRequest` 的 `onerror` 回调函数捕获。注意，这种错误无法通过状态码识别，因为 HTTP 回应的状态码有可能是 200。

### 非简单请求的跨域流程

非简单请求是那种对服务器提出特殊要求的请求，比如请求方法是 `PUT` 或 `DELETE`，或者 `Content-Type` 字段的类型是 `application/json`。

非简单请求的 CORS 请求，会在正式通信之前，增加一次 HTTP 查询请求，称为“预检”请求（preflight）。浏览器先询问服务器，当前网页所在的域名是否在服务器的许可名单之中，以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复，浏览器才会发出正式的 `XMLHttpRequest` 请求，否则就报错。这是为了防止这些新增的请求，对传统的没有 CORS 支持的服务器形成压力，给服务器一个提前拒绝的机会，这样可以防止服务器大量收到 `DELETE` 和 `PUT` 请求，这些传统的表单不可能跨域发出的请求。

**预检请求的回应：**

上述讲到的“预检”请求，用的请求方法是 `OPTIONS`，表示这个请求是用来询问的。头信息里面包含：

* `Origin`，表示请求来自哪个源。
* `Access-Control-Request-Method`，列出浏览器的 CORS 请求会用到哪些 HTTP 方法，上例是`PUT`。
* `Access-Control-Request-Headers`，该字段是一个逗号分隔的字符串，指定浏览器 CORS 请求会额外发送的头信息字段。

服务器收到“预检”请求以后，检查了 `Origin`、`Access-Control-Request-Method`和`Access-Control-Request-Headers` 字段以后，确认允许跨域请求，就可以做出回应。

如果服务器否定了“预检”请求，会返回一个正常的 HTTP 回应，但是没有任何 CORS 相关的头信息字段，或者明确表示请求不符合条件。

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

## 2.Nginx反向代理

使用 Nginx 反向代理实现跨域，是最简单的跨域方式。只需要修改 Nginx 的配置即可解决跨域问题，支持所有浏览器，支持 session，不需要修改任何代码，并且不会影响服务器性能。

**实现思路：** 通过 Nginx 配置一个代理服务器（与发送请求的页面同源）做跳板机，将请求反向代理到真正的 URL，并且可以修改一些头部信息，如：Cookie。

**安装：** [下载安装包](http://nginx.org/en/download.html)或者 `npm i nginx`，运行 `nginx -v` 查看是否安装成功。

**配置：** 修改 `nginx.conf` 文件（Mac：`/usr/local/etc/nginx/nginx.conf`）：

```shell
# 将 localhost:9700 的请求转发到 localhost:9800
http {
    server {
        listen       9700;
        server_name  localhost;
        location /api {
            rewrite ^/b/(.*)$ /$1 break; # 去除本地接口/api前缀, 否则会出现404
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://localhost:9800;
        }
    }
}
```

**更新配置：** 执行 `nginx -s reload` 重新载入配置。

了解更多 Nginx，可查看 [**Nginx基础使用**](https://app.gitbook.com/s/-M8fDLTBWl2H-MOzligj/tech/10nginx-ji-chu-shi-yong)。

## 3.Node中间件代理

实现原理：**同源策略是浏览器需要遵循的标准，而如果是其他服务器（如：node）向服务器请求就无需遵循同源策略。**

代理服务器需要做如下事情：

* 接受客户端请求 ；
* 将请求转发给服务器；
* 拿到服务器响应数据；
* 将响应转发给客户端。

**具体实现：**`http://127.0.0.1:5500/index.html` 通过代理服务器提供的 `http://localhost:3000` 接口，向目标服务器`http://localhost:4000` 请求数据。

```js
// http://127.0.0.1:5500/index.html
var xhr = new XMLHttpRequest()
xhr.open('post', 'http://localhost:3000/', true)
xhr.withCredentials = true
xhr.send(null)
```

```js
// 代理服务器（node1.js），http://localhost:3000
const http = require('http')
const server = http.createServer((request, response) => {
    // 代理服务器设置 CORS 的响应头字段
    response.writeHead(200, {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
        'Access-Control-Allow-Headers': 'Content-Type'
    })
    // 第二步：将请求转发给服务器
    const proxyRequest = http.request(
        {
            host: '127.0.0.1',
            port: 4000,
            url: '/',
            method: request.method,
            headers: request.headers
        },
        serverResponse => {
            // 第三步：收到服务器的响应
            var body = ''
            serverResponse.on('data', chunk => {
                body += chunk
            })
            serverResponse.on('end', () => {
                console.log('The data is ' + body)
                // 第四步：将响应结果转发给浏览器
                response.end(body)
            })
        }
    ).end()
})
server.listen(3000, () => {
    console.log('代理服务器：http://localhost:3000')
})
```

```js
// 目标服务器（node2.js），http://localhost:4000
const http = require('http')
const data = { title: 'frontend', password: '123456' }
const server = http.createServer((request, response) => {
  if (request.url === '/') {
    response.end(JSON.stringify(data))
  }
})
server.listen(4000, () => {
  console.log('目标服务器：http://localhost:4000')
})
```

## 4.webpack-dev-server

Webpack 内置了 `webpack-dev-server` 插件，`webpack-dev-server` 使用 `http-proxy-middleware` 实现跨域代理。

`webpack.config.js` 配置：

```javascript
module.exports = {
    //...
    devServer: {
        proxy: {
            '/api': {
                target: 'http://www.baidu.com/',
                pathRewrite: {'^/api' : ''}, // /api/users -> /users
                changeOrigin: true, // target是域名的话，需要这个参数，
                secure: false, // 设置支持https协议的代理
            },
            '/api2': {
                .....
            }
        }
    }
}
```

```js
module.exports = {
    //...
    devServer: {
        proxy: [{
            context: ['/auth', '/api'],
            target: 'http://localhost:3000',
        }]
    }
};
```

## 5.WebSocket

**原理：** 同源政策是浏览器对 `http/https` 协议的限制，WebSocket 是一种使用 `ws://`（非加密）和`wss://`（加密）作为前缀的通信协议，**浏览器不对该协议实行同源政策**。

客户端发送 Websocket 请求：

```js
let socket = new WebSocket('ws://localhost:3000')
socket.onopen = function () {
    socket.send('Hello WebSockets!')
}
socket.onmessage = function (e) {
    console.log(`Received Message: ${e.data}`)
}
```

Node.js 服务器：

```js
const WebSocket = require("ws");
let wss = new WebSocket.Server({ port: 3000 })

// 监听连接
wss.on("connection", function(ws) {
    ws.on("message", function(data) {
        console.log(data)
        ws.send("Hello Client！")
    })
})
```

了解更多 WebSocket ，可查看 [**前端进阶篇：WebSocket**](broken://spaces/-M8fDLTBWl2H-MOzligj/pages/fALsuzZd2tWckXuwebBy)。

## 6.JSONP

JSONP 是一种非官方的跨域数据交互协议。它的实现主要依赖于两点：

* `<script>` 标签不受同源策略限制，可以从不同域加载资源；
* `<script>` 请求回来的是一段 Js 代码，并且会立即在当前页面执行。

JSONP 的理念：与服务端约定好一个回调函数名，服务端接收到请求后，将返回一段 Javascript，在这段 Javascript 代码中调用了约定好的回调函数，并且将数据作为参数进行传递。当网页接收到这段 Javascript 代码后，就会执行这个回调函数，这时数据已经成功传输到客户端了。

首先，网页动态插入 `<script>` 元素，由它向跨域网址发出请求。

```js
function loadScriptFile (src) {
    const script = document.createElement('script')
    script.setAttribute("type","text/javascript")
    script.src = src
    document.body.appendChild(script)
}

window.onload = function () {
    loadScriptFile('http://example.com/ip?callback=foo')
}

function foo(data) {
    console.log('接收: ' + data.ip)
}
```

注意：该请求的查询字符串有一个 `callback` 参数，用来指定回调函数的名字，这对于 JSONP 是必需的。

服务器收到这个请求以后，会将数据放在回调函数的参数位置返回。

```javascript
foo({
    "ip": "8.8.8.8"
})
```

由于 `<script>` 元素请求的脚本，直接作为代码运行。这时，只要浏览器定义了 `foo` 函数，该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象，而不是字符串，因此避免了使用 `JSON.parse` 的步骤。

\*\*JSONP 的优点是：\*\*简单适用，兼容性更好，在更加古老的浏览器中都可以运行。虽然绝大多数现代的浏览器都已经支持 CORS，但一些比较老的浏览器只支持 JSONP。

\*\*JSONP 的缺点是：\*\*它只支持 GET 请求，而不支持 POST 请求等其他类型的 HTTP 请求。

## 7.window\.postMessage

`window.postMessage()` 方法提供了一种受控机制来规避此限制，只要正确的使用，就可以安全地实现跨域通信。

从广义上讲，一个窗口可以获得对另一个窗口的 window 对象（如：`targetWindow = window.opener`），然后在窗口上调用 `targetWindow.postMessage()` 方法发送消息，该消息可以被目标窗口的全局事件 `message` 接收。

```javascript
targetWindow.postMessage(message, targetOrigin, [transfer])
```

* **targetWindow：** 其他窗口的一个引用。通过以下方法获取：
  * 调用 `window.open()` 返回的窗口对象；
  * 页面上的 iframe 对象：比如 iframe 的 `contentWindow` 属性、、或者是命名过或数值索引的[window.frames](https://developer.mozilla.org/en-US/docs/DOM/window.frames)。
* \*\*message：\*\*将要发送到其他 window 的数据。
* **targetOrigin：** 指定哪些窗口能接收该消息，其值可以是字符串 `*` ，表示无限制，也可以是一个 URI。如果目标窗口的协议、域名、端口这三者的任意一项不匹配 `targetOrigin` 提供的值，那么消息就不会被发送。
* \*\*transfer \*\* 可选，是一串和 `message` 同时传递的 `Transferable` 对象。

以 `window.open` 为例：

```javascript
// 父页面：23-commumication(A).html
var targetWindow = window.open('23-commumication(B).html?type=5')
window.addEventListener('message', (e) => {
    console.log(`[postMessage]接收：${e.data}`)
})
```

```javascript
// 子页面：23-commumication(B).html
window.addEventListener('message', (e) => {
    console.log(`[postMessage]接收：${e.data}`)
})
```

父页面向子页面发送消息：

```javascript
targetWindow.postMessage('Hello, This is Page A!')
```

子页面向父页面发送消息：

```javascript
window.opener.postMessage('Hello, This is Page B!')
```

**注意：** 如果不是当前页面调用 `window.open()` 打开或者 `iframe` 内嵌的页面，则跟当前页面没有任何关系，即取不到页面窗口的 window 对象，也就无法使用 `postMessage` 方法发送信息。

**注意：** 如果 `window.open()` 打开或者 `iframe` 内嵌的页面是同源页面，则 `postMessage` 方法中的第二个参数是不传；如果是非同源页面，则第二个参数必传（可以传 `*` 或 URI），否则会报错，如：

```shell
Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://localhost:5200') does not match the recipient window's origin ('http://localhost:5300').
```

### postMessage+window\.open

**实现思路：** 在页面 A （父窗口）调用 `window.open()` 打开多个子页面 B、C...（即多个子窗口），并将子页面的 window 对象存在一个数组中。如果页面 A 需要广播消息，只需遍历数组，调用数组中 window 对象的 `postMessage` 方法发送消息即可；如果子页面 B 需要广播消息，则调用 `window.opener.postMessage` 方法向页面 A 发送消息，页面 A 再在 `message` 事件中遍历数组，调用数组中 window 对象的 `postMessage` 方法发送消息。

首先，在页面 A 把 `window.open()` 方法打开的页面的 window 对象储存到数组，然后定义 `message` 事件的监听函数：

```javascript
var targetWindows = []
function fnOpenWindow(url) {
    let tWindow = window.open(url)
    targetWindows.push(tWindow)
}

window.addEventListener('message', (e) => {
    if (e.data.from === location.href) {
        return
    }
    targetWindows = targetWindows.filter(w => !w.closed)
    targetWindows.forEach(w => w.postMessage(e.data, '*'))
    console.log('[window.open]接收信息：', `msg-${e.data.msg}，from-${e.data.from}`)
    document.querySelector('.box_05').querySelector('.content').innerHTML = e.data.msg
})
```

然后，在各子页面定义 `message` 事件的监听函数：

```javascript
window.addEventListener('message', (e) => {
    if (e.data.from === location.href) {
        return
    }
    console.log('[window.open]接收信息：', `msg-${e.data.msg}，from-${e.data.from}`)
})
```

最后，在子页面（如 B）中调用 `postMessage` 方法，实现广播消息：

```javascript
window.opener.postMessage({
    msg: "Hello, This is Page B!",
    from: location.href
}, '*')
```

[window.open DEMO Page A](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=5)

**注意：** 子页面的 window 对象上的 `opener` 属性，指向的是父页面的 window 对象，因此，子页面获得了父页面的控制权。出于安全考虑，可以限制子页面的 `window.opener` 属性。如果是 `a` 标签跳转，可以加 `rel=noopener` 属性或者 `rel=noreferrer`，如果是 Js 调用 `window.open()` 方法，可以：

```javascript
let childWindow = window.open(url)
childWindow.opener = null;
```

**注意：** 该方案使用有限制，被打开页面必需要有 `window.opener` 属性，并且指向打开该页面的页面 window 对象。即，如果子页面不是通过在另一个页面内的 `window.open` 打开的（如直接在地址栏输入链接或者从其他网站链接过来），则两者之间没有联系，无法通信。

### postMessage+iframe

该方案与 `postMessage+window.open` 类似，区别在于：适用于在当前页面内嵌 iframe 子页面的场景。父页面调用类似 `window.frames[0].postMessage()` 方法向子页面发送信息，子页面在 `message` 事件中调用 `window.parent.postMessage()` 或者 `e.source.postMessage()` 向父页面通信。

**实现思路：** 非同源页面 A 和 B ，A 内嵌一个 iframe 页面 C1，B 内嵌一个 iframe 页面 C2，C1、C2 指向同一 URL（或同源下的不同 URL 也可以）。当页面 A 需要向 页面 B 通信时，先将消息发给其内嵌的 iframe C1，由于 C1、C2 同源，它们之间可以使用上述讲到的任意方法通信，即 C2 可以接收 C1 发送的消息，然后再由 C2 将发送信息给页面 B。

我们以 BroadCast Channel 为例。

首先，在中间页面创建一个 BroadCast Channel 实例，并为实例的 `message` 事件上定义函数，接收来自 BroadCast Channel 的消息，再定义一个全局的 `message` 事件定义函数，接收来自父页面的消息。

```javascript
const BC = new BroadcastChannel('zhao')
// 接收到 BroadCast Channel 广播消息，发送给父页面
BC.onmessage = function (e) {
    window.parent.postMessage(e.data, '*')
    console.log('[BroadCast Channel]接收：', `msg-${e.data.msg}，from-${e.data.from}`)
}

// 接收来自父页面的消息，调用 BroadCast Channel 广播消息
window.addEventListener('message', function (e) {
    BC.postMessage(e.data)
    console.log('[父页面]接收：', `msg-${e.data.msg}，from-${e.data.from}`)
})
```

然后，在页面 A、B 中定义全局的 `message` 事件函数，接收来自 iframe 的消息：

```javascript
window.addEventListener('message', (e) => {
    console.log('[iframe]接收：', `msg-${e.data.msg}，from-${e.data.from}`)
})
```

最后，在页面 A 或者 B 发出消息：

```javascript
window.frames[0].postMessage({
    msg: "Hello, This is Page A!",
    from: location.href
}, '*')
```

[iframe DEMO Page A](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=6)

[iframe DEMO Page B](https://1927344728.github.io/demo-lizh/html/23-commumication\(B\).html?type=6)

**注意：** 主页面调用 iframe 的 `postMessage` 方法，需要等 iframe 加载完。否则报错：

```shell
Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://xxx.xxx.x.xxx:xxxx') does not match the recipient window's origin
```

## 8.document.domain

满足某些限制条件的情况下（**两个源的一级级域和二级域名相同，只是次级域名不同**），页面是可以修改它的源。Js 可以将 [`document.domain`](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/domain) 的值设置为其当前域或其当前域的父域，并且这个**修改后的域将用于后续源检查**。

**注意：document.domain只能设置为当前域和其父域，否则，浏览器会拒绝修改。**

\*\*注意：\*\*端口是由浏览器另行检查的。任何对 `document.domain` 的赋值操作，包括 `document.domain = document.domain` 都会导致端口被重写为 `null` 。

**注意：** 修改 `document.domain` 的方式**只适用于解决 Cookie 和 iframe 的跨域**问题。

## 9.window\.name+iframe

**浏览器的 window\.name 属性有个特性：无论是否同源，只要在同一个浏览器标签或者同一个 iframe 框架打开过的页面，后一个页面可以读取前一个页面设置的 window\.name 值（页面刷新后，该值还是存在）**。

**注意：** window\.name 的值大小可达 2MB。

根据这个特性，我们可以在主页面内嵌 iframe，先将 iframe 指向一个非同源的页面，然后将 iframe 指向一个与主页面同源的中间页面，再主页面访问同源是中间页面的 window\.name 值，间接实现非同源页面的通信。

比如：`http://localhost:5200/23-commumication(A).html` 要获取非同源页面 `http://localhost:5300/23-commumication(B).html` 的数据：

首先，在 `23-commumication(B).html` 页面，将数据赋值给 `window.name`：

```html
<!-- http://localhost:5300/23-commumication(B).html?type=7 -->
<script>
    window.name = "Hello, This is Page B！"
</script>
```

其次，在 `23-commumication(A).html` 内嵌一个指向 `23-commumication(B).html` 的 `iframe` 标签，在 `23-commumication(B).html`文件加载完成后，会执行文件中的 `window.name` 赋值语句。

```html
<!-- http://localhost:5200/23-commumication(A).html?type=7 -->
<iframe src="http://localhost:5300/23-commumication(B).html?type=7"></iframe>
<script>
    function getCrossOriginData() {
        const frame = document.querySelector("#windowOpenIframe")
        frame.onload = function () {
            // console.log(frame.contentWindow.name)
        }
    }
    getCrossOriginData()
</script>
```

**注意：** 由于 `23-commumication(A).html` 和 `23-commumication(B).html` 是非同源页面，无法通过 `frame.contentWindow.name` 直接获取数据。如果执行上面注释的 `console.log(frame.contentWindow.name)`，会报错：

```shell
Uncaught DOMException: Blocked a frame with origin "http://localhost:5200" from accessing a cross-origin frame.
```

然后，创建一个中间页面 `http://localhost:5200/proxy.html`（**也可以是不存在的页面，会报 404 错误，但不影响功能**），然后将 `23-commumication(A).html` 中 `iframe` 标签的 `src` 指向这个中间页面。

由于 `proxy.html` 和 `23-commumication(B).html` 是在同一个 iframe 中打开的，它们共享 `window.name`。而 `proxy.html` 和 `23-commumication(A).html` 是同源的，`23-commumication(A).html` 可以通过 `frame.contentWindow.name` 获取 `proxy.html` 的 `window.name` 值，也就是 `23-commumication(B).html` 的 `window.name` 值，如此，间接实现了的 `23-commumication(A).html` 和 `23-commumication(B).html` 的非同源通信。

修改 `getCrossOriginData` 方法如下：

```javascript
function getCrossOriginData() {
    const frame = document.querySelector("#windowOpenIframe")
    let isFirstLoad = true
    frame.onload = function () {
        // iframe 加载完 23-commumication(B).html 后，再去加载 proxy.html，两者共享 window.name 值。
        if (isFirstLoad === true) {
            isFirstLoad = false
            frame.src = 'http://localhost:5200/proxy.html'
        } else {
            // 获取 proxy.html、23-commumication(B).html 共享的 window.name 值
            console.log(frame.contentWindow.name)
            document.querySelector('.box_07').querySelector('.content').innerHTML = frame.contentWindow.name
        }
    }
}
getCrossOriginData()
```

**总结：** 该方案实现的关键在于 `proxy.html`文件，它和 `23-commumication(B).html` 是在同一个 iframe 访问的，并且它和 `23-commumication(A).html` 是同源的。

同理，该方法也可以实现**服务端跨域数据请求**：服务端需要提供一个页面地址，并将需要返回的数据赋值给 `window.name` 属性。客户端调用如下方法获取数据：

```js
function getCrossOriginData(targetUrl, proxyUrl, callback) {
    const iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    iframe.src = targetUrl

    let isFirstLoad = true
    iframe.onload = function () {
        if (isFirstLoad) {
            isFirstLoad = false
            iframe.contentWindow.location = proxyUrl
        } else {
            callback(iframe)
            iframe.contentWindow.document.write('')
            iframe.contentWindow.close()
            document.body.removeChild(iframe)
        }
    }
    document.body.appendChild(iframe)
}
getCrossOriginData(
    'http://localhost:5300/23-commumication(B).html?type=7',
    'http://localhost:5200/proxy.html.html',
    (iframe) => {
        console.log(iframe.contentWindow.name)
        document.querySelector('.box_07').querySelector('.content').innerHTML = iframe.contentWindow.name
    }
)
```

[查看 window.name DEMO](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=7)

## 10.location.hash+iframe

hash 指的是 URL 的 `#` 号后面的部分。URL 的 hash 变化，浏览器不会刷新页面。

在父窗口中，通过 iframe 内嵌子窗口。父窗口将数据写入子窗口 URL 的 hash 中，子窗口可通过监听 `hashchange`事件获取：

```js
// 父窗口
function sendLocationHashToFrame () {
    const frame = document.querySelector('#locationHashIframe')
    frame.src = frame.src + `#Hello, This is Page A`
}
window.onhashchange = () => {
    console.log(window.location.hash)
}
```

同理，子窗口也可以向父窗口的 hash 写入数据，被父窗口的 `hashchange` 事件获取：

```js
// 子窗口
function sendLocationHashToParent (e) {
    window.parent.location.href = 'http://localhost:5200/23-commumication(A).html?type=8#Hello, This is Page B!'
}
window.onhashchange = () => {
    console.log(window.location.hash)
}
```

[查看 location.hash DEMO](https://1927344728.github.io/demo-lizh/html/23-commumication\(A\).html?type=8)

**注意：** 只有当 hash 值有变化才能触发 `message` 事件，即如果设置的 hash 值与当前链接相同，不会触发 `message` 事件。

**注意：** `window.parent.location.href` 是唯一的可以跨域访问的 Location 对象属性，且该属性**只能跨域赋值，不能跨域取值**。

## 常见问题

### HTTP请求跨域错误原因

* 服务端设置的 `Access-Control-Allow-Origin` 不匹配；
* 服务端禁用了 `OPTIONS` 方法；
* HTTP 请求头部扩展了字段，服务端的响应头部没有添加允许；
* HTTP 请求被服务端重定向到一个不允许跨域的地址；
* 浏览器禁止跨域请求数据。如：IE 浏览器可以在 `工具 -> Internet 选项 -> 安全 -> 自定义级别 -> 其他`，设置是否启用 【通过域访问数据源】。

### HTTP请求无法携带Cookie

* 客户端没有设置 `withCredentials = true` ；
* 服务端响应头没有 `Access-Control-Allow-Credentials: true`，或者 `Access-Control-Allow-Origin`设置为通用符 `\*`。跨域发送 Cookie，`Access-Control-Allow-Origin` 不能设为通用符，**必须指定明确的、与请求网页一致的域名。**
* Cookie 的 `sameSite` 属性没有设置为 `none`；
* 浏览器禁止了跨域发送 Cookie。
  * chrome：打开 `chrome://flags/` 将 `SameSite by default cookies`、`Cookies without SameSite must be secure`两项设置成 Disable；
  * IE：\`\`设置 -> 隐私和安全性 -> cookie -> 不阻止cookie\`。
* 使用了 Mock 等第三方包。

### Canvas绘制图片的跨域问题

Canvas调用 `getImageData()` 或 `toDataURL()` 对图片操作的时候，会出现跨域问题，而且跨域问题还不止一层。

**方法一：** 图片储服务器设置允许跨域，客户端将 `img` 标签的 `crossOrigin` 属性设置为 `anonymous` ：

```js
// 储服务器设置允许跨域，img 标签加 crossOrigin 属性。
var canvas = document.createElement('canvas')
var context = canvas.getContext('2d')

var img = new Image()
img.crossOrigin = 'anonymous'
img.onload = function () {
    context.drawImage(this, 0, 0)
    context.getImageData(0, 0, this.width, this.height)
}
img.src = 'https://example.com/img/img_2323232.jpg'
```

**方法二：** 改用 Ajax 加载图片数据。即由服务端向图片存储服务器获取图片数据，转换成 base64 返回客户端。如果是请求频繁的图片，可以考虑由服务端转储到本地允许跨域的服务器上，返回转存后的图片链接到客户端。

```js
// Ajax 加载图片数据
var xhr = new XMLHttpRequest()
xhr.onload = function () {
    var url = URL.createObjectURL(this.response)
    var img = new Image()
    img.onload = function () {
        URL.revokeObjectURL(url)
    }
    img.src = url
}
xhr.open('GET', url, true)
xhr.responseType = 'blob'
xhr.send()
```

### json及jsonp的区别

JSON 是一种数据交换格式，而 JSONP 是一种依靠开发人员的聪明才智创造出的一种**非官方跨域数据交互协议**。

## 参考链接

[MDN 浏览器的同源策略](https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy)

[MDN CORS](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS)

[阮一峰 同源政策](https://javascript.ruanyifeng.com/bom/same-origin.html)

[阮一峰 跨域资源共享 CORS 详解](https://www.ruanyifeng.com/blog/2016/04/cors.html)

[阮一峰 浏览器同源政策及其规避方法](https://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html)

[九种 “姿势” 让你彻底解决跨域问题](https://segmentfault.com/a/1190000016653873)
