解决跨域的方案
Tips: 对于前端页面的运行可以 使用 http-server
jsonp
原理
利用 <script>
标签没有跨域限制的漏洞
通过 <script>
标签的src属性指向一个需要访问的地址并提供一个回调函数来接收回调数据
script获取到的内容会被当做js脚本进行执行
所以需要服务端在回调上做一个字符串拼接操作 callbackFunName(内容)
可以通过url传递需要的参数
如需要发送一个get请求http://sugarat.top/path1/path2?param1=1
- 客户端注册一个全局方法
function callbackFunName(res){}
- 服务端收到请求后获取到url上的参数
- 服务端返回字符串
callbackFunName({"name":"sugar","age":18})
- 客户端当做js脚本直接解析执行
- 就调用了方法
callbackFunName
并把里面的{"name":"sugar","age":18}
当做一个对象进行了传递
只支持get请求
简单使用示例
服务端代码
// 以Node.js为例 const http = require('http') const app = http.createServer((req, res) => { const jsonData = { name: 'sugar', age: 18 } res.end(`diyCallBackFun(${JSON.stringify(jsonData)})`) }) app.listen(3000)
客户端代码
<script> // jsonp的回调函数 function diyCallBackFun(data) { console.log(data) } </script> <script> let $srcipt = document.createElement('script') $srcipt.src = 'http://localhost:3000/path1/path2?param1=1¶m2=2' document.body.appendChild($srcipt) </script> <!-- 最终构造出的标签 --> <!-- <script src="localhost:3000/path1/path2?param1=1¶m2=2"></script> -->
页面中插入上述代码并运行可以在控制台看机输出
通用方法封装
/** * JSONP方法 * @param {string} url 请求路径 * @param {string} callbackName 全局函数名称(后端拼接的方法名称) * @param {function} success 响应的回调函数 */ function jsonp(url, callbackName, success) { const $script = document.createElement('script') $script.src = url + `&callback=${callbackName}` $script.async = true $script.type = 'text/javascript' window[callbackName] = function (data) { success && success(data) } document.body.appendChild($script) }
CORS
跨域资源共享(Cross-origin resource sharing)
允许浏览器向跨域服务器发送ajax请求
实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信
服务端在响应头设置 Access-Control-Allow-Origin 就可以开启 CORS
原理
如果发起的跨域AJAX请求是简单请求,浏览器就会自动在头信息之中,添加一个Origin字段, 用来表示 请求来自哪个源
如:origin: http://localhost:8080
如果Origin的内容不包含在请求的响应头Access-Control-Allow-Origin
中,就会抛出以下错误
与CORS有关的以Access-Control-
开头的响应头:
- Access-Control-Allow-Origin:该字段是CORS中必须有的字段,它的值是请求时Origin字段的值以
,
分割多个域名,或者是*
,表示对所有请求都放行 - Access-Control-Expose-Headers:列出了哪些首部可以作为响应的一部分暴露给外部(XMLHttpRequest)
- 默认情况下,只有七种 simple response headers (简单响应首部)可以暴露给外部:
- Cache-Control:控制缓存
- Content-Language:资源的语言组
- Content-Length:资源长度
- Content-Type:支持的媒体类型
- Expires:资源过期时间
- Last-Modified:资源最后修改时间
- Pragma:报文指令
- Access-Control-Allow-Credentials:值类型是布尔类型,表示跨域请求是否允许携带cookie
- CORS请求默认不携带cookie
- 还需要设置xhr(XMLHttpRequest)对象的withCredentials属性为true
简单示例
以Node.js为例子
const http = require('http') let server = http.createServer(async (req, res) => { // -------跨域支持----------- // 放行指定域名 res.setHeader('Access-Control-Allow-Origin', '*') //跨域允许的header类型 res.setHeader("Access-Control-Allow-Headers", "*") // 允许跨域携带cookie res.setHeader("Access-Control-Allow-Credentials", "true") // 允许的方法 res.setHeader('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS') let { method, url } = req // 对预检请求放行 if (method === 'OPTIONS') { return res.end() } console.log(method, url) res.end('success') }) // 启动 server.listen(3000, err => { console.log(`listen 3000 success`); })
反向代理
因为跨域是针对浏览器做出的限制
对后端服务没有影响
可以使用 Nginx,Node Server,Apache等技术方案为请求做一个转发
下面是一些示例
Nginx配置
server { listen 80; listen 443 ssl http2; server_name test.sugarat.top; index index.php index.html index.htm default.php default.htm default.html; root /xxx/aaa; # 省略其它配置 location /api { proxy_pass http://a.b.com; # 防止缓存 add_header Cache-Control no-cache; } }
访问 http://test.sugarat.top/api/user/login
,实际是nginx服务器 访问http://a.b.com/api/user/login
关于proxy_pass
属性,更多详细内容可参考proxy_pass url 反向代理的坑
Node Server
这里采用Node原生http模块+axios实现请求的转发
const http = require('http') const axios = require('axios').default // 要转发到哪里去 const BASE_URL = 'http://www.baidu.com' // 启动服务的端口 const PORT = 3000 const app = http.createServer(async (req, res) => { const { url, method } = req console.log(url); // 对预检请求放行 if (method === 'OPTIONS') { return res.end() } // 获取传递的参数 const reqData = await getBodyContent(req) console.log(reqData); const { data } = await axios.request({ method, url, baseURL: BASE_URL, data: reqData }) res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Content-Type', 'application/json;charset=utf-8') res.end(JSON.stringify(data)) }) app.listen(PORT, () => { console.log(`listen ${PORT} success`); }) function getBodyContent(req) { return new Promise((resolve, reject) => { let buffer = Buffer.alloc(0) req.on('data', chunk => { try { buffer = Buffer.concat([buffer, chunk]) } catch (err) { console.error(err); } }) req.on('end', () => { let data = {} try { data = JSON.parse(buffer.toString('utf-8')) } catch (error) { data = {} } finally { resolve(data) } }) }) }
测试页面
<h1>测试</h1> <script> fetch('http://localhost:3000/sugrec?name=test').then(res=>res.json()).then(console.log) </script>
运行结果,请求被成功转发
websocket
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现
使用示例
客户端
<body> <p><span>链接状态:</span><span id="status">断开</span></p> <label for="content"> 内容 <input id="content" type="text"> </label> <button id="send">发送</button> <button id="close">断开</button> <script> const ws = new WebSocket('ws:localhost:3000', 'echo-protocol') let status = false const $status = document.getElementById('status') const $send = document.getElementById('send') const $close = document.getElementById('close') $send.onclick = function () { const text = document.getElementById('content').value console.log('发送: ', text); ws.send(text) } ws.onopen = function (e) { console.log('connection open ...'); ws.send('Hello') status = true $status.textContent = '链接成功' } $close.onclick = function () { ws.close() } ws.onmessage = function (e) { console.log('client received: ', e.data); } ws.onclose = function () { console.log('close'); status = false $status.textContent = '断开连接' } ws.onerror = function (e) { console.error(e); status = false $status.textContent = '链接发生错误' } </script> </body>
服务端
这里采用Node实现,需安装websocket
模块
const WebSocketServer = require('websocket').server; const http = require('http'); const server = http.createServer(function (request, response) { console.log((new Date()) + ' Received request for ' + request.url); response.writeHead(200); response.end(); }); server.listen(3000, function () { console.log((new Date()) + ' Server is listening on port 3000'); }); const wsServer = new WebSocketServer({ httpServer: server, autoAcceptConnections: false }); function originIsAllowed(origin) { return true; } wsServer.on('request', function (request) { if (!originIsAllowed(request.origin)) { request.reject(); console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.'); return; } var connection = request.accept('echo-protocol', request.origin); console.log((new Date()) + ' Connection accepted.'); connection.on('message', function (message) { if (message.type === 'utf8') { console.log('Received Message: ' + message.utf8Data); connection.sendUTF(`${new Date().toLocaleString()}:${message.utf8Data}`); } }); connection.on('close', function (reasonCode, description) { console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.'); }); });
运行结果
location.hash
location的hash值发生变化,页面不会刷新,且浏览器提供了hashchange事件
主要用于iframe跨域通信
示例
父页面
<body> <h1>父页面</h1> <button id="send">send</button> <iframe id="iframe1" src="http://localhost:3001/2.html"></iframe> <script> const $send = document.getElementById('send') const $iframe = document.getElementById('iframe1') const oldSrc = $iframe.src $send.onclick = function () { $iframe.src = oldSrc + '#' + Math.random() * 100 } </script> </body>
子页面
<body> <h1>子页面</h1> <script> window.addEventListener('hashchange',function(e){ console.log(e); console.log(location.hash); }) </script> </body>
运行结果
window.name
只要当前的这个浏览器tab没有关闭,无论tab内的网页如何变动,这个name值都可以保持,并且tab内的网页都有权限访问到这个值
iframe中的页面利用上述特性,实现任意页面的window.name的读取
使用示例
父页面 1.html
<body> <h1>父页面</h1> <button id="send">send</button> <script> document.getElementById('send').addEventListener('click', function () { getCrossIframeName('http://localhost:3000/2.html', console.log) }) function getCrossIframeName(url, callback) { let ok = false const iframe = document.createElement('iframe') iframe.src = url iframe.style.width = '0px' iframe.style.height = '0px' iframe.onload = function () { if (ok) { // 第二次触发时,同域的页面加载完成 callback(iframe.contentWindow.name) // 移除 document.body.removeChild(iframe) } else { // 第一次触发onload事件,定向到同域的中间页面 // 经测试 中间页面不存在也可以,如存在页面内容为空也可 iframe.contentWindow.location.href = '/proxy.html' ok = !ok } } document.body.appendChild(iframe) } </script> </body>
中间页面 proxy.html
<!-- 空文件即可 -->
目标页面 2.html
<body> <script> const data = { name: '传输的数据', status: 'success', num: Math.random() * 100 } window.name = JSON.stringify(data) </script> </body>
运行结果
window.postMessage
window.postMessage 方法可以安全地实现跨源通信,可以适用的场景:
- 与其它页面之间的消息传递
- 与内嵌iframe通信
用法
otherWindow.postMessage(message, targetOrigin);
targetOrigin值示例:
- 协议+主机+端口:只有三者完全匹配,消息才会被发送
- *:传递给任意窗口
- /:和当前窗口同源的窗口
使用示例
父页面
<body> <h1>父页面</h1> <button id="send">send</button> <iframe id="iframe1" src="http://localhost:3001/2.html"></iframe> <script> const $send = document.getElementById('send') const $iframe = document.getElementById('iframe1') const oldSrc = $iframe.src $send.onclick = function () { $iframe.contentWindow.postMessage(JSON.stringify({ num: Math.random() }),'*') } </script> </body>
子页面
<body> <h1>子页面</h1> <script> window.addEventListener('message', function (e) { console.log('receive', e.data); }) </script> </body>
运行结果
document.domain
二级域名相同的情况下,比如 a.sugarat.top 和 b.sugarat.top 适用于该方式。
只需要给页面添加 document.domain = 'sugarat.top' 表示二级域名都相同就可以实现跨域
简单示例
首先修改host文件,添加两个自定义的域名,模拟跨域环境
父页面
<body> <h1>父页面</h1> <iframe id="iframe1" src="http://b.sugarat.top:3000/2.html"></iframe> <script> document.domain = 'sugarat.top' var a = 666 </script> </body>
子页面
<body> <h1>子页面</h1> <script> document.domain = 'sugarat.top' console.log('get parent data a:', window.parent.a); </script> </body>
运行结果
总结
上文只是介绍了常见的一些跨域方案,并配上了能直接复制粘贴运行的示例,方便读者理解与上手体验
在实际生产环境中需针对特定的场景进行方案的pick
面试中这也是一道经典考题,望能帮助读者加深理解
参考
原文首发于个人博客,专题系列会整理多篇,与大家一起学习,共同进步,如有错误,还请斧正