跨域 是前端领域绕不开的一道题,今天就来好好聊一聊前端跨域。javascript
同源策略(same-origin policy) 最初是由 Netspace 公司在 1995 年引入浏览器的一种安全策略,如今全部的浏览器都遵照同源策略,它是浏览器安全的基石。css
同源策略规定跨域之间的脚本是相互隔离的,一个域的脚本不能访问和操做另一个域的绝大部分属性和方法。所谓的 同源 指的是 协议相同,域名相同,端口相同。html
同源策略最初只是用来防止不一样域的脚本访问 Cookie 的,可是随着互联网的发展,同源策略愈来愈严格,目前在不一样域的场景下,Cookie、本地存储(LocalStorage,SessionStorage,IndexDB),DOM 内容,AJAX(Asynchronous JavaScript and XML,非同步的 JavaScript 与 XML 技术) 都没法正常使用。前端
下表给出以 http://www.a.com/page/index.html 为例子进行同源检测的示例:java
示例 | 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 | 失败 | 不一样域名 |
解决方案按照解决方式能够分为四个大的方面:node
src
或者 herf
属性的标签src
或者 herf
属性的标签全部具备 src
属性的标签都是能够跨域,好比:<script>
、<img>
、<iframe>
,以及 <link>
标签,这些标签给咱们了提供调用第三方资源的能力。git
这些标签也有限制,如:只能用于 GET
方式获取资源,须要建立一个 DOM 对象等。github
不一样的标签发送请求的机制不一样,须要区别对待。如:<img>
标签在更改 src
属性时就会发起请求,而其余的标签须要添加到 DOM 树以后才会发起请求。web
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
属性以及 iframe
标签的跨域能力。 window.name 的值不是普通的全局变量,而是当前窗口的名字,iframe 标签也有包裹的窗体,天然也就有 window.name 属性。ajax
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 的跨域能力就能够实现不一样域之间的数据通讯,具体步骤以下:
注意:当数据源页面载入成功后(即 window.name 已经赋值),须要把 iframe 的 src 指向访问页面的同源页面(或者空白页 about:blank;
),不然在读取 iframe.contentWindow.name
属性时会由于同源策略而报错。
window.name 还有一种实现思路,就是 数据页在设置完 window.name 值以后,经过 js 跳转到与父页面同源的一个页面地址,这样的话,父页面就能经过操做同源子页面对象的方式获取 window.name 的值,以达到通讯的目的。
原理:经过使用 js 对父子框架页面设置相同的 document.domain
值来达到父子页面通讯的目的。 限制:只能在主域相同的场景下使用。
iframe 标签是一个强大的标签,容许在页面内部加载别的页面,若是没有同源策略那咱们的网站在 iframe 标签面前基本没有安全可言。
www.a.com
与 news.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()})`
}
复制代码
原理:利用修改 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 是 HTML5 XMLHttpRequest Level 2 中的 API,能够安全的实现跨域通讯,它可用于解决如下方面的问题:
postMessage 的具体使用方法能够参考 window.postMessage ,其中有 2 点须要注意:
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)
复制代码
原理:借助 CSS3 的 content
属性获取传送内容的跨域传输文本的方式。
相比较 JSONP 来讲更为安全,不须要执行跨站脚本。
缺点就是没有 JSONP 适配广,且只能在支持 CSS3 的浏览器正常工做。
具体内容能够经过查看 CSST 了解。
Flash 有本身的一套安全策略,服务器能够经过 crossdomain.xml 文件来声明能被哪些域的 SWF 文件访问,经过 Flash 来作跨域请求代理,而且把响应结果传递给 javascript,实现跨域通讯。
同源策略针对的是浏览器,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
方法以及 stream
的 pipe
方法。
上面是一个最简单的 NodeJs Server Proxy 实现,真实场景须要考虑更多复杂的状况,更详细的能够介绍能够点击 如何编写一个 HTTP 反向代理服务器 进行了解。
进一步了解:HTTP 代理原理及实现(一) HTTP 代理原理及实现(二)
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)
复制代码
原理: <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 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 即 服务器推送事件,支持 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 方式跨域。
页面间的通讯首先推荐 HTML5 新 API postMessage 方式通讯,安全方便。
其次浏览器支持不佳时,当主域相同时推荐使用 document.domain
方式,主域不一样推荐 location.hash
方式。
非双工通讯场景建议使用轻量级的 SSE 方式。
双工通讯场景推荐使用 WebSocket 方式。