Skip to content
On this page

一、同源策略

协议、域名、端口都相同称为同域,某个不相同就是跨域

为什么浏览器不支持跨域?

  • 浏览器中的 cookie, localStorage 都仅支持同域
  • DOM 元素也有同源策略 (iframe)
  • ajax 也不支持跨域

二、实现跨域

2.1 、浏览器与服务器之间实现跨域

2.1.1、jsonp

介绍

  • 浏览器动态创建 scriptsrc 指向服务器,同时传递一个查询参数 cb=yyy
  • 服务器接收请求后,根据查询参数 cb, 构造如下的响应
    • yyy.call(undefined, data)
  • 浏览器接受响应后,会执行 yyy.call(undefined, data)
  • 最后在函数 yyy 中就能拿到请求的数据

封装

js
function jsonp({url, params, cb}) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[cb] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = {...params, cb}
    let query = Object.keys(params).reduce((pre, key) => {
      pre.push(`${key}=${params[key]}`)
    }, []).join('&')
    script.src=`${url}?${query}`
    document.body.appendChild(script)
  })
}
// 使用
jsonp({
  url: `www.baidu.com`,
  params: {
    wd: '123'
  },
  cb: 'show'
}).then(data => {
  console.log(data)
})

缺点

  • 因为JSONP是通过动态创建 script 标签实现的,而 script 只能发送 GET 请求
  • 不安全,存在 xss 攻击

2.1.2、cors

cors 主要是在服务端进行设置响应头,有如下设置

  • Access-Control-Allow-Origin: 允许哪个源访问我,可设置 *
  • Access-Control-Allow-Headers: 允许携带哪个 Header 访问我,
  • Access-Control-Allow-Methods: 允许哪些方法访问我
  • Access-Control-Max-Age: 预检请求的有效时间
  • Access-Control-Allow-Credentials: 允许携带 Cookie
  • Access-Control-Expose-Headers: 允许前端获取哪个头

设置 Access-Control-Max-Age,何为请求有效时间?

在发送 POSTPUT 等请求时,通常需要发送请求体,而浏览器在真正发送请求之前,会发送一个预检请求,也就是 OPTIONS 请求,只有当预检请求通过后,浏览器才会发送真正的请求。

当设置 Access-Control-Max-Age = 6000 后,表示预检请求的结果在 6s 之内是有效的。

js
const express = require('express')
const app = express()
const whileList = [
  'http://localhost:3000'
]

app.use(function(req, res, next){
  const {origin} = req.headers
  if (whileList.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Access-Control-Allow-Headers', 'name')
    res.setHeader('Access-Control-Allow-Methods', 'post')
    res.setHeader('Access-Control-Max-Age', 6000)
  }
  next()
})


app.get('/getData', (req, res) => {
  console.log(req.headers)
  res.end('Hello')
})

app.listen(4000)

2.2 页面之间实现跨域(iframe 场景)

以下所以示例都是基于 a, b 页面同域,通过 3000 端口访问,而 c 页面不同域,通过 4000 端口访问。而目的是 a 页面能拿到 c 页面中的数据

2.2.1、postMessage (首推)

使用 postMessage 发送消息,使用 onMessage 监听接收消息

a.html

html
<!DOCTYPE html>
<body>
  <h1>a.html</h1>
  <iframe id="iframe" src="http://localhost:4000/c.html" onload="load()"></iframe>
  <script>
    function load() {
      let iframe = document.getElementById('iframe')
      iframe.contentWindow.postMessage('data form a.html', 'http://localhost:4000')
      window.onmessage = function(e) {
        console.log(`a.html接收到:`, e.data);
      }
    }
  </script>
</body>
</html>

c.html

html
<!DOCTYPE html>
<body>
  <h1>c.html</h1>
  <script>
    window.onmessage = function(e) {
      console.log(`c.html接收到:`, e.data);
      e.source.postMessage('data form c.html', e.origin)
    }
  </script>
</body>
</html>

2.2.2、window.name

实现原理:

  • c 将数据放到 window.name
  • a 在加载 c 页面后将 iframe 地址换成同源下的 b 页面,此时会重新执行 load 函数,而 window.name 保存的数据并不会丢失

a.html

html
<!DOCTYPE html>
<body>
  <h1>a.html</h1>
  <iframe id="iframe" src="http://localhost:4000/c.html" onload="load()"></iframe>
  <script>
    let first = true
    function load() {
      let iframe = document.getElementById('iframe')
      if (first) {
        iframe.src="http://localhost:4000/b.html"
        first = false
      } else {
        console.log(iframe.contentWindow.name)
      }
    }
  </script>
</body>
</html>

c.html

html
<!DOCTYPE html>
<body>
  <h1>c.html</h1>
  <script>
    window.name="hello world!"
  </script>
</body>
</html>

2.2.3、location.hash

实现原理:

  • a 将传递给 c 的数据放到 iframe src 的 hash 中
  • c 将要传递的数据通过 hash 传给 b 页面
  • 因为 a,b 同源,所以 a 能拿到 c 传给 b 的数据
html
<!DOCTYPE html>
<body>
  <h1>a.html</h1>
  <iframe id="iframe" src="http://localhost:4000/c.html#dataFromA"></iframe>
  <script>
    window.onhashchange = function(){
      console.log(location.hash)
    }
  </script>
</body>
</html>
html
<!DOCTYPE html>
<body>
  <h1>c.html</h1>
  <script>
    console.log(window.hash) // dataFromA
    let iframe = document.createElement('iframe')
    iframe.src = `http://localhost:3000/b.html#dataFromC`
    document.appendChild(iframe)
  </script>
</body>
</html>
html
<!DOCTYPE html>
<body>
  <h1>b.html</h1>
  <script>
    console.log(window.hash) // dataFromC
    window.parent.parent.location.hash = window.hash
  </script>
</body>
</html>

2.2.6、document.domain

主要用在一级域名和二级域名之间通信

2.3、无跨域现在

2.3.1、websocket

属于高级 api, 常使用 socket.io

  • 客户端

    html
    <!DOCTYPE html>
    <body>
      <h1>a.html</h1>
      <script>
        const socket = new WebSocket('ws://localhost:4000')
        socket.onopen = function() {
          socket.send('data from client')
        }
        socket.onmessage = function(e) {
          console.log(`client received:`, e.data)
        }
      </script>
    </body>
    </html>
    
  • 服务端

    js
    let express = require('express')
    let WebSocket = require('ws')
    const wss = new WebSocket.server({port: 4000})
    wss.on('connection', ws => {
      ws.on('message', function(data) {
        console.log('data')
        ws.send('data from server')
      })
    })
    

2.4、其它方式

2.4.1、http-proxy (webpack中使用)

2.4.2、nginx