共计 10306 个字符,预计需要花费 26 分钟才能阅读完成。
前言
前端跨域的各种文章其实曾经很多了,但大部分还是不太合乎我胃口的介绍跨域。看来看去,如果要让本人了解印象粗浅,果然还是得本人敲一敲,并总结演绎整顿一篇博客进去,以此记录。
跨域的限度
跨域是为了阻止用户读取到另一个域名下的内容,Ajax 能够获取响应,浏览器认为这不平安,所以拦挡了响应。
跨域解决方案
除非特地阐明,否则下方标记的 html 文件默认都运行在 http://127.0.0.1:5500 服务下
CORS
CORS 即是指跨域资源共享。它容许浏览器向非同源服务器,收回 Ajax 申请,从而克服了 Ajax 只能同源应用的限度。这种形式的跨域次要是在后端进行设置。
这种形式的要害是后端进行设置,即是后端开启 Access-Control-Allow-Origin 为 *
或对应的 origin
就能够实现跨域。
浏览器将 CORS 申请分成两类:简略申请和非简略申请。
只有同时满足以下两大条件,就属于简略申请。
- 申请办法是以下是三种办法之一:
- HEAD
- GET
- POST
- HTTP 的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
但凡不同时满足下面两个条件,就属于非简略申请。
简略申请
cors.html
let xhr = new XMLHttpRequest() xhr.open('GET', 'http://localhost:8002/request')
xhr.send(null)
server.js
const express = require('express')
const app = express()
app.use((req, res, next) => {res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500') // 设置容许哪个域拜访
next()})
app.get('/request', (req, res) => {res.end('server ok')
})
app.listen(8002)
非简略申请
下面的是简略申请,如果咱们用非简略申请的形式,比方申请办法是 PUT,也能够通过设置实现跨域。
非简略申请的 CORS 申请,会在正式通信之前,减少一次 HTTP 查问申请,称为 ” 预检 ” 申请。
浏览器先询问服务器,服务器收到 ” 预检 ” 申请当前,查看了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段当前,确认容许跨源申请,浏览器才会收回正式的 XMLHttpRequest 申请,否则就报错。
let xhr = new XMLHttpRequest()
xhr.open('PUT', 'http://localhost:8002/request')
xhr.send(null)
server.js
const express = require('express')
const app = express()
let whileList = ['http://127.0.0.1:5500'] // 设置白名单
app.use((req, res, next) => {
let origin = req.headers.origin
console.log(whitList.includes(origin))
if (whitList.includes(origin)) {res.setHeader('Access-Control-Allow-Origin', origin) // 设置容许哪个域拜访
res.setHeader('Access-Control-Allow-Methods', 'PUT') // 设置容许哪种申请办法拜访
}
next()})
app.put('/request', (req, res) => {res.end('server ok')
})
app.listen(8002)
整个过程发送了两次申请,跨域胜利。
当然,还能够设置其余参数:
- Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器 CORS 申请会额定发送的头信息字段
- Access-Control-Allow-Credentials
示意是否容许发送 Cookie。默认状况下,Cookie 不包含在 CORS 申请之中。
- Access-Control-Expose-Headers
CORS 申请时,XMLHttpRequest 对象的 getResponseHeader()办法只能拿到 6 个根本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其余字段,就必须在 Access-Control-Expose-Headers 外面指定。
- Access-Control-Max-Age
用来指定本次预检申请的有效期,单位为秒。有效期是 20 天(1728000 秒),即容许缓存该条回应 1728000 秒(即 20 天),在此期间,不必收回另一条预检申请。
Node 中间件代理
实现原理:同源策略是浏览器须要遵循的规范,而如果是服务器向服务器申请就没有跨域一说。
代理服务器,须要做以下几个步骤:
- 承受客户端申请。
- 将申请转发给服务器。
- 拿到服务器响应数据。
- 将响应转发给客户端。
这次咱们应用 express 中间件 http-proxy-middleware 来代理跨域, 转发申请和响应
案例三个文件都在同一级目录下:
index.html
let xhr = new XMLHttpRequest()
xhr.open('GET', '/api/request')
xhr.onreadystatechange = () => {if (xhr.readyState === 4 && xhr.status === 200) {console.log('申请胜利,后果是:', xhr.responseText) // request success
}
}
xhr.send(null)
nodeMdServer.js
const express = require('express')
const {createProxyMiddleware} = require('http-proxy-middleware')
const app = express()
// 设置动态资源
app.use(express.static(__dirname))
// 应用代理
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:8002',
pathRewrite: {'^/api': '', // 重写门路},
changeOrigin: true,
})
)
app.listen(8001)
nodeServer.js
const express = require('express')
const app = express()
app.get('/request', (req, res) => {res.end('request success')
})
app.listen(8002)
运行http://localhost:8001/index.html
,跨域胜利
平时 vue/react 我的项目配置 webpack-dev-server 的时候也是通过 Node proxy 代理的形式来解决的。
Nginx 反向代理
实现原理相似于 Node 中间件代理,须要你搭建一个直达 nginx 服务器,用于转发申请。
这种形式只需批改 Nginx 的配置即可解决跨域问题,前端除了接口换成对应模式,而后前后端不须要批改作其余批改。
实现思路:通过 nginx 配置一个代理服务器(同域不同端口)做跳板机,反向代理要跨域的域名,这样能够批改 cookie 中 domain 信息,不便以后域 cookie 写入,实现跨域登录。
nginx 目录下的 nginx.conf 批改如下:
// proxy 服务器
server {
listen 80;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; # 反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; # 批改 cookie 里域名
index index.html index.htm;
# 当用 webpack-dev-server 等中间件代理接口拜访 nignx 时,此时无浏览器参加,故没有同源限度,上面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; # 以后端只跨域不带 cookie 时,可为 *
add_header Access-Control-Allow-Credentials true;
}
}
启动 Nginx
index.html
var xhr = new XMLHttpRequest()
// 前端开关:浏览器是否读写 cookie
xhr.withCredentials = true
// 拜访 nginx 中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true)
xhr.send()
server.js
var http = require('http')
var server = http.createServer()
var qs = require('querystring')
server.on('request', function (req, res) {var params = qs.parse(req.url.substring(2))
// 向前台写 cookie
res.writeHead(200, {'Set-Cookie': 'l=123456;Path=/;Domain=www.domain2.com;HttpOnly', // HttpOnly: 脚本无奈读取})
res.write(JSON.stringify(params))
res.end()})
server.listen(8080)
jsonp
原理:利用了 script 标签可跨域的个性,在客户端定义一个回调函数(全局函数),申请服务端返回该回调函数的调用,并将服务端的数据以该回调函数参数的模式传递过去,而后该函数就被执行了。该办法须要服务端配合实现。
实现步骤:
- 申明一个全局回调函数,参数为服务端返回的 data。
- 创立一个 script 标签,拼接整个申请 api 的地址(要传入回调函数名称如 ?callback=getInfo),赋值给 script 的 src 属性
- 服务端承受到申请后处理数据,而后将函数名和须要返回的数据拼接成字符串,拼装实现是执行函数的模式。(getInfo(‘server data’))
- 浏览器接管到服务端的返回后果,调用了申明的回调函数。
jsonp.html
function getInfo(data) {console.log(data) // 通知你一声,jsonp 跨域胜利
}
let script = document.createElement('script')
script.src = 'http://localhost:3000?callback=getInfo' //
document.body.appendChild(script)
server.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {let { callback} = req.query
res.end(`${callback}('通知你一声,jsonp 跨域胜利')`)
})
app.listen(3000)
jQuery 的 $.ajax() 办法当中集成了 JSONP 的实现,在此就不写进去了。
在开发中可能会遇到多个 JSONP 申请的回调函数名是雷同的,而且这种形式用起来也麻烦,故咱们本人封装一个 jsonp 函数
function jsonp({url, params, callback}) {return new Promise((resolve, reject) => {let script = document.createElement('script')
// 定义全局回调函数
window[callback] = function (data) {resolve(data)
document.body.removeChild(script) // 调用结束即删除
}
params = {callback, ...params} // {callback: "getInfo", name: "jacky"}
let paramsArr = []
for (const key in params) {paramsArr.push(`${key}=${params[key]}`)
}
script.src = `${url}?${paramsArr.join('&')}` // http://localhost:3000/?callback=getInfo&name=jacky
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000',
params: {name: 'jacky',},
callback: 'getInfo',
}).then(res => {console.log(res) // 通知你一声,jsonp 跨域胜利
})
服务端解构的时候能够取出参数
app.get('/', (req, res) => {let { callback, name} = req.query
res.end(`${callback}('通知你一声,jsonp 跨域胜利')`)
})
长处:兼容性好
毛病:因为 script 自身的限度,该跨域形式仅反对 get 申请,且不平安可能蒙受 XSS 攻打
postMessage
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多能够跨域操作的 window 属性之一,它可用于解决以下方面的问题:
- 页面和其关上的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的 iframe 消息传递
- 下面三个场景的跨域数据传递
总之,它能够容许来自不同源的脚本采纳异步形式进行无限的通信,能够实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- otherWindow:其余窗口的一个援用,比方 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。
- message: 将要发送到其余 window 的数据。
- targetOrigin: 通过窗口的 origin 属性来指定哪些窗口能接管到音讯事件,其值能够是字符串 ”*”(示意无限度)或者一个 URI。在发送音讯的时候,如果指标窗口的协定、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么音讯就不会被发送;只有三者齐全匹配,音讯才会被发送。
- transfer(可选):是一串和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给音讯的接管方,而发送一方将不再保有所有权。
这次咱们把两个 html 文件挂到两个 server 下,采取 fs 读取的形式引入,运行两个 js 文件
postMessage1.html
<body>
<iframe src="http://localhost:8002" frameborder="0" id="frame" onLoad="load()"></iframe>
<script>
function load() {let frame = document.getElementById('frame')
frame.contentWindow.postMessage('你好,我是 postMessage1', 'http://localhost:8002') // 发送数据
window.onmessage = function (e) {
// 承受返回数据
console.log(e.data) // 你好,我是 postMessage2
}
}
</script>
</body>
postMsgServer1.js
const express = require('express')
const fs = require('fs')
const app = express()
app.get('/', (req, res) => {const html = fs.readFileSync('./postMessage1.html', 'utf8')
res.end(html)
})
app.listen(8001, (req, res) => {console.log('server listening on 8001')
})
postMessage2.html
<body>
<script>
window.onmessage = function (e) {console.log(e.data) // 你好,我是 postMessage1
e.source.postMessage('你好,我是 postMessage2', e.origin)
}
</script>
</body>
postMsgServer2.js
const express = require('express')
const fs = require('fs')
const app = express()
app.get('/', (req, res) => {const html = fs.readFileSync('./postMessage2.html', 'utf8')
res.end(html)
})
app.listen(8002, (req, res) => {console.log('server listening on 8002')
})
websocket
WebSocket 是一种网络通信协定。它实现了浏览器与服务器全双工通信,同时容许跨域通信,长连贯形式不受跨域影响。因为原生 WebSocket API 应用起来不太不便,咱们个别都会应用第三方库如 ws。
Web 浏览器和服务器都必须实现 WebSockets 协定来建设和保护连贯。因为 WebSockets 连贯长期存在,与典型的 HTTP 连贯不同,对服务器有重要的影响。
socket.html(http://127.0.0.1:5500/socket.html
)
let socket = new WebSocket('ws://localhost:8001')
socket.onopen = function () {socket.send('向服务端发送数据')
}
socket.onmessage = function (e) {console.log(e.data) // 服务端传给你的数据
}
运行nodeServer.js
const express = require('express')
const WebSocket = require('ws')
const app = express()
let wsServer = new WebSocket.Server({port: 8001})
wsServer.on('connection', function (ws) {ws.on('message', function (data) {console.log(data) // 向服务端发送数据
ws.send('服务端传给你的数据')
})
})
document.domain + iframe
这种形式只能用于二级域名雷同的状况下。
比方 a.test.com 和 b.test.com 就属于二级域名,它们都是 test.com 的子域
只须要给页面增加 document.domain =’test.com’ 示意二级域名都雷同就能够实现跨域。
比方:页面 a.test.com:3000/test1.html 获取页面 b.test.com:3000/test2.html 中 a 的值
test1.html
<body>
<iframe
src="http://b.test.com:3000/test2.html"
frameborder="0"
onload="load()"
id="iframe"
></iframe>
<script>
document.domain = 'test.com'
function load() {console.log(iframe.contentWindow.a)
}
</script>
</body>
test2.html
document.domain = 'test.com'
var a = 10
window.name + iframe
浏览器具备这样一个个性:同一个标签页或者同一个 iframe 框架加载过的页面共享雷同的 window.name 属性值。在同个标签页里,name 值在不同的页面加载后也仍旧存在,这些页面上 window.name 属性值都是雷同的。利用这些个性,就能够将这个属性作为在不同页面之间传递数据的介质。
因为平安起因,浏览器始终会放弃 window.name 是 string 类型。
关上http://localhost:8001/a.html
- http://localhost:8001/a.html
<body>
<iframe
src="http://localhost:8002/c.html"
frameborder="0"
onload="load()"
id="iframe"
></iframe>
<script>
let first = true
function load() {if (first) {// 第 1 次 onload(跨域页)胜利后,切换到同域代理页面
let iframe = document.getElementById('iframe')
iframe.src = 'http://localhost:8001/b.html'
first = false
} else {// 第 2 次 onload(同域 b.html 页)胜利后,读取同域 window.name 中数据
console.log(iframe.contentWindow.name) // 我是 c.html 里的数据
}
}
</script>
</body>
- http://localhost:8001/b.html(不须要往 html 加内容,默认 html 构造模板即可)
- http://localhost:8002/c.html
<body>
<script>
window.name = '我是 c.html 里的数据'
</script>
</body>
c 页面给 window.name 设置了值, 即使 c 页面销毁,但 name 值不会被销毁;a 页面仍旧可能失去 window.name。
location.hash + iframe
实现原理:a.html 欲与 c.html 跨域互相通信,通过两头页 b.html 来实现。三个页面,不同域之间利用 iframe 的 location.hash 传值,雷同域之间间接 js 拜访来通信。
具体实现步骤:一开始 a.html 给 c.html 传一个 hash 值,而后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最初 b.html 将后果放到 a.html 的 hash 值中。
同样的,a.html 和 b.html 是同域的,都是 http://localhost:8001,也就是说 b 的 hash 值能够间接复制给 a 的 hash。c.html 为 http://localhost:8002 下的
a.html
<body>
<iframe src="http://localhost:8002/c.html#jackylin" style="display: none;"></iframe>
<script>
window.onhashchange = function () {
// 检测 hash 的变动
console.log(456, location.hash) // #monkey
}
</script>
</body>
b.html
window.parent.parent.location.hash = location.hash
// b.html 将后果放到 a.html 的 hash 值中,b.html 可通过 parent.parent 拜访 a.html 页面
c.html
console.log(location.hash) // #jackylin
let iframe = document.createElement('iframe')
iframe.src = 'http://localhost:8001/b.html#monkey'
document.body.appendChild(iframe)
总结
- CORS 反对所有的 HTTP 申请,是跨域最支流的计划
- 日常工作中,用得比拟多的跨域计划是 CORS 和 Node 中间件及 Nginx 反向代理
- 不论是 Node 中间件代理还是 Nginx 反向代理,次要是通过同源策略对服务器不加限度。
参考
- 跨域资源共享 CORS 详解
- 九种跨域形式实现原理(完整版)
- 可能是最好的跨域解决方案了
- ps:集体技术博文 Github 仓库,感觉不错的话欢送 star,给我一点激励持续写作吧~