浅谈跨域问题的出现及其解决方案

项目源码:https://github.com/trp1119/cross-domain

1 跨域问题的出现

1.1 什么是同源策略

同源策略(Same origin policy)是浏览器最核心也最基础的安全功能,同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol)、主机(host)和端口号(port)。[1]

1.2 什么是跨域请求(非同源策略请求)

跨域请求,即非同源策略请求,指当前发起请求的域与该请求指向的资源所在的域不一致。[1]

当前页面url 被请求页面url 是否跨域 原因
http://www.test.com http://www.test.com/index.html 同源(协议、域名、端口号相同)
http://www.test.com/ https://www.test.com/index.html 跨域 协议不同(http/https)
http://www.test.com/ http://www.baidu.com/ 跨域 主域名不同(test/baidu)
http://www.test.com/ http://blog.test.com/ 跨域 子域名不同(www/blog)
http://www.test.com:8080/ http://www.test.com:7001/ 跨域 端口号不同(8080/7001)

1.3 跨域请求发生场景

  • 在现代前端开发中,我们经常需要调用第三方的服务接口(例如 mock serverfake api),随着专业化分工的出现有很多专业的信息服务提供商为前端开发者提供各类接口,这种情况下就需要进行跨域请求。
  • 在前后端分离的项目中,前端后端分属于不同的服务跨域问题在采用这种架构的时候就存在,而且现在很多项目都采用这种前后分离的方式。

1.4 同源策略带来的跨域请求限制

  • 无法读取非同源网页的 CookieLocalStorageIndexedDB
  • 无法接触非同源网页的 DOM
  • 无法向非同源地址发送 AJAX 请求 [1]

举例

数据服务器(server_database)配置(5000 端口)

/**
 * 数据服务器
 */

let express = require('express'),
    app = express()

app.listen(5000, () => {
  console.log('数据服务器启动成功,运行在5000端口')
})

app.get('/queryInfo', (req, res) => {
  let data = {
    code: 0,
    msg: '非同源数据!'
  }
  res.send(data)
})

客户端(静态资源)服务器(server_static)配置(8000 端口)

/**
 * 客户端(静态资源)服务器
 */

let express = require('express'),
    app = express()

app.listen(8000, () => {
  console.log('客户端(静态资源)服务器启动成功,运行在8000端口')
})

app.get('/queryInfo', (req, res) => {
  res.send({
    code: 0,
    msg: '同源数据!'
  })
})

app.use(express.static('./static'))

客户端数据请求

<script>
  // 同源请求
  $.ajax({
    url: 'http://localhost:8000/queryInfo',
    method: 'get',
    dataType: 'json',
    success: (res) => {
      console.log(JSON.stringify(res))
    }
  })
  // 非同源请求(跨域请求)
  $.ajax({
    url: 'http://localhost:5000/queryInfo',
    method: 'get',
    dataType: 'json',
    success: (res) => {
      console.log(JSON.stringify(res))
    }
  })
</script>

客户端数据请求结果

1.4-1.png

同源请求下,得到服务器返回数据

{"code":0,"msg":"同源数据!"}

非同源请求(跨域请求)下,浏览器报错

Failed to load http://localhost:5000/queryInfo: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access.

tips:在未经允许的情况下,浏览器禁止 js 读取另一个域名的内容。但浏览器并不阻止向另一个域名发送请求。

跨域请求(非同源策略请求)限制为浏览器限制,非服务器限制。无论是否是跨域请求,服务器均会返回数据。

1.4-2.png
1.4-3.png

2 为什么会有跨域限制

  • 保护cookie、LocalStorage 和 IndexedDB

    cookie 中存着 sessionID(登录凭证)。当用户访问恶意网站时,如果没有同源策略,那么这个网站就可以通过 js 访问 document.cookie 得到用户关于各个网站的 sessionID,如果这个 sessionID 在有效期内,恶意网站就可以利用 sessionID登录各个网站,获取用户其他信息。 [2]

  • 保护DOM操作

    恶意网站通过 iframe 加载支付宝页面,当用户进入恶意网站后,误以为是支付宝官方页面,输入用户名、密码等信息。如果没有同源策略,恶意网站就可以通过 DOM操作 获取到用户输入值,从而控制用户账户。 [2]

  • 限制 ajax操作

    cookie 工作机制

    1. 客户向 A 网站的服务器发送登录请求,并携带账号密码数据

    2. A 网站的服务器校验账号密码正确后,返回响应并给本地添加了cookie

    3. 之后客户再次向 A 网站发起请求会自动带上A网站存储在本地的cookie

    4. A 网站的服务器从cookie中获取账号密码数据后,返回登陆成功界面

    2-1.png

用户登录过支付宝后,支付宝在本地设置了 cookie 信息。如果没有同源策略,当用户访问恶意网站时,恶意网站利用存储在本地的 cookie 等信息通过 ajax 向支付宝发起登录请求,从 ajax 回调中解析到用户数据信息。[3]

3 八种跨域解决方案

3.1 JSONP

3.1.1 JSONP 可用前提

浏览器安全性和方便性是成反比的,十位数的密码提高了安全性,但是不方便记忆。同样,同源策略提升了 Web 前端的安全性,但牺牲了Web拓展上的灵活性。

设想若把 htmljscssflashimage 等文件全部布置在一台服务器上,小型网站这样还可以,大中型网站如果这样做服务器无法承受。为解决服务器冗余,在实型前后端分离,静态资源服务器、图片资源服务器、视频资源服务器等拆分后,虽然系统变的更加灵活,但受制于浏览器同源策略限制,不同服务器之间通信受到限制。虽然保证了安全,Web 方便性大打折扣。

所以,现代浏览器在安全性和可用性之间选择了一个平衡点。在遵循同源策略的基础上,选择性地为同源策略“开放了后门”。 例如 scriptimglinkiframe 等标签,都允许垮域引用资源,严格说这都是不符合同源要求的。(当然,用户只能是引用这些资源而已,并不能读取这些资源的内容。例如在自己域内可以读取百度 logo 图片,但无法读取到该数据的二进制资源。)[4]

利用浏览器允许 script 标签跨域引用资源的特性,形成了一种非正式传输协议,JSONP

3.1.2 一般 JSONP 使用方法及其实现原理

JSONPJSON with Padding ),一种非官方跨域数据交互协议。在使用时,用户传递一个 callback 参数给服务端,然后服务端返回数据时会将这个 callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来自动处理返回数据。[5]

3.1.2.1 JSONP 的使用

JSONP 客户端调用方法

<script>
  // JSONP回调函数
  function fn (res) {
    console.log(res) // res 为从服务器获取的数据
  }
</script>
<!-- 一般 JSONP 使用方法 -->
<script src="http://localhost:5000/queryInfo?callback=fn"></script>

JSONP 服务器配置

/**
 * 数据资源服务器
 */

let express = require('express'),
    app = express()

app.listen(5000, () => {
  console.log('数据服务器启动成功,运行在5000端口')
})

app.get('/queryInfo', (req, res) => {
  let data = {
    code: 0,
    msg: '非同源数据!'
  }
  // JSONP 跨域数据返回
  let fn = req.query.callback // 获取客户端传递的函数名,注意,此处 callback 要与前端协商设置
  res.send(`${fn}(${data})`) // 返回指定格式的内容,函数名(数据) 这种格式
})

返回数据

3.1.2.1-1.png
3.1.2.2 JSONP 实现跨域请求原理
3.1.2.2-1.png

注意:客户端定义的必须是全局函数,因为浏览器中收到服务器返回的函数执行只有在全局函数下才能运行。

3.1.2.2-2.png
3.1.3 ajax 下使用 JSONP及其原理

JSONP 是非官方跨域数据交互协议,但 ajax 对其进行了封装,可采用 ajax 发起 jsonp 跨域请求。(axiosjsonp 请求方式)

3.1.3.1 使用 ajax 进行 JSONP 跨域请求

JSONP 客户端调用方法

<script>
  // 采用 ajax 发起 JSONP 跨域请求
  $.ajax({
    url: 'http://localhost:5000/queryInfo',
    method: 'GET',
    dataType: 'jsonp', // 当 data-type 设置为 jsonp 的时候,实现的是 jsonp 跨域请求
    jsonp: 'callback', // 自定义传递时的 callback 名称,默认为 'callback'
    // jsonpCallback: 'fn', // 自定义函数名
    success: (res) => {
      console.log(JSON.stringify(res)) // 从服务器获取的结果
    }
  })
</script>

接口调用

3.1.3.1-1.png
3.1.3.1-2.png

返回数据

3.1.3.1-3.png
3.1.3.2 ajax 实现 JSONP 跨域请求原理

ajax 对 JSONP 的封装依然才用的是 JSONP 实现原理,即通过动态创建 script 标签,然后拼装数据后发起请求。具体实现可参考 3.1.4 封装一个简单的 JSONP 实现返回 Promise

3.1.4 封装一个简单的 JSONP 实现返回 Promise

JSONP 封装

;(function anonymous(window) {
  /**
   * JSONP 方法
   * url 请求的接口地址
   * options 配置项
   *    jsonp: 'callback'(默认值)
   *    jsonpCallback: 随机生成的全局函数/自定义全局函数名
   *    timeout: 3000(默认值)
   */
  let jsonp = function (url, options = {}) {
    // 返回 Promise
    return new Promise((resolve, reject) => {
      // 验证参数合法性
      if (typeof url === 'undefined') {
        reject('url必须传递!')
        return
      }
      // 发送 jsonp 请求
      let SCRIPT = document.createElement('script'),
          CALL_BACK = options.jsonp || 'callback',
          FN_NAME = options.jsonpCallback || `JSONP${new Date().getTime()}`,

      SCRIPT.src = `${url}${url.indexOf('?') >= 0 ? '&' : '?'}${CALL_BACK}=${FN_NAME}&_${new Date().getTime()}`
      document.body.appendChild(SCRIPT)

      // 成功或失败后执行的函数
      window[FN_NAME] = function (result) {
        document.body.removeChild(SCRIPT)
        window[FN_NAME] = null
        resolve(result)
      }
    })
  }
  if (typeof module !== 'undefined' && module.exports !== 'undefined') {
    module.exports = {
      jsonp
    }
  }
  window.jsonp = jsonp

})(typeof window === 'undefined' ? global : window)
// 判断正在不同的环境下去,让 window 代表不同的全局对象,浏览器环境下就是 window,node 环境下执行代码就是 global

使用自行封装的 JSONP 发起跨域请求

<script src="jsonp.js"></script>
<script>
  // 自己封装一个简单地 jsonp 跨域请求,返回 Promise
  jsonp('http://localhost:5000/queryInfo').then(res => {
    console.log(JSON.stringify(res))
  })
</script>

接口调用

3.1.4-1.png
3.1.4-2.png

返回数据

3.1.4-3.png
3.1.5 JSONP 不足
  • JSONP 由于采用 script 资源文件请求,而资源请求为 GET 请求,故仅在 GET 请求中使用 JSONP 跨域请求。
  • 使用 ajax 请求网站,而服务器返回的 JSONP callback 是恶意执行代码,导致返回浏览器后会自动执行恶意代码,威胁数据安全。(XSS 攻击)

3.2 CORS 跨域资源共享

CORS (Cross-Origin Resource Sharing),跨域资源共享

CORS 需要浏览器和服务器同时支持,才可以实现跨域请求,目前几乎所有浏览器都支持 CORSIE 则不能低于 IE10CORS 的整个过程都由浏览器自动完成,前端无需做任何设置,跟平时发送 ajax 请求并无差异。所以,实现 CORS 的关键在于服务器,只要服务器实现 CORS 接口,就可以实现跨域通信。[6]

3.2.1 CORS 跨域配置方式

服务器未配置时限制跨域

3.2.1-1.png

由图中可以看出,浏览器不允许跨域原因已指出,No “Access-Control-Allow-Origin” header,故可在服务端进行相关头部信息配置以实现跨域请求。6

客户端发送请求

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  // 客户端为普通的 axios get 请求 
  axios({
    url: 'http://localhost:5000/queryInfo',
    method: 'get',
  }).then(res => {
    console.log(res)
  })
</script>

服务端配置

/**
 * 数据资源服务器
 */

let express = require('express'),
    app = express()

app.listen(5000, () => {
  console.log('数据服务器启动成功,运行在5000端口')
})

// 基于CORS设置允许跨域请求
app.use((req, res, next) => {
  // 允许哪些源可以向这个服务器发送AJAX请求(通配符是 '*',表示允许所有的源,也可以单独设置某个源,'http://localhost:8000',这样只允许 http://localhost:8000 的请求)
  // 不使用通配符是为了保证接口和数据的安全,不能让所有的源都能访问。而且一旦设置了允许携带凭证过来,则设置 '*' 通配符会被报错,此时只能设置具体的源!且只能设置一个允许访问的源。
  res.header('Access-Control-Allow-Origin', '*')
  // 是否允许跨域的时候携带凭证(例如 cookie 凭证,true 为允许,false 为不允许,设置为 false,客户端和服务器之间不会传递 cookie,这样 session 存储就失效了)(session 之所以有用是因为客户端从 cookie 中取 sid ,即将sid通过 cookie 传递给服务器进行校验)
  // 一般都设置为 true
  res.header('Access-Control-Allow-Credentials', true)
  // 允许的请求头部(哪些头部信息是合法的)
  res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-Width, Cookie')
  // 允许的请求方式(一定要有 OPTIONS)
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, HEAD, OPTIONS')
  // 设置 OPTIONS 请求目的:我们吧这个请求当做一个试探性请求,当客户端需要向服务器发送请求的时候,首先发送一个 OPTIONS 请求,服务器接受到是 OPTIONS 请求,看一下是否允许跨域,允许返回成功。如果服务器不允许跨域,则客户端会出现跨域请求不允许的错误。如果客户端检测到不允许跨域,则后续的请求都不再进行。 =》 客户端 axios 框架就是这样处理的,自己写的没有写 OPTIONS 请求。
  req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next()
  // next() 为 express 中间件语法
  next()
})

app.get('/queryInfo', (req, res) => {
  let data = {
    code: 0,
    msg: '非同源数据!'
  }
  // CORS 跨域数据返回
  res.send(data)
})

接口调用

3.2.1-2.png

返回数据

3.2.1-3.png
3.2.2 CORS 跨域配置介绍
3.2.2.1 Access-Control-Allow-Origin
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Origin', 'http://localhost:8000')

允许哪些源可以向这个服务器发送数据请求(通配符是 '*',表示允许所有的源,也可以单独设置某个源,如 'http://localhost:8000',这样只允许 http://localhost:8000 的请求)。

不使用通配符 '*' 是为了保证接口和数据的安全,即不能让所有的源都能访问。而且一旦设置了允许携带凭证过来,则设置 '*' 通配符会被报错,此时只能设置具体的源!且只能设置一个允许访问的源。 7 8

3.2.2.2 Access-Control-Allow-Credentials
res.header('Access-Control-Allow-Credentials', true)

是否允许跨域的时候携带凭证(例如 cookie 凭证,设置为 true 为允许携带 cookie 凭证,false 为不允许。设置为 false 客户端和服务器之间不会传递 cookie,这样 session 存储就失效了)(session 之所以有用是因为客户端从 cookie 中取 sid ,即将 sid 通过 cookie 传递给服务器进行校验)

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  // 客户端为普通的 axios get 请求 
  axios({
    url: 'http://localhost:5000/queryInfo',
    method: 'get',
    withCredentials: true, // 服务端和客户端 withCredentials 属性都要设置为 true,无则客户端与服务端无法基于请求头进行 cookie 传递
  }).then(res => {
    console.log(res)
  })
</script>

携带凭证需要客户端设置 withCredentials: true,此时,若 Access-Control-Allow-Origin 设置为通配符 '*',即 res.header('Access-Control-Allow-Origin', '*') ,浏览器会报错(因为任何域都携带凭证请求会影响安全)。只允许设置域形式。

3.2.2.2-1.png

同样,若 Access-Control-Allow-Origin 设置多个 域,即 res.header('Access-Control-Allow-Origin', 'http://localhost:8000, http://localhost:8001') ,浏览器会报错。

3.2.2.2-2.png

即在客户端携带凭证请求情况下只能设置允许一个域的请求。 7 8

未设置 withCredentialstrue 时的请求头(无 cookie

3.2.2.2-3.png

设置 withCredentialstrue 时的请求头(有 cookie

3.2.2.2-4.png
3.2.2.3 Access-Control-Allow-Headers
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-Width, Cookie')

设置允许的请求头部信息,即哪些头部信息是合法的。

3.2.2.4 Access-Control-Allow-Methods
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, HEAD, OPTIONS')

允许的请求方式(一定要有 OPTIONS)。

3.2.3 CORS 跨域不足

若携带凭证发起请求,CORS 只能指定一个允许源,不能使用通配符和指定多个源。

3.3 node 作为中间件代理

Nodejsrequest 模块是服务端发起请求的工具包,可在本服务器向其他域的服务器发起请求。使用前需安装 request 插件。 9

yarn add request

客户端发送请求

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  axios.get('/queryInfo').then((res) => {
    console.log(res)
  })
</script>

客户端(静态资源)服务器端配置

/**
 * 客户端(静态资源)服务器
 */

let express = require('express'),
    app = express()
    request = require('request')

app.listen(9000, () => {
  console.log('静态资源服务器启动成功,运行在9000端口')
})

app.get('/queryInfo', (req, res) => {
  // 在服务器向其他域发起请求
  request('http://localhost:5000/queryInfo', (err, response, body) => {
    res.send(body)
  })
})

app.use(express.static('./static'))

接口调用

3.3-1.png
3.3-2.png

返回数据

3.3-3.png

3.4 http proxy 代理

proxy 只是一层代理,用于把指定 path 代理去数据服务器提供的地址,他的背后是由 node server 提供服务的。

同源策略限制是浏览器进行限制的,服务器间相互数据请求并不受浏览器同源策略限制。

设置代理后,当客户端请求某跨域接口时,实际请求的是客户端所在服务器某个接口, 当客户端服务器收到客户端请求时,会根据代理设置,通过服务器间通信请求数据服务器的数据(跨域数据),请求到数据后,再将数据通过客户端服务器返回给客户端。

这样,客户端请求的是自身服务器接口,而不是跨域接口,不会受同源策略限制,实现跨域。 10

3.4.1 proxy 跨域设置
3.4.1.1 webpack 中 proxy 设置

webpack dev-server 使用了非常强大的 http-proxy-middleware 包用于解决跨域请求。

webpack.config.js 文件中进行配置。 11

简单配置

devServer: {
  proxy: {
     "/queryInfo": "http://localhost:5000"
  }
}

复杂配置

devServer: {
  proxy: {
    '/queryInfo': {
      target: 'http://localhost:5000',
      changeOrigin: true
    }
  }
}

客户端发送请求

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  axios.get('/queryInfo').then(res => {
    console.log(res)
  })
</script>

接口调用

3.4.1.1-1.png

返回数据

3.4.1.1-2.png
3.4.1.2 vue 中 proxy 设置

vue.config.js 中进行配置。 12

简单配置

简单配置,这样配置后全局接口都会被代理到 http://localhost:5000

module.exports = {
  // 简单配置
  devServer: {
    proxy: 'http://localhost:5000'
  }
}

复杂配置

如果要配置部分接口代理,或进行 https 支持等设置,可进行复杂配置。

module.exports = {
  // 复杂配置
  devServer: {
    proxy: {
      '/queryInfo': {
        target: 'http://localhost:5000',
        ws: true,
        changeOrigin: true
      },
    }
  }
}

客户端发送请求

<script>
import axios from 'axios'

export default {
  name: 'app',
  mounted () {
    // axios.get('http://localhost:5000/queryInfo').then((res) => {
    //   window.console.log(res)
    // })
    axios.get('/queryInfo').then((res) => {
      window.console.log(res)
    })
  }
}
</script>

接口调用

3.4.1.2-1.png

返回数据

3.4.1.2-2.png
3.4.1.3 react 中 proxy 设置

简单配置

可在 package.json 中进行简单配置,这样配置后全局接口都会被代理到 http://localhost:500013

"proxy": "http://localhost:5000"

复杂配置

如果要配置部分接口代理,或进行 https 支持等设置,可在 src 目录下新建 setupProxy.js 文件进行复杂配置。

使用此配置需先安装 http-proxy-middleware 插件。 13

yarn add http-proxy-middleware
// setupProxy.js 设置
const proxy = require('http-proxy-middleware')

module.exports = function(app) {
  app.use(
    '/queryInfo',
    proxy({
      target: 'http://localhost:5000',
      changeOrigin: true,
    })
  )
}

客户端发送请求

import axios from 'axios'

// axios.get('http://localhost:5000/queryInfo').then((res) => {
//   console.log(res)
// })

axios.get('/queryInfo').then((res) => {
  console.log(res)
})

接口调用

3.4.1.3-1.png

返回数据

3.4.1.3-2.png
3.4.2 proxy 缺点

仅在本地开发环境中使用,由于线上使用的是打包后的静态文件,故需要线上环境服务器配置以支持跨域。

3.5 nginx 反向代理

使用 nginx 启动客户端服务(http://localhost:7000)跨域请求服务端数据(http://localhost:5000/queryInfo),此时受浏览器同源策略限制,浏览器会报错。 14

客户端发送请求

<script>
  // 客户端为普通的 axios get 请求 
  axios.get('http://localhost:5000/queryInfo').then(res => {
    console.log(res)
  })
</script>
3.5-1.png

客户端服务器配置

可在 niginx 服务器文件夹 conf/nginx.conf 下进行代理配置。

server {
    listen       7000; // 端口号设置
    server_name  localhost;

    location / {
        root   html/test; // 静态资源文件夹
        index  index.html index.htm;
    }

    location /queryInfo { // 跨域代理设置
        proxy_pass http://localhost:5000;
    }
}

客户端发送请求

设置后,客户端 axios 请求需改为 axios.get('/queryInfo') 以形成访问同源接口样式。

<script>
  // 客户端为普通的 axios get 请求 
  axios.get('/queryInfo').then(res => {
    console.log(res)
  })
</script>

接口调用

3.5-2.png

返回数据

3.5-3.png

3.6 window.name

3.6.1 window.name 特性

页面在浏览器端展示的时候,总能在控制台拿到一个全局变量 window,该变量有一个 name 属性,其有以下特征: 15

  • 每个浏览器窗口都有独立的 window.name 与之对应。
  • 在一个浏览器窗口的生命周期中(被关闭前),窗口载入的所有页面同时共享一个 window.name,每个页面对 window.name 都有读写的权限。
  • window.name 一直存在与当前窗口,即使是有新的页面载入也不会改变 window.name 的。
  • window.name 可以存储不超过 2M 的数据,数据格式按需自定义。

举例

在 C页面(http://localhost:5000/window.name/C.html)请求同源服务器,获取到数据并赋值给window.name

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  axios.get('http://localhost:5000/queryInfo').then((res) => {
    window.name = JSON.stringify(res)
  })
</script>
3.6.1-1.png

此时,在同一浏览器窗口,将链接改为非同源的 A页面(http://localhost:8000/window.name/A.html),发现此时仍可以通过 window.name 拿到数据。

3.6.1-2.png

利用这一点,可以试图在 A页面 用 iframe 加载 C页面,然后取到其 window.name 中的值。

<body>
  <iframe src="http://localhost:5000/window.name/C.html" frameborder="0" id="iframeBox"></iframe>
  <script>
    iframeBox.onload = () => {
      console.log(iframeBox.contentWindow.name)
    }
  </script>
</body>

运行发现浏览器进行了跨域请求限制。

3.6.1-3.png
3.6.2 利用 window.name 进行跨域

因为 A页面 与 C页面 一直存在于同一浏览器窗口内,window.name 值一直存在,所以可以使用无任何内容的 中间页面 proxy.html(与 A页面 同源),在 C页面 加载后,将 iframesrc 值更改为中间页面 proxy.html ,此时,proxy.html 页面的 window.name 值与 C页面 是一致的。由于 A页面 与 中间页面 proxy.html 同源,故 A页面 此时可以取到 window.name 值。 15

<body>
  <iframe src="http://localhost:5000/window.name/C.html" frameborder="0" id="iframeBox"></iframe>
  <script>
    let count = 0
    iframeBox.onload = () => {
      if (count === 0) {
        // 由于替换 src 后 iframe 会重新执行 onload,而执行 onload 后又会替换 src,故添加计数器以防止死循环
        iframeBox.src = "http://localhost:8000/window.name/proxy.html"
        count++
        return
      }
      console.log(iframeBox.contentWindow.name)
    }
  </script>
</body>
3.6.2-1.png

3.7 document.domain

3.7.1 document.domain 跨域使用

document.domain 用来得到当前网页的域名两个文档,只有在 document.domain 都被设定为同一个值,表明他们打算协作;或者都没有设定 document.domain 属性并且 url 的域是一致的,这两种条件下,一个文档才可以去访问另一个文档。

如果不是因为这个特殊的策略,每一个站点都会成为他的子域的 XSS 攻击的对象(例如,http://a.test.com 可以被来自 http://b.test.com 站点的恶意文件攻击)。

利用两个文档 document.domain 相同即可协作的特性,在 A页面 使用 iframe 加载 B页面,并将 A页与B页面设置相同的 document.domain ,进行跨域获取数据。 16

A页面

// A页面链接 http://a.test.com:5000/document.domain/A.html
<body>
  <iframe src="http://b.test.com:5000/document.domain/B.html" frameborder="0" id="iframeBox"></iframe>
  <script>
    document.domain = 'test.com'
    iframeBox.onload = () => {
      console.log(iframeBox.contentWindow.data)
    }
  </script>
</body>

B页面

<body>
  <script>
    document.domain = 'test.com'
    window.data = {
      code: 0,
      msg: '非同源数据!'
    }
    // axios.get('http://b.test.com:5000/queryInfo').then((res) => {
    //   console.log(res)
    //   window.data = JSON.stringify(res)
    // })
  </script>
</body>

返回数据

3.7.1-1.png
3.7.2 document.domain 的缺点

在根域范围内,浏览器允许把 domain 属性的值设置为它的上一级域。例如,在 a.test.com 域内,可以把domain 设置为 test.com16

所以 document.domain 只能处理父域相同,子域不同的情况。

3.8 postMessage

受浏览器跨域限制,非同源的页面无法进行通信,window.postMessage() 方法提供了一种受控机制来规避此限制。window.postMessage() 方法可以安全地实现 Window 对象之间的跨域通信。例如,在一个页面和它生成的弹出窗口之间,或者是页面和嵌入其中的 iframe 之间。

一般来说,一个窗口可以获得对另一个窗口的引用(例如,通过 targetWindow=window.opener ),然后使用 targetWindow.postMessage() 在其上派发 MessageEvent。接收窗口随后可根据需要自行处理此事件。传递给 window.postMessage() 的参数通过事件对象暴露给接收窗口。

3.8.1 postMessage API 与 onmessage API
3.8.1.1 postMessage API

targetWindow.postMessage(message, targetOrigin, [transfer]) 有三个参数,transfer 可选。 17

  • mesaage 就是要发送到目标窗口的消息。
  • targetOrigin 就是指定目标窗口的来源,必须与消息发送目标相一致。如果接收方窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送。值可以是字符串 “*”url“*” 表示任何目标窗口都可接收。
  • transfer 是可选项,数组内的对象是实现 Transferable 接口的对象。它和 message 一样会被传递给目标页面,这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
3.8.1.1 onmessage API
window.onmessage = function(e){ }

参数 emessage 实例,里面包含了 dataoriginsource 等属性,data 是发送方发送的 message , origin 是发送方所属的域,source 是发送方的 window 对象的引用。

3.8.2 使用 postMessage 实现跨域通信

A页面

// A页面链接 http://localhost:8000/postMessage/A.html
<body>
  <iframe src="http://localhost:5000/postMessage/B.html" frameborder="0" id="iframeBox"></iframe>
  <script>
    let dataA = {
      code: 0,
      msg: 'A数据!'
    }
    iframeBox.onload = () => {
      iframeBox.contentWindow.postMessage(dataA, 'http://localhost:5000')
      window.onmessage = function (e) {
        console.log(e.data)
      }
    }
  </script>
</body>

B页面

// B页面链接 http://localhost:5000/postMessage/B.html
<body>
  <h3>B页面请求同源接口获取数据</h3>
  <script>
    let dataB = {
      code: 0,
      msg: 'B数据!'
    }
    window.onmessage = function (e) {
      console.log(e.data)
      e.source.postMessage(dataB, e.origin)
    }
  </script>
</body>

返回数据

3.8.2-1.png

4 总结

跨域方式 跨域分类 静态资源服务器配合 数据服务器配合
jsonp JSOP
cors CORS
node request 代理
http proxy 代理
nginx 代理
window.name iframe 是(页面)
document.domain iframe 是(页面)
postMessage iframe 是(页面)

5 主要参考资料

[1] 什么是跨域?跨域解决方法

[2] 浏览器为什么要设计同源策略?

[3] AJAX跨域访问被禁止的原因

[4] 对于浏览器的同源策略你是怎样理解的呢?

[5] JSONP

[6] axios

[7] cors实现请求跨域

[8] CORS on ExpressJS

[9 Request - Simplified HTTP client

[10] webpack配置proxy反向代理的原理是什么?

[11] webpack devServer.proxy

[12] Vue devServer.proxy

[13] React Proxying API Requests in Development

[14] nginx 之 proxy_pass详解

[15] JS跨域--window.name

[16] Document.domain

[17] 利用window.postMessage()实现跨域消息传递

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容