1. 项目概述为什么全栈CSRF防御是每个开发者的必修课最近在Code Review一个前后端分离项目时我又一次看到了那个熟悉的“老朋友”——一个对敏感操作比如修改邮箱的POST请求前端只是简单调用了接口而后端除了校验登录态几乎没有任何额外的防护。这让我心里咯噔一下仿佛看到了一个敞开的保险柜。这个“老朋友”就是CSRFCross-Site Request Forgery跨站请求伪造。你可能觉得现在都用Token了CSRF不是老掉牙的问题了吗但现实是在React、Vue这类现代前端框架构建的单页应用SPA中CSRF的防御姿势如果不对或者前后端配合稍有疏忽防御就会形同虚设。尤其是在全栈开发的语境下前端工程师可能过度依赖后端后端工程师又可能对前端框架的请求机制理解不深导致防御链条出现缺口。这个项目标题“5个方案React/Vue全栈CSRF防御实战指南”精准地切中了这个痛点。它不是一个泛泛而谈的安全概念科普而是直接瞄准了“React/Vue”这个具体的技术栈和“全栈”这个协作场景提供了可落地的“实战指南”。对于正在使用或计划使用React、Vue进行全栈开发的工程师、架构师甚至是独立开发者这篇文章的价值在于它能帮你系统地构建起从浏览器到服务器贯穿整个请求生命周期的防御体系。你会明白防御CSRF不仅仅是后端加个Token那么简单它涉及到Cookie策略、同源策略、框架特性以及部署环境等一系列需要通盘考虑的因素。接下来我将结合多年踩坑经验为你拆解这5种核心防御方案的原理、适用场景、具体实现以及那些文档里不会写的“坑”。2. 核心防御方案全景解析与选型逻辑在深入代码之前我们必须先建立起一个宏观的认知CSRF攻击的本质是“冒用”攻击者诱导受害者的浏览器以其已登录的身份和权限向目标网站发起非本意的请求。因此所有防御方案的出发点都是让服务器能够区分“这是用户自愿发起的请求”还是“被伪造的请求”。基于这个核心业界成熟的防御思路主要围绕“验证请求来源”和“增加攻击者无法预测的凭证”两大方向展开。下面这5种方案我会按照从“基础必备”到“增强加固”的顺序并结合SPA的特点进行分析。2.1 方案一同步令牌模式Synchronizer Token Pattern这是最经典、最广为人知的方案也是很多框架如Spring Security, Django内置支持的方式。原理与流程用户访问网站服务器在生成页面或API响应时创建一个随机、不可预测的令牌Token将其存储在服务器的Session中同时通过某种方式传递给前端。前端在发起敏感请求如POST、PUT、DELETE时必须将这个令牌作为请求的一部分通常在请求头或请求体中携带给服务器。服务器收到请求后比对请求中的令牌和Session中存储的令牌是否一致。一致则认为是合法请求否则拒绝。在SPA中的挑战与实现 传统多页应用MPA中令牌可以轻松嵌入在表单的隐藏域里。但在SPA中页面一次性加载后续通过API交互令牌的获取和携带方式需要调整。获取令牌通常需要一个专门的API端点如GET /api/csrf-token来获取最新的CSRF令牌。这个端点应该设置为SameSiteStrict或Lax的Cookie或者通过响应体返回。携带令牌前端需要将这个令牌存储起来例如放在Vuex、Redux或内存中并在每次发起非幂等请求时将其添加到请求头中比如X-CSRF-TOKEN。令牌更新为了安全每次使用后或定期应更新令牌。注意切勿将CSRF令牌通过Cookie自动发送。因为Cookie会被浏览器自动携带这就失去了“攻击者无法预测”的意义。正确的做法是让前端JavaScript显式地读取令牌并添加到请求头。选型理由原理清晰防御效果好是许多安全规范的推荐做法。但它需要前后端配合并增加了一次获取令牌的API调用。2.2 方案二双重Cookie验证Double Submit Cookie这个方案巧妙利用了浏览器同源策略对Cookie读写的限制。原理与流程用户访问网站服务器在响应中设置一个Cookie例如csrf_tokenrandomValue。这个Cookie的SameSite属性可以设为Lax。同时服务器将这个randomValue也通过响应体如JSON或另一个Cookie但需前端JS可读传给前端。前端发起敏感请求时需要做两件事自动行为浏览器会依据SameSite规则自动在请求中带上csrf_token这个Cookie。手动行为前端JS需要将获取到的randomValue作为自定义请求头如X-CSRF-TOKEN或请求参数不推荐可能因日志泄露附加到请求上。服务器收到请求后同时检查请求头或参数中的值和Cookie中的值是否一致。因为攻击者无法读取目标站点的Cookie受同源策略保护所以他无法伪造出正确的请求头值。在SPA中的优势 相比同步令牌它减少了一次专门获取令牌的API调用。令牌Cookie由服务器在首次访问或登录后设置前端只需从首次响应的数据中或通过document.cookie需确保Cookie的HttpOnlyfalse这带来了XSS风险需权衡读取一次之后存储在内存中用于构造请求头即可。选型理由实现相对简单减少了网络交互。但需要注意防范XSS攻击因为如果Cookie不是HttpOnly被XSS脚本窃取后此方案将完全失效。因此确保没有XSS漏洞是该方案的前提。2.3 方案三基于Cookie的SameSite属性这是近年来随着浏览器标准升级而越来越主流的“基础设施级”方案它从请求发送的源头进行控制。原理SameSite是Cookie的一个属性用于限制第三方上下文即来自其他站点的请求携带Cookie。它有三个值Strict最严格。浏览器只会在同站请求即当前页面URL的域与请求目标域相同中携带此Cookie。这意味着从其他网站链接过来连登录态都会丢失用户体验可能受影响。Lax默认值在现代浏览器中。允许在顶级导航如点击链接且是安全的HTTP方法如GET中携带Cookie。但会阻止在跨站POST请求或通过img,script等标签发起的请求中携带Cookie。这能有效防御大多数CSRF攻击。NoneCookie将在所有上下文中发送但必须同时设置Secure属性即仅限HTTPS。在SPA中的实践 对于身份认证的Cookie如sessionId或token将其设置为SameSiteLax或Strict是当前防御CSRF最简单有效的方式之一。因为大多数CSRF攻击依赖于浏览器自动在跨站请求中携带用户的认证Cookie而Lax模式阻止了这一点。选型理由配置简单几乎零成本是必须启用的第一道防线。但它不是银弹因为老旧浏览器不支持。Lax对GET请求放行如果你的应用有通过GET方法执行敏感操作这是错误的设计依然存在风险。它保护的是Cookie本身不被跨站携带但如果你的应用使用其他方式认证如Bearer Token放在请求头则此属性无效。2.4 方案四验证请求头如Origin/Referer这个方案依赖于HTTP标准头来检查请求来源。原理与流程 服务器检查请求头中的Origin或Referer字段判断其是否来源于受信任的站点即你自己的网站域名。Origin存在于POST、跨域等请求中标明请求发起的源协议域名端口不包含路径。它比Referer更安全因为不会被篡改在浏览器控制下。Referer包含了完整的来源URL可能包含路径和查询参数但存在被某些浏览器配置或扩展移除的风险以及可能泄露敏感路径信息。在SPA中的注意事项同源请求可能没有Origin头浏览器在发起同源的XMLHttpRequest或Fetch请求时默认不发送Origin头。这会导致你的校验逻辑失败。解决方案是前端在发起请求时可以手动添加一个自定义头如X-Requested-With: XMLHttpRequest后端同时校验这个自定义头和Origin/Referer的存在性。处理空值需要制定明确的策略。例如如果Origin和Referer头均缺失可以认为请求来自同源需结合其他验证或者直接拒绝。策略必须统一且安全。选型理由实现简单可以作为辅助验证手段。但它不能作为唯一的防御措施因为Referer可能被篡改或缺失且在某些合法场景如从HTTPS跳转到HTTP或用户隐私设置下会不被发送。通常与Token方案结合使用。2.5 方案五自定义请求头与预检请求CORS Preflight的利用这是一个非常巧妙且适合纯API服务如前后端完全分离部署在不同域名的方案。原理 利用CORS跨源资源共享机制。当浏览器发起一个非简单请求例如带有自定义头X-CSRF-TOKEN的请求到不同源的地址时会先发起一个OPTIONS方法的预检请求。服务器必须在预检请求的响应中明确允许该自定义头浏览器才会发送真正的请求。关键点攻击者通过form或img发起的CSRF请求无法添加自定义HTTP头。因此如果后端只接受带有特定自定义头如X-CSRF-TOKEN的敏感请求那么来自恶意网站的伪造请求会因为缺少这个头在预检阶段就被浏览器阻止根本到不了服务器。在SPA中的实现后端为所有需要CSRF保护的API端点配置CORS在Access-Control-Allow-Headers中包含你的自定义CSRF头如X-CSRF-TOKEN。前端在发起所有非GET请求时统一添加该自定义头。头的值可以来自上述任一Token方案。后端校验该自定义头的存在性和有效性。选型理由对于跨域部署的SPA非常优雅将一部分校验工作交给了浏览器标准。但它依赖于CORS的正确配置并且要求前端必须使用JavaScript发起请求Ajax/Fetch对于服务端渲染或混合应用场景可能不适用。3. React/Vue全栈实战方案组合与代码实现理论讲完我们来点实在的。在实际的全栈项目中我们很少只采用单一方案而是根据应用架构进行组合。这里我以“同步令牌自定义请求头CORS”作为主流SPA的推荐组合给出React和Vue下的前后端实战代码片段。3.1 后端实现以Node.js Express为例首先我们构建一个提供令牌和进行校验的中间件。// middleware/csrfMiddleware.js const crypto require(crypto); // 生成随机令牌 const generateToken () crypto.randomBytes(32).toString(hex); // CSRF令牌管理中间件 const csrfProtection (req, res, next) { // 1. 为GET请求或首次访问提供令牌 if (req.method GET req.path /api/csrf-token) { const token generateToken(); // 将令牌存入session这里用内存示例生产环境用Redis等 req.session.csrfToken token; // 通过JSON响应返回令牌前端需要手动读取并存储 return res.json({ csrfToken: token }); } // 2. 对非安全方法POST, PUT, DELETE, PATCH进行校验 const safeMethods [GET, HEAD, OPTIONS]; if (!safeMethods.includes(req.method)) { const clientToken req.headers[x-csrf-token]; // 从前端自定义头获取 const serverToken req.session.csrfToken; if (!clientToken || clientToken ! serverToken) { // 令牌缺失或不匹配记录日志并返回403 console.warn(CSRF validation failed for ${req.method} ${req.path}); return res.status(403).json({ error: Invalid CSRF token }); } // 校验通过后可以选择更新令牌增加安全性 // req.session.csrfToken generateToken(); } next(); }; // CORS配置中间件配合自定义头方案 const corsOptions { origin: process.env.FRONTEND_URL || http://localhost:3000, // 你的前端地址 credentials: true, // 允许携带Cookie如果需要 allowedHeaders: [Content-Type, Authorization, X-CSRF-TOKEN], // 允许的自定义头 }; module.exports { csrfProtection, corsOptions };关键点解析我们为GET /api/csrf-token这个专用端点生成并返回令牌。令牌存储在服务端Session中。对于非安全方法我们要求请求头X-CSRF-TOKEN必须存在且与Session中的值匹配。CORS配置中明确允许了X-CSRF-TOKEN这个自定义头这样前端跨域请求时才能成功。3.2 前端实现React篇使用Axios在React项目中我们通常会在请求拦截器中统一处理CSRF令牌。// utils/axiosInstance.js import axios from axios; const axiosInstance axios.create({ baseURL: process.env.REACT_APP_API_BASE_URL, withCredentials: true, // 如果需要发送认证Cookie则设为true }); let csrfToken null; // 定义一个函数来获取CSRF令牌 const fetchCsrfToken async () { try { const response await axiosInstance.get(/api/csrf-token); csrfToken response.data.csrfToken; return csrfToken; } catch (error) { console.error(Failed to fetch CSRF token:, error); // 可以根据错误类型进行重试或跳转登录 throw error; } }; // 请求拦截器为所有非GET请求添加CSRF令牌头 axiosInstance.interceptors.request.use( async (config) { const { method } config; // 如果是非安全方法需要CSRF令牌 if (method ![get, head, options].includes(method.toLowerCase())) { // 如果内存中没有令牌则先获取一次 if (!csrfToken) { await fetchCsrfToken(); } // 添加自定义请求头 config.headers[X-CSRF-TOKEN] csrfToken; } return config; }, (error) { return Promise.reject(error); } ); // 响应拦截器处理令牌过期或无效的情况例如403错误 axiosInstance.interceptors.response.use( (response) response, async (error) { const originalRequest error.config; // 如果是CSRF令牌错误假设后端返回403并且尚未重试过 if (error.response?.status 403 !originalRequest._retry) { originalRequest._retry true; // 尝试重新获取CSRF令牌 await fetchCsrfToken(); // 更新原请求的CSRF头 originalRequest.headers[X-CSRF-TOKEN] csrfToken; // 重新发起请求 return axiosInstance(originalRequest); } return Promise.reject(error); } ); export default axiosInstance;使用方式 在你的React组件中直接导入这个配置好的axiosInstance进行API调用即可无需再关心CSRF令牌的细节。// components/UserProfile.js import React, { useState } from react; import axiosInstance from ../utils/axiosInstance; function UserProfile() { const [email, setEmail] useState(); const handleUpdateEmail async () { try { // 发起POST请求拦截器会自动处理CSRF令牌 await axiosInstance.post(/api/user/email, { email }); alert(邮箱更新成功); } catch (error) { alert(更新失败 error.message); } }; return ( div input value{email} onChange{(e) setEmail(e.target.value)} / button onClick{handleUpdateEmail}更新邮箱/button /div ); }3.3 前端实现Vue篇使用Axios Vue插件在Vue项目中我们可以将Axios实例挂载为全局属性或使用Vue插件实现类似的效果。// plugins/axios.js import axios from axios; // 创建axios实例 const axiosInstance axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL, withCredentials: true, }); let csrfToken null; const fetchCsrfToken async () { const response await axiosInstance.get(/api/csrf-token); csrfToken response.data.csrfToken; return csrfToken; }; // 请求拦截器 axiosInstance.interceptors.request.use( async (config) { const method config.method?.toLowerCase(); if (method ![get, head, options].includes(method)) { if (!csrfToken) { await fetchCsrfToken(); } config.headers[X-CSRF-TOKEN] csrfToken; } return config; }, (error) Promise.reject(error) ); // 响应拦截器 axiosInstance.interceptors.response.use( (response) response, async (error) { const originalRequest error.config; if (error.response?.status 403 !originalRequest._retry) { originalRequest._retry true; await fetchCsrfToken(); originalRequest.headers[X-CSRF-TOKEN] csrfToken; return axiosInstance(originalRequest); } return Promise.reject(error); } ); // 作为Vue插件安装 const AxiosPlugin { install(Vue) { Vue.prototype.$http axiosInstance; }, }; export { AxiosPlugin, axiosInstance };在main.js中安装插件// main.js import Vue from vue; import App from ./App.vue; import { AxiosPlugin } from ./plugins/axios; Vue.use(AxiosPlugin); new Vue({ render: h h(App), }).$mount(#app);在Vue组件中使用!-- components/UserProfile.vue -- template div input v-modelemail / button clickupdateEmail更新邮箱/button /div /template script export default { data() { return { email: , }; }, methods: { async updateEmail() { try { await this.$http.post(/api/user/email, { email: this.email }); alert(邮箱更新成功); } catch (error) { alert(更新失败 error.message); } }, }, }; /script3.4 实战心得部署与运维中的关键配置代码写完了部署上线才是真正的考验。这里有几个容易忽略但至关重要的点Session存储上述示例用了内存Session这在单机开发没问题但生产环境多实例部署时必须使用外部集中存储如Redis、Memcached或数据库。否则用户请求打到不同服务器实例上Session中的CSRF令牌会不一致导致校验失败。Cookie的SameSite与Secure即使你用了Token方案也务必将你的会话Cookie如sessionId设置为SameSiteLax或Strict并确保在生产环境HTTPS下设置Securetrue。这是纵深防御的重要一环。API网关与负载均衡如果你的架构中有API网关如Nginx, Kong确保它不会过滤或修改关键的请求头特别是Origin,X-CSRF-TOKEN等。前端路由与令牌获取时机在SPA中用户可能直接访问深链接如/user/profile。你需要确保应用初始化时例如在Vue的router.beforeEach或React的顶级组件useEffect中就尝试获取一次CSRF令牌避免第一个敏感请求因无令牌而失败。4. 常见问题排查与进阶防御策略即使按照上面的步骤做了在实际开发中你依然会遇到各种奇怪的问题。下面是我总结的“排坑指南”和更深层的防御思考。4.1 问题排查速查表问题现象可能原因排查步骤与解决方案前端请求返回403Invalid CSRF token1. 前端未正确携带令牌。2. 后端Session丢失或令牌不匹配。3. 跨域请求被CORS策略阻止。1.检查浏览器开发者工具Network标签确认请求头中是否有X-CSRF-TOKEN值是否正确。2.检查后端Session确认请求对应的Session中是否存在csrfToken且值是否与前端发送的一致。检查Session存储是否正常工作多实例部署时重点排查。3.检查CORS预检请求对于跨域POST请求先看是否有OPTIONS请求其响应头Access-Control-Allow-Headers是否包含X-CSRF-TOKEN。首次敏感请求成功后续请求失败后端在校验后更新了令牌但前端仍在使用旧的令牌。检查后端校验逻辑。如果选择了“每次校验后更新令牌”的策略后端必须在响应中返回新的令牌例如在JSON body的某个字段前端响应拦截器需要捕获并更新内存中的令牌。这是一个更安全但更复杂的流程需前后端约定好。本地开发正常上线后失败1. 生产环境前端域名/端口与后端不同跨域问题。2. 生产环境Cookie的SameSite/Secure属性配置更严格。3. 生产环境使用了CDN缓存了获取令牌的GET请求。1. 确认生产环境CORS配置的origin是否正确。2. 检查生产环境服务器设置的Cookie属性确保SameSite和Secure符合预期。3.切勿缓存CSRF令牌接口在GET /api/csrf-token的响应头中添加Cache-Control: no-store, max-age0。在iframe中发起请求失败如果会话Cookie设置了SameSiteLax或Strict在跨域的iframe中发起的请求不会携带该Cookie导致Session查找失败进而CSRF校验失败。1. 避免在跨域iframe中进行敏感操作。2. 如果业务必须考虑使用SameSiteNone; Secure的Cookie并配合严格的X-Frame-Options或Content-Security-Policy的frame-ancestors指令来防止点击劫持。4.2 进阶策略防御“同站”CSRF与Token泄露上述方案主要防御“跨站”请求伪造。但CSRF还有一种更隐蔽的变种——“同站”CSRF。如果攻击者能在你的主站域名下例如通过子域名漏洞、XSS注入恶意页面他发起的请求就是“同站”的SameSiteCookie限制和Origin检查都会失效。防御同站CSRF根治XSS这是所有客户端安全问题的根源。严格实施输入输出编码、使用CSP内容安全策略等。关键操作使用二次确认对于修改密码、转账等极高危操作强制要求用户进行二次验证如输入密码、验证码。为不同操作使用独立令牌可以为“修改邮箱”和“修改密码”生成不同的CSRF令牌甚至每次操作都使用一次性令牌进一步提升安全性。Token泄露的应对 如果CSRF令牌因为XSS漏洞被窃取那么所有基于令牌的防御都会失效。因此尽量缩短令牌有效期可以将会话CSRF令牌与用户操作绑定操作完成后即失效。绑定用户上下文在生成CSRF令牌时不仅使用随机数还可以混合用户ID、会话ID的哈希值使得被盗的令牌无法被其他用户使用。监控异常记录CSRF校验失败的日志短时间内大量失败请求可能预示着攻击或令牌泄露。4.3 自动化测试与安全扫描防御措施上线后如何确保其持续有效单元/集成测试编写测试用例模拟携带/不携带/携带错误CSRF令牌的请求断言其是否返回预期的成功/403状态。E2E测试使用Cypress、Playwright等工具模拟真实用户流程确保整个链路的CSRF防护正常工作。定期安全扫描使用ZAP、Burp Suite等工具对应用进行主动扫描检测CSRF等漏洞。确保扫描配置能正确处理你应用的认证和令牌机制。5. 方案选型决策树与架构适配建议面对这么多方案到底该怎么选我画了一个简单的决策树来帮你快速判断首先为所有会话Cookie设置 SameSiteLax (或 Strict)。 | v 你的前端和后端是否部署在同一个域名下 / \ 是 否跨域部署 | | v v 方案选择灵活。 必须配置CORS并强烈推荐使用 推荐组合 【方案五自定义请求头】。 【方案一同步令牌】 自定义请求头。 或【方案二双Cookie】。 | v 是否需要极致的简洁性且能确保无XSS风险 / \ 是 否 | | v v 考虑【方案二双Cookie验证】。 优先使用【方案一同步令牌】。 | v 是否担心“同站”CSRF或需要更高安全等级 / \ 是 否 | | v v 实施进阶策略 当前方案已足够。 - 关键操作二次验证。 - 操作绑定一次性令牌。架构适配建议传统服务端渲染SSR应用优先使用框架内置的CSRF中间件如同步令牌令牌可直接嵌入在页面表单中。现代SPAReact/Vue/Angular推荐同步令牌 自定义请求头 CORS组合。这是目前最主流、最健壮的方案。基于JWT的无状态API由于没有服务器Session同步令牌模式需要调整。可以将CSRF令牌作为JWT的一个字段csrf签发前端将其从解码后的JWT中取出放入自定义请求头。后端校验JWT签名后再比对请求头中的CSRF字段与JWT中的是否一致。移动端/桌面端原生App这些环境不受浏览器同源策略限制但可能嵌入WebView。方案核心不变使用令牌但获取和携带令牌的方式需遵循原生网络库的规则。最后我想分享一个深刻的体会安全是一个过程而不是一个功能。CSRF防御方案的实现从设计、编码、测试到部署运维需要前后端工程师密切协作对HTTP协议、浏览器安全和应用架构有共同的理解。千万不要认为用了某个框架或库就高枕无忧定期复查你的安全配置关注浏览器安全标准的更新比如SameSite默认值的变化才能让你的应用在多变的环境中保持坚固。