
1. 这年头还在被CSRF打脸先说说我当年踩的坑先说结论CSRF跨站请求伪造这玩意儿2026年了依然是Web安全里的“老熟人”而且远没到可以躺平的地步。很多前端同学觉得CSRF是后端的事Token是后端生成的自己只要把Token塞进请求头里就行——我当年也是这么想的直到线上环境被人用CSRF刷爆了接口踩了一周坑才明白前端在CSRF防御里的角色远不是“工具人”那么简单。那是一次典型的攻击用户在登录态有效期内访问了一个恶意站点。恶意站点里藏了一张图片src指向我方网站的某个“改密码”或“发消息”接口。浏览器自动带上Cookie请求合法发出后端傻傻地信任了于是用户在不知情的情况下完成了敏感操作。更阴的是那张图片的地址还有参数攻击者连耗时都省了。那会儿我方的接口虽然校验了登录态但完全没有防CSRF措施。攻击者不需要拿到你的Cookie不需要知道你的密码只需要“借用”你的身份发一个请求即可。事后排查日志时请求的Referer字段直接暴露了来源是那个恶意域名但后端压根没看。说白了不是我技术多差而是从设计上就没把“请求来源不可信”当回事。这篇文章我就把CSRF的原理、同源策略和Token方案的细节一次讲透所有方法都验证过、坑都踩过按步骤抄就行。不管你是在维护老项目还是在搭建2026年的新前端基座这套组合拳都能用。2. 先搞懂CSRF的本质浏览器帮你背了黑锅2.1 CSRF为什么能成功核心就三个条件CSRF能成立需要同时满足三个条件缺一个都打不起来。记住这个框架你排查和防御就有方向了。第一个条件是用户处于登录态。也就是说被害者在目标站点有一个有效的会话通常是Cookie里有会话ID或者其他自动携带的身份凭证。第二个条件是目标接口存在副作用。所谓副作用就是这次请求会改变数据状态——改密码、发帖、转账、删数据都算GET、POST都可能中招。如果接口是纯查询、不产生状态变更CSRF的破坏力就小很多。第三个条件是浏览器自动携带凭证。Cookie的SameSite属性如果没设置成Lax或Strict跨站请求时Cookie就跟着走了这正是CSRF能得手的关键。为什么说“浏览器帮你背了黑锅”因为攻击者并不需要拿到你的Cookie文件浏览器出于设计原因会在你访问第三方站点、向目标站点发请求时自动带上Cookie。前端没法用JavaScript直接读取Cookie有HttpOnly保护但这恰恰成了CSRF可利用的点我读不到但我能用。2.2 同源策略在这件事里扮演什么角色很多人把同源策略理解成“浏览器不允许跨域请求”这其实是错的。同源策略限制的是“跨域读取响应”而不是“跨域发送请求”。举个例子页面A域名是a.example.com它通过img或form向b.example.com发请求浏览器根本不会拦——请求照样发出去Cookie照样带上只是JS拿不到响应内容而已。这就有意思了同源策略帮不了你挡住CSRF的请求发出但它能帮你判断请求是否来自可信来源。服务端拿到的Origin头、Referer头就是浏览器基于同源策略标记出来的来源信息。如果一次“合法操作”的请求来源是一个完全陌生的站点那你基本可以断定这是CSRF。所以我一直跟团队里的人说同源策略不是用来“防”CSRF的它是用来“识别”CSRF的。真正的拦截动作要么在后端校验来源要么靠前端/后端配合加Token要么在浏览器层面用SameSite把Cookie管住。2.3 2026年了为什么CSRF还没死有个很自然的疑问既然SameSite Cookie已经默认Lax了CSRF是不是已经过时了我只能说理想很丰满现实很骨感。SameSite默认Lax确实堵掉了大部分跨站GET请求但有几个漏洞依旧存在。第一SameSiteNone; Secure的Cookie照样在跨站请求里发送很多第三方登录、嵌入场景必须这么配。第二同站same-site不等于同源same-originsite是协议注册域名a.example.com和b.example.com虽然源不同但属于同站SameSite管不住这种请求。第三老项目里Cookie的SameSite属性可能压根没设置过或者项目里一直用的是旧浏览器内核。再加上局部网络环境里的代理、客户端WebView等场景浏览器策略并不总是生效。我自己的结论是浏览器层面的防御都在“尽力而为”但你不能把身家性命全押在浏览器策略上。服务端校验和Token机制必须兜底。这也是为什么2026年我写这篇东西不是考古而是真的还有人在裸奔。3. Token方案拆解前端到底该怎么接怎么发3.1 双提交TokenDouble Submit Cookie为什么适合前端现在主流的CSRF防御方案里有一个对前端特别友好的双提交Cookie机制。原理是后端在用户会话里随机生成一个Token把它同时写入Cookie和返回体或由前端自行存储。前端在发请求时从Cookie里读出这个Token放到自定义请求头里。后端校验时只需比对请求头里的Token和Cookie里的Token是否一致如果一致就放行。为什么说它适合前端因为它不需要服务端存储会话Token天然适配无状态服务。后端不需要维护一个巨大的Token字典也不用在分布式场景下做同步只要比对两个值是否相等就行。而前端要做的就是在请求拦截器里“读Cookie、塞Header”这么简单。当然这个方案的前提是Cookie里的Token不能被跨域读取。这就要给Token Cookie加上HostOnly属性也就是不设置Domain和HttpOnly关闭因为前端要读。有人会问HttpOnly关了是不是不安全这里要说明白Token Cookie本身就是用来让前端读的它防的不是脚本盗取而是防跨域请求时被自动带上并被服务端误判。只要同源策略正常第三方脚本没法跨域读你的Cookie双提交就能撑起一片天。3.2 前端拦截器的标准写法以Axios为例一个干净的前端CSRF拦截器大概是这样的import Cookies from js-cookie; const CSRF_COOKIE_NAME csrf_token; const CSRF_HEADER_NAME X-CSRF-Token; // 仅对写操作启用CSRF防护 const SAFE_METHODS [GET, HEAD, OPTIONS, TRACE]; axios.interceptors.request.use(config { const method (config.method || get).toUpperCase(); // GET等幂等请求一般不需要加Token但如果你后端要求全加也不冲突 if (SAFE_METHODS.includes(method)) { return config; } // 只在同源请求下自动加Token避免跨域场景误伤 const targetUrl new URL(config.url, window.location.origin); if (targetUrl.origin ! window.location.origin) { return config; } const token Cookies.get(CSRF_COOKIE_NAME); if (token) { config.headers[CSRF_HEADER_NAME] token; } return config; }, error Promise.reject(error));这里有几个细节值得你多看一眼。第一为什么只拦截写操作因为CSRF的危害集中在有副作用的请求上GET通常只做查询。只给写操作加Token既降低了所有请求的复杂度也减少了误伤。第二为什么只对同源请求加第三方接口、跨域调用场景下对方自己的CSRF策略我们管不了不掺和是最好的。第三为什么用js-cookie因为原生Cookie API没有“按名取一个值”这么便捷的方法手写解析Cookie字符串很啰嗦用库能少些低级错误。3.3 同步表单场景别忘了隐藏域单页应用用拦截器很顺手但如果项目里还有传统同步表单——比如服务端渲染的页面登录、留言、支付这些还是整页提交——拦截器就失效了。因为同步表单提交是浏览器原生行为不经过Axios。这时候你需要往form里塞一个隐藏域form action/api/transfer methodPOST input typehidden namecsrf_token value{{.CSRFToken}} input typetext nameaccount input typetext nameamount button typesubmit确认转账/button /form后端模板渲染时把Token塞进隐藏域提交后后端比对隐藏域里的Token与会话里的Token。这个方案在传统后端框架比如服务端渲染的Java、PHP、Python项目里极其常见。前端别觉得这不关自己的事如果你负责的前端工程里有模板引擎这活儿就是你的事。3.4 Token放Cookie还是放内存我的取舍建议关于Token存哪江湖上一直有分歧。有人坚持放内存比如Vuex、Redux、Pinia刷新页面后重新拉取有人喜欢放Cookie图省事。我的建议是看你项目的实际场景。如果后端用的是“会话内校验”模式也就是后端在Session里存Token那前端放内存完全没问题缺点是刷新后Token没了得重新获取。如果后端用的是“双提交”模式那Token必须在Cookie里否则校验无从谈起。如果项目里既有服务端渲染页面、又有API请求那Cookie放Token是唯一的选择不然两种场景要维护两份Token。我个人在实际项目中更倾向Cookie放Token 自定义Header的模式原因很简单前端实现最统一所有请求走一套逻辑服务端渲染页面通过隐藏域读同一个Cookie值两边不会打架。但要注意Cookie的SameSite要设成Lax这样跨站脚本没法通过Cookie“借用”Token去构造请求——组合拳的意义就在这儿。4. 同源策略侧的防御实操Origin、Referer一个也别放过4.1 为什么服务端必须校验Origin和Referer你可能会想前端加一个X-CSRF-Token头就够了吧不不够。为什么因为攻击者完全可以先在你的站上触发一个请求比如通过某个未过滤的JSONP接口、或者通过表单探测把Token偷出来再构造恶意请求。Token是重要防线但不能是唯一防线。服务端校验Origin或Referer是最朴素但非常有效的辅助手段。在CSRF攻击中恶意请求的来源几乎必然不是你的站点。如果后端统一校验这个来源攻击者连“偷Token”这一步都没机会——请求直接被打回。我在实际项目中要求后端对两类头做校验一类是Origin只要有这个头就必须在信任名单里另一类是Referer在Origin缺失的旧浏览器场景下兜底。顺序上以Origin为主Referer为辅两个都缺失的请求要小心但也不一定全拦——因为有些隐私设置会禁用Referer这里有误杀风险。4.2 前端该配合做什么把机密信息赶出URL前端在“源头治理”上的第二件事是确保敏感操作的参数不落在URL上。很多老项目有这种坏毛病转账金额、目标账号、操作类型统统拼在URL查询参数里。这不光是日志泄漏风险还让CSRF攻击者造一个img或a链接变得毫无难度。我见过一个惨烈的例子某后台的“删除文章”功能是GET /admin/article/delete?id123攻击者只需要在恶意页面上放一张src指向这个地址的图片点都不需要点只要加载页面就删了。这还是普通操作如果是改密码接口带newPassword参数的那画面不敢想。所以前端在设计和开发时就要立规矩所有有副作用的操作一律使用POST、PUT、DELETE等非GET方法。敏感参数放进请求体或请求头不拼URL。请求签名、校验码等机密信息禁止出现在URL上。你可能会觉得这是后端该定的约束但前端在联调时天天写接口第一时间就该发现“这个接口怎么是GET删除”然后怼回去改掉而不是默默接上。这种安全意识2026年的前端必须要有。4.3 跨域场景的例外处理别把CORS和CSRF搞混这里要特别提一下跨域请求。很多人一听到“跨域”就觉得要放宽CSRF限制这其实是把CORS和CSRF混为一谈了。CORS是浏览器帮你决定“能不能读取响应”的机制跨域请求发出去了但响应能否被JS读取要看后端是否返回Access-Control-Allow-Origin。而CSRF关心的是“请求是否被伪造”一个请求不管是同源还是跨域发来的只要它带着合法身份凭证就可能被利用。所以结论是跨域场景下你反而要更严格地对待CSRF。如果后端开启了CORS并允许携带Cookie那这个接口天然暴露面更大。此时除了Token还建议加上精确的Access-Control-Allow-Origin白名单禁止使用*。对自定义头如X-CSRF-Token明确加入预检请求的处理。敏感接口校验Origin的时候把信任名单维护好不用的域名别留。我见过一个真实事故某项目为了让某个小程序的WebView跨域调用后端把CORS开成了*结果CSRF的Token也等于是“公开的”。不是Token没生成是Token白发了——攻击者直接发一个fetch带上自定义Header就完事因为后端允许任何源“读取并携带”。这两个概念不掰扯清楚防御就会漏成筛子。5. 2026年实战组合拳从后端到前端的完整落地方案5.1 三层防线我来给你盘一盘真正可靠的CSRF防御应该是分层防守的。下面这套方案我在多个项目里落地过可以当清单用。第一层是浏览器侧把会话Cookie的SameSite设为Lax或Strict这是成本最低、覆盖面最广的一道防线。大部分老代码只要改属性就有效果没必要升级框架。第二层是服务端侧校验Origin和Referer非信任来源直接拒绝。同时对所有写操作接口强制要求CSRF Token。第三层是前端侧请求拦截器自动加Token、敏感操作只用POST、不让机密信息出现在URL上以及在前端构建期做好依赖安全检查避免引入带风险的第三方脚本。你注意到没有这三层不是“三选一”而是“三合一”。每一层都有自己的盲区浏览器侧管不住同站请求服务端侧拦不住合法来源内的攻击者前端侧防不了服务端不做校验的裸奔。只有叠起来才能覆盖到绝大多数攻击面。5.2 一个后端校验的参考伪代码下面给一个服务端校验的伪代码方便你理解整个链路长什么样也方便你拿去和后端同学对齐。import re from flask import request, abort TRUSTED_ORIGINS {https://app.example.com, https://admin.example.com} def csrf_protect(): # 1. 安全方法直接放行GET/HEAD/OPTIONS/TRACE if request.method in (GET, HEAD, OPTIONS, TRACE): return # 2. 优先校验 Origin origin request.headers.get(Origin) if origin: if origin not in TRUSTED_ORIGINS: abort(403, Untrusted Origin) return # 3. Origin缺失时回退校验Referer referer request.headers.get(Referer) if referer: # 只校验站点根防止路径伪造 m re.match(r^https://([^/])/, referer) if not m or fhttps://{m.group(1)} not in TRUSTED_ORIGINS: abort(403, Untrusted Referer) return # 4. 两个头都缺失保守校验Token token request.headers.get(X-CSRF-Token) cookie_token request.cookies.get(csrf_token) if not token or token ! cookie_token: abort(403, CSRF Token Missing or Mismatch)这段代码的思路是“能校验来源就校验来源来源可信则Token可以从宽来源不可信直接打回”。有些团队觉得麻烦我建议直接一点只要是非安全方法一律同时校验来源和Token。多一次校验的耗时几乎可以忽略但安全性上限高很多。5.3 登录、登出这些特殊接口别漏了CSRF防御最容易漏的不是主业务接口而是那些“边缘接口”——登录、登出、验证码获取、头像上传。攻击者篡改这些接口虽然不一定能直接偷数据但搞出“账号锁定”“服务异常”是够用的。登录接口的CSRF有个特殊点用户此时可能还没有登录态那Token从哪来常见做法是服务端在登录页面渲染时输出一个一次性Token前端带着它去提交登录。如果用户没有先“拿到Token的资格”登录请求就没有Token可带——此时靠SameSiteLax来兜底。登出接口同理CSRF登出攻击防不好用户会莫名其妙掉线体验极差。上传接口是CSRF重灾区因为它天然接受POST且可能有副作用大的操作。文件上传加上Token校验还不够最好再做一次内容校验校验文件类型、大小、后缀以及文件是否真的被前端解析过。CSRF只是入口上传后的存储型XSS才是大BOSS一前一后要一起防。5.4 前端工程化把CSRF防御写进基建而不是写进个人习惯防御如果只靠每个人写代码时“注意一下”那等于没有防御。2026年的前端工程化应该把这套机制沉淀到基建里。我比较推荐的做法是封装一个统一的request模块在模块内部完成CSRF Token的注入。这样业务代码只用request.get()/request.post()不需要关心Token从哪来。团队里的新同学即使不懂CSRF也不会踩坑。代码评审时只用看业务逻辑什么跨域、拦截器都是透明的。另一个关键点是把Cookie名和Header名做成常量前后端通过接口文档对齐。这里有个非常容易翻车的细节名字稍微对不上比如前端发X-CSRF-TOKEN后端校验的是X-CSRF-Token请求直接被403但你在页面上肉眼看不出来只能看网络面板。所以我在项目里统一约定所有和CSRF相关的标识都用小写连字符形式文档里标注清楚。6. 实战排查CSRF相关的疑难杂症速查表6.1 线上出问题怎么快速定位是不是CSRF被拦你最终逃不过一个问题上线后部分接口突然大量403。这时候先别急着把CSRF校验关掉按下面的顺序排查。先看请求是不是非安全方法POST/PUT/DELETE如果是GET被拦截大概率是拦截器把SAFE_METHODS判错了检查config.method的大小写。再看前端有没有成功带上Token打开DevTools的Network面板点击那个403的请求找到Request Headers看有没有X-CSRF-Token。如果没有问题在拦截器如果有但403问题在后端比对逻辑如果后端比对发现两次读取的Cookie不一样多半是路径或域的问题。Cookie的域和路径是另一个重灾区。Token写入时如果用的是path/api而页面在/admin下前端就读不到。我建议Token Cookie统一用path/并设置SameSiteLax。跨域场景千万别用Domain前缀否则子域全都能读到等于给攻击者发钥匙。6.2 常见问题与解决方案速查表现象可能原因解决方案所有POST请求都403后端没正确对比Token或前端拦截器没生效打开Network面板确认请求头是否有Token无则查拦截器有则查后端代码部分用户请求被拦部分正常Cookie路径不一致或SameSite设置不一致统一Token Cookie的路径为/域不设Domain跨域正常同域反而不行拦截器对同源请求判断逻辑出错检查new URL()拼接逻辑确认targetUrl.origin和window.location.origin完全一致服务端渲染页面提交表单403隐藏域的名字和后端校验名不一致确认namecsrf_token和后端读取的字段名完全一致第三方登录跳转回来403跳转过程中Cookie没带上或者Token被重置确认跳转链接、回调地址都在信任名单内Token在此场景建议每次请求重新生成刷新页面后Token失效Token只在内存里或Cookie过期把Token持久化到Cookie并设置合理过期时间或刷新后重新拉取6.3 一个真实排障案例Token时灵时不灵有一次项目联调测试同学反馈“时好时坏过一会儿又403”。我打开Network面板一看发现请求头里Token一会儿有、一会儿没有。继续查才发现前端在某个页面里用了document.cookie手动追加了一个同名Cookie而我们的拦截器用的是js-cookie的get它是按枚举顺序返回的——手动追加的Cookie在后面拦截器读到的正是那个冒牌货。这种“同名覆盖但读取顺序不一致”的坑非常隐蔽。我的建议是项目里统一用一套Cookie管理方式不要混用原生API和第三方库。如果你从拦截器里取Token就用js-cookie如果你在服务端渲染模板里注入了Cookie确保跟前端代码用的是同一个逻辑读取。Cookie这玩意儿虽然老旧但它带来的坑永远不会过时。还有一个常见坑Token在响应里每次更新但前端没有把新Token同步到Cookie。比如后端在登录后返回一个Token前端存到了localStorage但请求拦截器读的却是Cookie——于是第一次请求成功第二次Token过期就403。这种现象看起来像“随机失败”实际是前后端没对齐Token的存储与更新策略。后来我把规则改成“以后端写入Cookie为准前端只负责读和塞”问题彻底消失。6.4 一些容易“顺手关掉防御”的时刻我特别想提醒一件事排障时千万别为了临时过个接口就把CSRF校验删了。开发环境可以理解但线上千万别这么干。如果你实在要快速验一个接口通不通与其禁用校验不如在本地临时构造一个合法请求或者用Postman带好Token去调。真要在线上关校验必须有审批、有记录、有时限我见过不止一次“临时关一下”变成“永久裸奔”的。还有一类容易翻车的操作用了第三方SDK比如监控上报、埋点、客服聊天插件后这些SDK发请求时不会走你自己的拦截器。如果它们恰好访问了你后端的写接口Token就带不上了。这种情况要么把这些SDK的请求放到白名单里单独处理要么确保它们走的域名和你主站的CSRF校验规则无关。记住你做的防御只保护你自己写的代码路径第三方代码全在你控制之外。7. 最后说一点我在实践中越用越深的体会CSRF防御这件事做得越久越会发现它不单是“加个Token”的局部问题而是整个系统信任模型的设计问题。你信任了什么、不信任什么都会反映在接口设计上。前端能做的是让每一次请求都带着“我确实来自可信前端”的证据而后端要做的是不放过任何一个可疑来源——这句话说起来简单做到位要靠一套覆盖开发、测试、线上监控的完整机制。我个人现在的习惯是每次新项目启动时先把安全和权限相关的设计文档写清楚哪些接口需要CSRF Token、哪些接口需要额外的来源校验、Cookie策略是什么、CORS白名单是什么。写文档这事看着耽误时间但等出事故再救火那才是真的耽误时间。前端老鸟的经验就四个字提前想好。如果你现在正在维护老项目建议从今天起做三件事第一把所有写操作的接口过一遍确认它们都用了POST类方法第二确认线上所有Cookie都设置了SameSite核心Cookie设置成Lax或Strict第三找后端同事或者自己看一眼代码确认服务端对Origin/Referer有校验逻辑。这三件事做完你至少脱离了“裸奔”级别。剩下的再慢慢用Token体系和工程化把防护拉满。CSRF不是一道多难的题它就是一场持续的、和攻击者抢时间、抢认知的攻防战。你每多设一道防线攻击者的成本就高一分。把基础打牢了不管2026年还是2030年你都不用在凌晨三点爬起来处理漏洞告警——那种背锅的感觉希望我这篇文章能帮你彻底避开。