那些年前端跨过的域

栏目: Json · 发布时间: 5年前

内容简介:同源策略(same-origin policy) 最初是由 Netspace 公司在 1995 年引入浏览器的一种安全策略,现在所有的浏览器都遵守同源策略,它是浏览器安全的基石。同源策略规定跨域之间的脚本是相互隔离的,一个域的脚本不能访问和操作另外一个域的绝大部分属性和方法。所谓的同源策略最初只是用来防止不同域的脚本访问 Cookie 的,但是随着互联网的发展,同源策略越来越严格,目前在不同域的场景下,Cookie、本地存储(LocalStorage,SessionStorage,IndexDB),

同源策略(same-origin policy) 最初是由 Netspace 公司在 1995 年引入浏览器的一种安全策略,现在所有的浏览器都遵守同源策略,它是浏览器安全的基石。

同源策略规定跨域之间的脚本是相互隔离的,一个域的脚本不能访问和操作另外一个域的绝大部分属性和方法。所谓的 同源 指的是 协议相同域名相同端口相同

同源策略最初只是用来防止不同域的脚本访问 Cookie 的,但是随着互联网的发展,同源策略越来越严格,目前在不同域的场景下,Cookie、本地存储(LocalStorage,SessionStorage,IndexDB),DOM 内容,AJAX(Asynchronous JavaScript and XML,非同步的 JavaScript 与 XML 技术) 都无法正常使用。

下表给出以 http://www.a.com/page/index.html 为例子进行同源检测的示例:

示例 URL 结果 原因
A http://www.a.com/page/login.html 成功 同源
B http://www.a.com/page2/index.html 成功 同源
C https://www.a.com/page/secure.html 失败 不同协议
D http://www.a.com:8080/page/index.html 失败 不同端口
E http://static.a.com/page/index.html 失败 不同域名
F http://www.b.com/page/index.html 失败 不同域名

解决方案

解决方案按照解决方式可以分为四个大的方面:

  • 纯前端方式
  • 纯后端方式
  • 前后端配合的方式
  • 其他方式

纯前端方式

  • src 或者 herf 属性的标签
  • window.name
  • document.domain
  • location.hash
  • postMessage
  • CSST (CSS Text Transformation)
  • Flash

src 或者 herf 属性的标签

所有具有 src 属性的标签都是可以跨域,比如: <script><img><iframe> ,以及 <link> 标签,这些标签给我们了提供调用第三方资源的能力。

这些标签也有限制,如:只能用于 GET 方式获取资源,需要创建一个 DOM 对象等。

不同的标签发送请求的机制不同,需要区别对待。如: <img> 标签在更改 src 属性时就会发起请求,而其他的标签需要添加到 DOM 树之后才会发起请求。

const img = new Image()
img.src = 'http://domain.com/picture' // 发起请求

const iframe = document.createElement('iframe')
iframe.src = 'http://localhost:8082/window_name_data.html'
document.body.appendChild(iframe) // 发起请求

window.name

原理:利用神奇的 window.name 属性以及 iframe 标签的跨域能力。 window.name 的值不是普通的全局变量,而是当前窗口的名字,iframe 标签也有包裹的窗体,自然也就有 window.name 属性。

window.name 属性神奇的地方在于 name 值在不同的页面(甚至不同域)加载后依旧存在,且在没有修改的情况下不会变化。

// 打开一个空白页,打开控制台
window.name = JSON.stringify({ name: 'window', version: '1.0.0' })
window.location = 'http://baidu.com'
//页面跳转且加载成功后, window.name 的值还是我们最初赋值的值
console.log(window.name) // {"name":"window","version":"1.0.0"}

window.name 属性结合 iframe 的跨域能力就可以实现不同域之间的数据通信,具体步骤如下:

  1. 在访问页面(http://a.com/page.html)动态创建 iframe 标签,src 属性指向数据页面(http://b.com/data.html)
  2. 为 iframe 绑定 load 事件,当数据页面载入成功后,把 iframe 的 src 属性指向同源代理页面(也可以是空白页)
  3. 当 iframe 再次 load,即可以操作 iframe 对象的 contentWindow.name 属性,获取数据源页面设置的 window.name 值

注意:当数据源页面载入成功后(即 window.name 已经赋值),需要把 iframe 的 src 指向访问页面的同源页面(或者空白页 about:blank; ),否则在读取 iframe.contentWindow.name 属性时会因为同源策略而报错。

window.name 还有一种实现思路,就是 数据页在设置完 window.name 值之后,通过 js 跳转到与父页面同源的一个页面地址 ,这样的话,父页面就能通过操作同源子页面对象的方式获取 window.name 的值,以达到通信的目的。

document.domain

原理:通过使用 js 对父子框架页面设置相同的 document.domain 值来达到父子页面通信的目的。 限制:只能在主域相同的场景下使用。

iframe 标签是一个强大的标签,允许在页面内部加载别的页面,如果没有同源策略那我们的网站在 iframe 标签面前基本没有安全可言。

www.a.comnews.a.com 被认为是不同的域,那么它们下面的页面能够通过 iframe 标签嵌套显示,但是无法互相通信(不能读取和调用页面内的数据与方法),这时候我们可以使用 js 设置 2 个页面的 document.domain 的值为 a.com (即它们共同的主域),浏览器就会认为它们处于同一个域下,可以互相调用对方的方法来通信。

// http://www.a.com/www.html
document.domain = 'a.com'

// 设置一个测试方法给 iframe 调用
window.openMessage = function () {
  alert('www page message !')
}

const iframe = document.createElement('iframe')

iframe.src = 'http://news.a.com:8083/document_domain_news.html'
iframe.style.display = 'none'
iframe.addEventListener('load', function () {
  // 如果未设置相同的主域,那么可以获取到 iframeWin 对象,但是无法获取 iframeWin 对象的属性与方法
  const iframeWin = iframe.contentWindow
  const iframeDoc = iframeWin.document
  const iframeWinName = iframeWin.name

  console.log('iframeWin', iframeWin)
  console.log('iframeDoc', iframeDoc)
  console.log('iframeWinName', iframeWinName)

  // 尝试调用 getTestContext 方法
  const iframeTestContext = iframeWin.getTestContext()

  document.querySelector('#text').innerText = iframeTestContext
})
document.body.appendChild(iframe)


// http://news.a.com/news.html
document.domain = 'a.com'

// 设置 windon.name
window.name = JSON.stringify({ name: 'document.domain', version: '1.0.0' })

// 设置一些全局方法
window.getTestContext = function () {
  // 尝试调用父页面的方法
  if (window.parent) {
    window.parent.openMessage()
  }

  return `${document.querySelector('#test').innerText} (${new Date()})`
}

location.hash

原理:利用修改 URL 中的锚点值来实现页面通信。URL 中有 #abc 这样的锚点信息,此部分信息的改变不会产生新的请求(但是会产生浏览器历史记录),通过修改子页的 hash 值传递数据,通过监听自身 URL hash 值的变化来接收消息。

该方案要做到父子页面的双向通信,需要用到 3 个页面:主调用页,数据页,代理页。这是因为主调用页可以修改数据页的 hash 值,但是数据页不能通过 parent.location.hash 的方式修改父页面的 hash 值(仅 IE 与 Chrome 浏览器不允许),所以只能在数据页中再加载一个代理页( 代理页与主调用页同域 ),通过同域的代理页去操作主调用页的方法与属性。

// http://www.a.com/a.html
const iframe = document.createElement('iframe')

iframe.src = 'http://www.b.com/b.html'
iframe.style.display = 'none'
document.body.appendChild(iframe)

setTimeout(function () {
  // 向数据页传递信息
  iframe.src = `${iframe.src}#user=admin`
}, 1000)

window.addEventListener('hashchange', function () {
  // 接收来自代理页的消息(也可以让代理页直接操作主调用页的方法)
  console.log(`page: data from proxy.html ---> ${location.hash}`)
})

// http://www.a.com/b.html
const iframe = document.createElement('iframe')

iframe.src = 'http://www.a.com/proxy.html'
iframe.style.display = 'none'
document.body.appendChild(iframe)

window.addEventListener('hashchange', function () {
  // 收到主调用页传来的信息
  console.log(`data: data from page.html ---> ${location.hash}`)

  // 一些其他的操作
  const data = location.hash.replace(/#/ig, '').split('=')

  if (data[1]) {
    data[1] = String(data[1]).toLocaleUpperCase()
  }

  setTimeout(function () {
    // 修改子页 proxy.html iframe 的 hash 传递消息
    iframe.src = `${iframe.src}#${data.join('=')}`
  }, 1000)
})

// http://www.a.com/proxy.html
window.addEventListener('hashchange', function () {
  console.log(`proxy: data from data.html ---> ${location.hash}`)

  if (window.parent.parent) {
    // 把数据代理给同域的主调用页(也可以直接调用主调用页的方法传递消息)
    window.parent.parent.location.hash = location.hash
  }
})

postMessage

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,可以安全的实现跨域通信,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的 iframe 消息传递
  • 上面三个场景的跨域数据传递

postMessage 的具体使用方法可以参考window.postMessage ,其中有 2 点需要注意:

  • postMessage 方法依附于具体的 window 对象,比如 iframe 的 contentWindow,执行 window.open 语句返回的窗口对象等。
  • targetOrigin 参数可以指定哪些窗口接收消息,包含 协议 + 主机 + 端口号,也可以设置为通配符 '*'。
// http://www.a.com/a.html
const iframe = document.createElement('iframe')

iframe.src = 'http://www.b.com/b.html'
iframe.style.display = 'none'
iframe.addEventListener('load', function () {
  const data = { user: 'admin' }

  // 向 b.com 传送跨域数据
  // iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.b.com')
  iframe.contentWindow.postMessage(JSON.stringify(data), '*')
})
document.body.appendChild(iframe)

// 接受 b.com 返回的数据
window.addEventListener('message', function (e) {
  console.log(`a: data from b.com ---> ${e.data}`)
}, false)


// http://www.b.com/b.html
window.addEventListener('message', function (e) {
  console.log(`b: data from a.com ---> ${e.data}`)

  const data = JSON.parse(e.data)

  if (data) {
    data.user = String(data.user).toLocaleUpperCase()

    setTimeout(function () {
      // 处理后再发回 a.com
      // window.parent.postMessage(JSON.stringify(data), 'http://www.a.com')
      window.parent.postMessage(JSON.stringify(data), '*')
    }, 1000)
  }
}, false)

CSST (CSS Text Transformation)

原理:借助 CSS3 的 content 属性获取传送内容的跨域传输文本的方式。

相比较 JSONP 来说更为安全,不需要执行跨站脚本。

缺点就是没有 JSONP 适配广,且只能在支持 CSS3 的浏览器正常工作。

具体内容可以通过查看 CSST 了解。

Flash

Flash 有自己的一套安全策略,服务器可以通过 crossdomain.xml 文件来声明能被哪些域的 SWF 文件访问,通过 Flash 来做跨域请求代理,并且把响应结果传递给 javascript,实现跨域通信。

纯后端方式

  • Server Proxy
  • CORS(Cross-origin resource sharing)

Server Proxy

同源策略针对的是浏览器,http/https 协议不受此影响,所以通过 Server Proxy 的方式就能解决跨域问题。

实现步骤也比较简单,主要是服务端接收到客户端请求后,通过判断 URL 实现特定跨域请求就代理转发(http,https),并且把代理结果返回给客户端,从而实现跨域的目的。

// NodeJs
const http = require('http')

const server = http.createServer(async (req, res) => {
  if (req.url === '/api/proxy_server') {
    const data = 'user=admin&group=admin'
    const options = {
      protocol: 'http:',
      hostname: 'www.b.com',
      port: 8081,
      path: '/api/proxy_data',
      method: req.method,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(data),
      },
    }
  
    const reqProxy = http.request(options, (resProxy) => {
      res.writeHead(resProxy.statusCode, { 'Content-Type': 'application/json' })
      resProxy.pipe(res) // 将 resProxy 收到的数据转发到 res
    })

    reqProxy.write(data)
    reqProxy.end()
  }
})

NodeJs 中 Server Proxy 主要使用 http 模块的 request 方法以及 streampipe 方法。

上面是一个最简单的 NodeJs Server Proxy 实现,真实场景需要考虑更多复杂的情况,更详细的可以介绍可以点击 如何编写一个 HTTP 反向代理服务器 进行了解。

进一步了解: HTTP 代理原理及实现(一) HTTP 代理原理及实现(二)

CORS(Cross-origin resource sharing)

CORS 的全称是“跨域资源共享”(Cross-origin resource sharing),是 W3C 标准。通过 CORS 协议实现跨域通信关键部分在于 服务器 以及 浏览器支持情况 (IE不低于IE10),整个 CORS 通信过程都是浏览器自动完成,对开发者来说 CORS 通信与同源的 AJAX 请求没有差别。

浏览器将 CORS 请求分为两类:简单请求(simple request)和 非简单请求(not-so-simple request)。更加详细的信息可以通过阅读阮一峰老师 的 跨域资源共享 CORS 详解 文章进行深入了解。

// server.js
// http://www.b.com/api/cors
const server = http.createServer(async (req, res) => {
  if (typeof req.headers.origin !== 'undefined') {
    // 如果是 CORS 请求,浏览器会在头信息中增加 origin 字段,说明请求来自于哪个源(协议 + 域名 + 端口)

    if (req.url === '/api/cors') {
      res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
      res.setHeader('Access-Control-Allow-Credentials', true)
      res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS')
      res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, X-Access-Token')

      const resData = {
        error_code: 0,
        message: '',
        data: null,
      }

      if (req.method === 'OPTIONS') {
        // not-so-simple request 的 预请求
        res.setHeader('status', 200)
        res.setHeader('Content-Type', 'text/plain')
        res.end()
        return
      } else if (req.method === 'GET') {
        // simple request
        Object.assign(resData, { data: { user: 'admin' } })
      } else if (req.method === 'PUT') {
        // not-so-simple
        res.setHeader('Set-Cookie', ['foo=bar; HttpOnly', 'bar=baz; HttpOnly', 'y=88']) // 设置服务器域名 cookie
        Object.assign(resData, { data: { user: 'ADMIN', token: req.headers['x-access-token'] } })
      } else {
        Object.assign(resData, { data: { user: 'woqu' } })
      }

      res.setHeader('status', 200)
      res.setHeader('Content-Type', 'application/json')
      res.write(JSON.stringify(resData))
      res.end()
      return
    }

    res.setHeader('status', 404)
    res.setHeader('Content-Type', 'text/plain')
    res.write(`This request URL '${req.url}' was not found on this server.`)
    res.end()
    return
  }
})


// http://www.a.com/cors.html
setTimeout(function () {
  console.log('CORS: simple request')
  ajax({
    url: 'http://www.b.com:8082/api/cors',
    method: 'GET',
    success: function (data) {
      data = JSON.parse(data)
      console.log('http://www.b.com:8082/api/cors: GET data', data)

      document.querySelector('#test1').innerText = JSON.stringify(data)
    },
  })
}, 2000)

setTimeout(function () {
  // 设置 cookie
  document.cookie = 'test cookie value'

  console.log('CORS: not-so-simple request')
  ajax({
    url: 'http://www.b.com:8082/api/cors',
    method: 'PUT',
    body: { user: 'admin' },
    header: { 'X-Access-Token': 'abcdefg' },
    success: function (data) {
      data = JSON.parse(data)
      console.log('http://www.b.com:8082/api/cors: PUT data', data)

      document.querySelector('#test2').innerText = JSON.stringify(data)
    },
  })
}, 4000)

前后端配合的方式

  • JSONP(JSON with Padding)

JSONP(JSON with Padding)

原理: <script> 标签可以跨域加载并执行脚本。

JSONP 是一种简单高效的跨域方式,并且易于实现,但是因为有跨站脚本的执行,比较容易遭受 CSRF(Cross Site Request Forgery,跨站请求伪造) 攻击,造成用户敏感信息泄露,而且 因为 <script> 标签跨域方式的限制,只能通过 GET 方式获取数据。

// server.js
// http://www.b.com/api/jsonp?callback=callback
const server = http.createServer((req, res) => {
  const params = url.parse(req.url, true)
 
  if (params.pathname === '/api/jsonp') {
    if (params.query && params.query.callback) {
      res.writeHead(200, { 'Content-Type': 'text/plain' })
      res.write(`${params.query.callback}(${JSON.stringify({ error_code: 0, data: 'jsonp data', message: '' })})`)
      res.end()
    }
  }

  // ...
})


// http://www.a.com/jsonp.html
const script = document.createElement('script')
const callback = function (data) {
  console.log('jsonp data', typeof data, data)
}

window.callback = callback // 把回调函数挂载到全局对象 window 下
script.src = 'http://www.b.com:8081/api/jsonp?callback=callback'
setTimeout(function () {
  document.body.appendChild(script)
}, 1000)

其他方式

  • WebSocket
  • SSE(Server-sent events)

WebSocket

WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。

// 服务端实现可以使用 socket.io,详见 https://github.com/socketio/socket.io


// client
const socket = new WebSocket('ws://www.b.com:8082')

socket.addEventListener('open', function (e) {
  socket.send('Hello Server!')
})
socket.addEventListener('message', function (e) {
  console.log('Message from server', e.data)
})

SSE(Server-sent events)

SSE 即 服务器推送事件,支持 CORS,可以基于 CORS 做跨域通信。

// server.js
const server = http.createServer((req, res) => {
  const params = url.parse(req.url, true)
 
  if (params.pathname === '/api/sse') {
    // SSE 是基于 CORS 标准实现跨域的,所以需要设置对应的响应头信息
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
    res.setHeader('Access-Control-Allow-Credentials', true)
    res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, X-Access-Token')

    res.setHeader('status', 200)
    res.setHeader('Content-Type', 'text/event-stream')
    res.setHeader('Cache-Control', 'no-cache')
    res.setHeader('Connection', 'keep-alive')

    res.write('retry: 10000\n')
    res.write('event: connecttime\n')
    res.write(`data: starting... \n\n`)

    const interval = setInterval(function () {
      res.write(`data: (${new Date()}) \n\n`)
    }, 1000)

    req.connection.addListener('close', function () {
      clearInterval(interval)
    }, false)

    return
  }
})


// http://www.a.com:8081/sse.html
const evtSource = new EventSource('http://www.b.com:8082/api/sse')

evtSource.addEventListener('connecttime', function (e) {
  console.log('connecttime data', e.data)

  document.querySelector('#log').innerText = e.data
})
evtSource.onmessage = function(e) {
  const p = document.createElement('p')

  p.innerText = e.data
  console.log('Message from server', e.data)

  document.querySelector('#log').append(p)
}

setTimeout(function () {
  evtSource.close()
}, 5000)

最佳实践

No silver bullets:没有一种方案能够适用所有的跨域场景,针对特定的场景使用合适的方式,才是最佳实践。

  • 资源跨域
  • 页面相互通信
  • 客户端与服务端通信

资源跨域

对于静态资源,推荐借助 <link> <script> <img> <iframe> 标签原生的能力实现跨域资源请求。

对于第三方接口,推荐基于 CORS 标准实现跨域,浏览器不支持 CORS 时推荐使用 Server Proxy 方式跨域。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript王者归来

JavaScript王者归来

月影 / 清华大学出版社 / 2008-7 / 86.00元

你手中的这本《JavaScript王者归来》不仅是一本传播知识的书,更是一本求道的书。 本书分为五个部分循序渐进地与读者讨论了JavaScript的方方面面,从简单的语言基础到丰富的实际应用再到深入剖析语言本质的高级话题,字里行间包含着作者多年工作中对JavaScript实践乃至程序设计思想的深入思考和总结。 本书揭开了JavaScript的面纱,绕过误解和虚幻的表象,引领你探索程序王......一起来看看 《JavaScript王者归来》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具