跨域

本文最后更新于 2025年8月3日 下午

参考:

什么是跨域

同源策略

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSRF 等攻击。一个域名地址由 协议、子域名、主域名、端口号、请求资源地址 组成。所谓同源是指 “协议+域名+端口” 三者相同,即便两个不同的域名指向同一个 IP 地址,也非同源。

同源策略限制内容有:

  • Cookie、WebStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 通信请求

有三个标签是允许跨域加载资源:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

跨域场景

协议子域名主域名端口号中任意一个不相同时,都算不同域。不同域之间相互请求资源为“跨域”。

常见跨域场景(来自掘金@浪里行舟)

特别说明两点:

  • 第一,如果是协议和端口造成的跨域问题“前台”是无能为力的。

  • 第二,在跨域问题上,仅仅是通过“URL 的首部”来识别,而不会根据域名对应的 IP 地址是否相同来判断。(“URL 的首部”可以理解为“协议、域名、端口三者必须匹配”)

请求跨域了,那么请求到底发出去没有?

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求是发出了。

跨域解决方案

JSONP

原理

利用<script>标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP 请求一定需要对方的服务器做支持才可以。

JSONP 和 AJAX 都是客户端向服务器端发送请求,从服务器端获取数据的方式。
但是,AJAX 属于同源策略,JSONP 属于非同源策略(跨域请求)。

优缺点

JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是其仅支持 GET 方法具有局限性,而且不安全,可能会遭受 XSS 攻击。想使用完整的 REST 接口,请使用 CORS 或者其他代理方式。

实现流程

1.前端定义解析函数(例如 jsonpCallback = function(){….})。 2.通过 params 形式包装请求参数,并且声明执行函数(例如 cb = jsonpCallback)。 3.后端获取前端声明的执行函数(jsonpCallback),并以带上参数并调用执行函数的方式传递给前端。

  • 声明一个回调函数,其函数名(如 show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的 data)。
  • 创建一个<script>标签,把那个跨域的 API 数据接口地址,赋值给 script 的 src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
  • 服务器接收到请求后,进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是 show,它准备好的数据是 show(’我不爱你’)。
  • 最后服务器把准备的数据通过 HTTP 协议返回给客户端,客户端再调用执行之前的回调函数(show),对返回的数据进行操作。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,可以自己封装一个 JSONP 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 前端
<script type="text/javascript">  
window.jsonpCallback = function(res{
console.log(res);  
};
</script>
<script type="text/javascript"
src="http://localhost:8080/api/jsonp?msg=hello&cb=jsonpCallback">
</script>
// 后端
const Koa = require("koa"), fs = require("fs"), app = new Koa();
app.use(async (ctx, next) => {
if (ctx.path === "/api/jsonp") {
const { cb, msg } = ctx.query;    
ctx.body = `${cb}(${JSON.stringify({ msg })})`;    
return;  
}
});
app.listen(8080);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 前端
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 = { ...params, callback }; // wd=b&callback=show
let arrs = [];
for (const key in params) {
arrs.push(`${key}=${params[key]}`);
}
script.src = `${url}?${arrs.join("&")}`;
document.body.appendChild(script);
});
}
jsonp({
url: "http://localhost:3000/say",
params: { wd: "Iloveyou" },
callback: "show",
}).then((data) => {
console.log(data);
});
// 后端
let express = require("express");
let app = express();
app.get("/say", function (req, res) {
let { wd, callback } = req.query;
console.log(wd); // Iloveyou
console.log(callback); // show
res.end(`${callback}('我不爱你')`);
});
app.listen(3000);

这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后端返回 show(‘我不爱你’),最后会运行 show() 这个函数,打印出’我不爱你’。

CORS

原理

跨域资源共享(CORS)是一种机制,它使用额外的 HTTP 头来告诉浏览器,让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器 「不同的域、协议或端口」请求一个资源时,资源会发起一个「跨域 HTTP 请求」。

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就能够实现跨域。服务端设置 Access-Control-Allow-Origin 就可以开启 CORS,该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。

简单请求

不会触发“CORS 预检请求”,这样的请求为“简单请求”。同时满足以下五个条件,就属于简单请求。

条件 1,使用下列方法之一:

  • GET
  • HEAD
  • POST

条件 2,人为设置以下集合外的请求头:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (需要注意额外的限制)
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width

条件 3,Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

条件 4:
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

条件 5:
请求中没有使用 ReadableStream 对象。

复杂请求

不符合简单请求的就是复杂请求。复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为”预检”请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

前端示例

仅显示前端部分。

1
2
3
4
5
6
7
8
9
10
11
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>

// 简单请求
<script>  
axios.get("http://127.0.0.1:8080/api/corslist");
</script>

// 复杂请求
<script>  
axios.get("http://127.0.0.1:8080/api/corslist", { headers: { cc"xxx" } });
</script>

window.postMessage

原理

window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 Document.domain 设置为相同的值)时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

window.postMessage() 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的 iframe 消息传递
  • 上面三个场景的跨域数据传递

window.postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以用来实现跨文本档、多窗口、跨域消息传递。

使用

otherWindow.postMessage(message, targetOrigin, [transfer]);

  • otherWindow:其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。
  • message: 将要发送到其他 window 的数据。
  • targetOrigin: 通过窗口的 origin 属性来指定哪些窗口能接收到消息事件。
  • transfer(可选) : 是一串和 message 同时传递的 Transferable 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
1
2
3
4
5
6
7
8
9
10
11
12
// 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('我爱你', 'http://localhost:4000') // 发送数据
window.onmessage = function(e) { // 接受返回数据
console.log(e.data) // 我不爱你
}
}
</script>
1
2
3
4
5
6
7
// b.html
<script>
window.onmessage = function(e) {
console.log(e.data) // 我爱你
e.source.postMessage('我不爱你', e.origin)
}
</script>

http://localhost:3000/a.html页面向http://localhost:4000/b.html传递“我爱你”,后者传回”我不爱你”。

WebSocket

原理

WebSocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket 和 HTTP 都是应用层协议,都基于 TCP 协议。但 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关。

原生 WebSocket API 使用起来不太方便,可以使用Socket.io,它很好地封装了 WebSocket 接口,提供了更简单、灵活的接口,也对不支持 WebSocket 的浏览器提供了向下兼容。

使用

本地文件socket.htmllocalhost:3000发生数据和接受数据。

1
2
3
4
5
6
7
8
9
10
// socket.html
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('我爱你'); // 向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data); // 接收服务器返回的数据
}
</script>
1
2
3
4
5
6
7
8
9
10
11
// server.js
let express = require("express");
let app = express();
let WebSocket = require("ws"); // 记得安装ws
let wss = new WebSocket.Server({ port: 3000 });
wss.on("connection", function (ws) {
ws.on("message", function (data) {
console.log(data);
ws.send("我不爱你");
});
});

使用原生 WebSocket API。

1
2
3
4
5
6
7
8
9
10
// 前端部分
<script>  
let socket = new WebSocket("ws://localhost:8080");  
socket.onopen = function({    
socket.send("秋风的笔记");  
};  
socket.onmessage = function(e{    
console.log(e.data);  
};
</script>
1
2
3
4
5
6
7
8
9
10
// 后端部分
<script>
const WebSocket = require("ws");
const server = new WebSocket.Server({ port8080 });
server.on("connection"function(socket{  
socket.on("message"function(data{    
socket.send(data);  
});
});
</script>

其它

(略)见:https://juejin.cn/post/6844903767226351623#heading-16。


跨域
https://xuekeven.github.io/2021/09/25/跨域/
作者
Keven
发布于
2021年9月25日
更新于
2025年8月3日
许可协议