先说一个我印象挺深的场景某团队做前后端分离项目后端把会话Cookie加上HttpOnly之后前端同事当天就找过来说登录接口返回的Set-Cookie在浏览器里能看到但JavaScript里document.cookie一片空白下一步没法做。于是有人建议把HttpOnly去掉因为“反正现在有CSRF Token了Cookie藏着也没啥用”。这个建议对不对对了一半但把差点把CSRF防线也给拆了。后来我们花了不少时间把“HttpOnly到底防什么”“CSRF到底怎么防”“Cookie藏起来之后前端怎么配合后端做校验”这三件事彻底理顺前后端通信才不再扯皮。这个标题里的“设置HTTP-only标志防御CSRF攻击”本身是个常见但容易误读的表述。HttpOnly不是直接防御CSRF的武器它真正的战场是XSS攻击——防止注入脚本偷走会话凭证。但它确实改变了前后端的通信方式Cookie里的东西前端读不到了那CSRF Token怎么传、怎么存、怎么带上请求就成了一个绕不开的设计问题。这篇就把这条链路从头到尾拆一遍从攻击原理到方案选型再到能直接落地的代码和排查经验一次讲清楚。1. CSRF攻击的本质浏览器替你“背锅”的全过程1.1 一次典型的CSRF攻击是怎么发生的CSRF跨站请求伪造说起来不复杂受害者已经登录了目标站点A浏览器里存着A的会话Cookie这时候受害者被诱导打开了攻击者的站点B。站点B里藏着一个自动提交的表单或者一个带参数的图片请求目标是指向A站点的一个敏感接口比如改密码、转账、发消息。攻击流程里最关键的一点是浏览器在发这个请求时会自动带上属于A站点的Cookie。后端一看这个请求带着我的会话Cookie认证没问题就正常处理了。但它没想过一件事——这个请求到底是不是用户在A站点页面上主动发起的用户此时根本不知情。他只是在看B站点的页面结果地址栏里对A站点的请求已经发出去了Cookie自动帮忙“验明正身”后端也就放心执行了操作。这就是CSRF能成立的根本原因服务端只相信Cookie代表的身份却无法区分请求是用户真实意愿还是被第三方站点诱导发出的。1.2 为什么浏览器会“主动”带上Cookie这里需要把Cookie的携带规则说清楚。Cookie在设置的时候就已经带好了作用域包括Domain域名和Path路径有些还带SameSite属性。浏览器在发起请求时只要当前请求的目标URL满足Cookie的Domain、Path和协议约束就会无条件把Cookie放进请求头不需要页面里的JavaScript做任何事也不会询问页面是谁打开的。这本来是一个很顺手的设计——用户登录后刷新页面、跳转页面、请求API都不需要前端手动维护登录状态Cookie自己会跟着请求走。但同一套机制也带来CSRF的隐患攻击者站点构造的请求目标域名是A浏览器依然按照A的Cookie作用域把Cookie带上。也就是说CSRF不是攻击者“拿到了”你的Cookie而是浏览器“主动替”攻击者把Cookie发送了出去。Cookie本身没有泄漏给攻击者但攻击者已经不需要Cookie了他要的只是用户浏览器“附赠”的这个认证凭据。这也是很多新手困惑的地方我的Cookie加了HttpOnly攻击者看不到为什么还会被CSRF打答案很简单——CSRF从头到尾就不需要“看到”Cookie它只需要浏览器在请求里自动带上Cookie就够了。HttpOnly防的是JavaScript读取Cookie而CSRF利用的是HTTP请求头自动附带Cookie的机制两者根本不在同一个环节上。1.3 CSRF的核心危害与触发条件CSRF的危害取决于被攻击接口的“敏感度”。如果目标站点有一个修改手机号的接口攻击者可以诱导用户在不知情的情况下把账号手机号改成攻击者的然后通过手机验证码完成密码重置账号就彻底易主了。还有一类典型的CSRF发生在管理后台攻击者诱导管理员访问恶意页面后台的“添加管理员”或者“导出数据”接口被自动触发后果更严重。一个CSRF攻击要成立通常满足三个条件目标接口使用Cookie作为认证凭据且没有额外的来源校验目标接口只检查请求是否已认证不检查请求来源是否可信攻击者能预测请求的参数结构或者通过固定的GET/POST表单就能触发的接口。所以你会注意到凡是用了Authorization Header传Token的纯API接口天然没有CSRF问题——因为攻击者的站点B没办法让浏览器自动给请求加上目标站点的Authorization头这个头只有前端代码能加攻击者没这个权限。1.4 了解攻击链路之后防御思路就清晰了知道CSRF的本质之后防御方向就非常明确了重点不是你多想一个“认证之外”的前端校验而是要让服务器能识别“这个请求是不是用户本人在目标站点页面里主动发起的”。现在业界常用的手段有几个方向CSRF Token同步令牌模式、双重Cookie提交、SameSite Cookie限制、Origin/Referer来源校验、自定义Header配合CORS策略。不同方案解决的是不同层面的问题但核心逻辑是一致的——“仅仅带了Cookie还不够你还得拿出另一个只有目标站点页面才能拿到的凭据或者证明请求来自可信任的站点上下文”。到这里HttpOnly的问题就浮出水面了如果CSRF Token要靠前端JavaScript配合去读取和注入而HttpOnly又把所有Cookie都藏了起来前后端就得想出一套新的“沟通语言”。2. HTTP-only的真实战线它到底防住了什么2.1 HTTP-only的作用原理HttpOnly是Set-Cookie响应头里的一个标志一旦设置浏览器就会禁止JavaScript通过document.cookie访问这个Cookie。它不会影响浏览器在HTTP请求里自动附带的逻辑所以对服务端来说每次请求依然能正常收到这个Cookie认证完全不受影响。举个例子服务端返回一个Set-Cookie头Set-Cookie: SESSION9f8e7d6c5b4a3210; Path/; HttpOnly; SameSiteLax; Secure这个SESSION Cookie会正常存到浏览器里之后每次向目标域名发请求浏览器自动带上但页面里的任何脚本执行document.cookie时只能看到其它没加HttpOnly的Cookie看不到SESSION。恶意脚本偷偷把document.cookie发回攻击者服务器这个操作也拿不到SESSION的原始值。2.2 和XSS的关系两者经常被一起提但攻击路径完全不同XSS跨站脚本攻击是攻击者把恶意脚本注入到目标站点的页面里脚本在目标站点的上下文中执行。这时候脚本能干的事非常多读取cookie、篡改页面、模拟用户操作、向目标站点内网发请求等等。如果会话Cookie没有HttpOnlyXSS脚本直接document.cookie拿会话ID发给攻击者服务器攻击者就能在远端伪造这个会话实现“盗号”。而CSRF是攻击者通过自己的站点B诱导用户浏览器向目标站点A发请求利用的是“Cookie自动附带”的机制而不是直接拿Cookie内容。两者一对比就特别清楚攻击类型利用对象攻击者需要拿到Cookie吗核心防护XSS页面脚本执行环境需要拿到会话靠它盗号HttpOnly、CSP、输出编码CSRF浏览器Cookie自动携带机制不需要Cookie只是附带品CSRF Token、SameSite、来源校验所以HttpOnly和CSRF的关系更像“间接战友”——它虽然防不住CSRF但它保证了CSRF防御方案里的会话凭证不会被XSS顺手牵羊。如果会话Cookie能被脚本轻易读走攻击者直接盗号根本不需要费劲搞CSRF了。这也是为什么“设置HttpOnly防御CSRF”这个说法虽然不严谨但在实践中经常被放在一起讲它确实提升了整体会话安全级别让CSRF防御有了可靠的前提。2.3 只靠HttpOnly的常见误解我见过不少人以为只要设了HttpOnly就万事大吉结果被CSRF打了之后还很委屈“我Cookie都藏起来了攻击者怎么还能利用”其实只要目标接口接收Cookie认证并且没有其它校验HttpOnly完全无法阻止那个“附带Cookie的请求”被自动发出。比如用curl模拟一个不带Token的POST请求带上受害者的Cookie服务端照样正常处理跟页面里有没有HttpOnly毫无关系。把HttpOnly当成CSRF的“免死金牌”是误区但反过来觉得“有了CSRF Token就不需要HttpOnly”也同样危险。CSRF Token解决了跨站伪造问题但解决不了XSS偷会话的问题。Token是随机值被盗走之后攻击者可以正常通过校验会话Cookie里藏着用户的认证身份一旦被盗攻击者全盘接管。所以成熟的做法是HttpOnly保护会话Cookie防止XSS盗号CSRF Token或SameSite保护请求本身防止跨站伪造。两条防线各管各的缺一不可。3. 防御CSRF的体系化设计从单点防护到纵深防御3.1 主流防线逐一拆解CSRF防御不是选一个方案就完事而是要考虑多个维度的组合。目前应用最广的方案大致是下面这些第一种是同步令牌模式Synchronizer Token Pattern。服务端生成一个随机Token存到Session或服务端存储里页面前的表单或请求带上这个Token。提交时服务端比对请求里的Token和Session里存的是否一致。因为是随机、不可预测的攻击者无法在恶意页面里预先构造这个Token所以能有效防止跨站伪造。第二种是双重Cookie提交Double Submit Cookie。服务端把Token写入一个Cookie另外要求请求参数或请求头里带上同一个Token值。服务端校验时从Cookie里取出Token和请求里的Token对比。攻击者因浏览器同源策略无法读取目标站点的Cookie除非目标站点的子域被攻破或者存在特殊漏洞也就无法在恶意请求里构造出相同值。第三种是SameSite Cookie属性。现代浏览器支持Cookie的SameSite属性我们可以在Set-Cookie时设为Lax或Strict。Lax模式下跨站请求中只有顶级导航的GET请求会带CookiePOST、iframe、fetch等都不会带这已经能挡住绝大多数的CSRF攻击。Strict更严格所有跨站请求都不带Cookie但用户体验上会有一些代价比如从第三方站点跳转回来时登录态可能丢失。第四种是来源校验检查请求的Origin头或Referer头是否属于可信站点。缺点也比较明显Referer在某些场景下会被浏览器省略比如HTTPS页面跳到HTTP页面时不会发送Referer太依赖这个就容易被误伤。第五种是自定义Header配合CORS策略。要求所有敏感请求必须带一个自定义Header比如X-Requested-With或者自定义的X-CSRF-Token。因为自定义Header会触发CORS预检而跨站表单提交无法添加自定义Header所以攻击者的恶意请求根本通不过服务端校验。这个方案通常会和别的方案配合使用而不是单独扛大旗。3.2 纵深防御的推荐组合现在的Web应用环境里我最推荐的是“SameSite CSRF Token”的双层组合再加上HttpOnly保护会话Cookie。这样即使某一层被绕过另一层还挡着。举个例子同一套接口Cookie设置了SameSiteLaxPOST跨站请求在浏览器层面就不会带Cookie绝大多数CSRF攻击在第一道防线就被拦下了。但Lax并不是万能的某些特定的跨站场景仍然可能带Cookie比如顶级导航的GET请求或者部分老浏览器的兼容行为。这时候第二道防线——CSRF Token校验——就能兜底。Token校验不为浏览器行为做假设只看请求里有没有合法的Token所以无论攻击者通过什么方式发出请求只要没有Token就拒绝。同时会话Cookie设HttpOnly能保证这道防线不被XSS瓦解。整体防御模型就变成了“浏览器层防一部分 服务端层防一部分 客户端脚本侵袭防护”每一层负责的威胁类型不同组合在一起覆盖得更全面。3.3 核心矛盾HttpOnly让前端读不到CookieToken怎么传这里终于落到标题里最实际的问题上。在同步令牌模式中服务端把Token存在Session里前端需要拿到这个Token放进请求参数或Header里。如果服务端把Token也塞进一个HttpOnly Cookie里那么前端JavaScript从document.cookie里读不到也就没法把它放进自定义Header里。这是前后端通信最直接的一个“断点”。要打通这个断点得明确一件事CSRF Token不是会话凭证它是一次性或者周期性的跨站请求校验凭据即使被JavaScript读取也不会直接泄露用户身份。会话凭证那一份SESSION Cookie必须HttpOnly保护好但CSRF Token可以走一条专门给前端读的通道。这个思路落实下来就出现了下面几种主流的前后端通信设计。4. 前后端通信的几个关键设计Token下发、存储、注入4.1 登录接口一次性把会话和Token都确认好前后端通信的第一步是登录。用户提交用户名密码后服务端校验成功接下来要做两件事设置HttpOnly会话Cookie同时把CSRF Token交给前端。比较简洁的做法是在登录接口的响应头里同时设置两个Cookie一个是SESSION的HttpOnly Cookie一个是XSRF-TOKEN的普通Cookie。这里故意让XSRF-TOKEN不设HttpOnly因为它就是要给前端脚本读取的。前端登录之后从document.cookie里读XSRF-TOKEN的值存进请求库的默认Header里后续请求自动带上。还有一种做法是登录接口的响应体里直接返回一个JSON字段比如{ csrfToken: xxx }前端拿到之后放内存变量或JavaScript模块里每次发请求时手动或者通过拦截器填进Header。这么做的好处是Cookie里不存任何前端需要读取的东西Cookie策略更干净缺点是刷新页面后Token就丢了前端需要重新调一个接口去拿或者接受刷新后重新获取Token的流程。4.2 前端如何自动带上Token请求拦截器是主力不管是Cookie方案还是接口返回方案前端最终都要把Token放到自定义Header里。这里几乎都是用统一的请求封装来做的。以axios为例默认配置里就支持从Cookie读取CSRF Token并自动注入Header。如果你用Angular内置的HttpClient也有类似逻辑。import axios from axios; axios.defaults.xsrfCookieName XSRF-TOKEN; axios.defaults.xsrfHeaderName X-XSRF-TOKEN; axios.defaults.withCredentials true; axios.interceptors.request.use(function (config) { // 从Cookie里读XSRF-TOKEN const token readCookie(XSRF-TOKEN); if (token) { config.headers[X-XSRF-TOKEN] token; } return config; });这样每一次API请求都会自动带上X-XSRF-TOKEN。如果Token不在Cookie里而是登录接口返回的那就换成把返回值赋给一个全局变量拦截器里读这个变量。需要注意的问题是Token的时效性服务端如果把Token设成短时效前端在Token过期后必须能感知到并且重新获取。这个环节通常和401处理逻辑绑在一起。4.3 跨域场景下的通信“暗坑”预检请求前后端分离项目只要跨域并且请求里自定义了Header浏览器就会先发一个OPTIONS预检请求询问服务端允许哪些Header、哪些方法、是否允许携带凭证。如果服务端CORS配置不完整前端请求直接失败在预检阶段看起来就像接口不可用。这套机制本身是浏览器在帮你做安全决策跨域请求如果带自定义Header必须经过服务端明确“点头”才能放行。而攻击者从恶意站点发起的简单表单请求压根不会走预检流程服务端一旦要求必须有自定义Header这种请求就会被直接拒绝。也就是说自定义Header不仅让前端能传Token它本身也是防御链路上的一环。服务端要正确响应OPTIONS并且配置Access-Control-Allow-Headers包含X-XSRF-TOKENAccess-Control-Allow-Credentials为trueAccess-Control-Allow-Origin明确指定可信来源不能是星号——因为带Cookie凭证的跨域请求不允许星号通配。这一条容易被人忽略后面常见问题里还会专门讲。4.4 几种通信方案的选择对比把这几种方案放在一起看使用场景和取舍就清晰多了方案会话Cookie是否HttpOnlyCSRF Token如何传给前端推荐场景需要注意的问题独立XSRF-TOKEN Cookie是前端从非HttpOnly的Cookie读取并写入Header同源部署、前后端页面由同一服务或同域网关托管需要保证Token是安全随机数Cookie作用域要收窄登录接口返回Token是登录响应体返回前端存内存并写入Header前后端完全分离、接口经常被跨域调用页面刷新后Token丢失需要额外的Token获取接口Authorization Bearer不依赖Cookie登录返回JWT等Token前端存内存或存储纯API、移动端和Web复用同一套需要防XSS盗Token建议配CSP和短时效Token方案一和方案二是传统Cookie会话体系下的选择方案三是彻底绕开Cookie的“无会话”风格。选型时主要看两点服务端还有没有存Session以及前端对Token存续期的容忍度。如果服务端还在用Session存登录态那方案一和方案二更顺手如果服务端已经JWT完全无状态化那就别硬套Cookie体系直接用方案三更干净。5. 从“看懂”到“能跑通”一套可直接落地的实现示例5.1 后端生成Token、下发Cookie、校验Header先看后端。为了让问题本身更清晰这里用一个最朴素的Servlet Filter写法说明核心逻辑不依赖具体安全框架的封装。核心要做三件事生成Token并写入Cookie、对非安全请求校验Token、对安全请求放行。public class CsrfFilter implements jakarta.servlet.Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; // 首次访问时生成CSRF Token并写入Cookie String token getCookie(req, XSRF-TOKEN); if (token null) { token generateSecureToken(); resp.addHeader(Set-Cookie, XSRF-TOKEN token ; Path/; SameSiteLax); } // GET、HEAD、OPTIONS等安全方法不校验直接放行 if (GET.equals(req.getMethod()) || HEAD.equals(req.getMethod()) || OPTIONS.equals(req.getMethod())) { chain.doFilter(request, response); return; } // 从请求头里拿Token与Cookie里的值比对 String headerToken req.getHeader(X-XSRF-TOKEN); if (headerToken null || !constantTimeEquals(headerToken, token)) { resp.sendError(403, CSRF token missing or mismatch); return; } chain.doFilter(request, response); } }这里最关键的细节是constantTimeEquals。直接使用equals比较字符串理论上存在时间侧信道风险攻击者可以通过响应时间逐字符猜测Token。正确做法是先把两个字符串转成字节数组然后按固定长度做异或累加最后判断结果是否为0。Token本身用SecureRandom生成它和普通随机数相比安全性更高IOW它产生的值是不可预测的这是Token有效的根本保障。如果项目已经用了Spring Security那实现更简单框架内置了CookieCsrfTokenRepositoryhttp .csrf(csrf - csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ) .authorizeHttpRequests(auth - auth .requestMatchers(/api/login).permitAll() .anyRequest().authenticated() );withHttpOnlyFalse这个方法的命名其实是反直觉的是把CSRF Token所在的Cookie设置成非HttpOnly这样前端脚本才能读。注意它只影响CSRF Token那个Cookie不影响你的会话Cookie——会话Cookie的HttpOnly需要自己配置。5.2 前端登录、取Token、带Token发请求前端的完整流程分三步。第一步提交登录请求拿到会话Cookie和XSRF-TOKEN Cookie第二步从Cookie里读取XSRF-TOKEN第三步在后续请求的Header里带上它。用axios实现最简洁的方式是这样// 第一步登录 async function login(username, password) { await axios.post(/api/login, { username, password }, { withCredentials: true }); } // 第二步登录成功后读取Token并设置为默认Header async function initCsrfToken() { const token readCookie(XSRF-TOKEN); if (token) { axios.defaults.headers.common[X-XSRF-TOKEN] token; } } // 第三步之后的业务请求 async function changePassword(oldPwd, newPwd) { // 这个请求会自动带上X-XSRF-TOKEN return axios.post(/api/change-password, { oldPwd, newPwd }); }readCookie这个函数不用引入库直接操作document.cookie就可以。但要注意一个细节如果Token是通过登录接口响应体返回的而不是Cookie那上面的读取逻辑就要改一下登录接口的返回值先存到一个模块级变量里拦截器从这个变量取。5.3 接口级校验不是所有接口都需要CSRF Token另外一个容易被忽视的点不是所有请求都需要CSRF防护。只读请求、公开接口、预检请求虽然不带Token但也不能被误伤。以查询类接口为例它本身不修改数据CSRF攻击即使伪造了GET请求最多读到一些数据如果接口设计得不好还可能造成信息泄露但这与CSRF防护是另一个问题。因此后端的校验规则通常是GET、HEAD、OPTIONS放行POST、PUT、PATCH、DELETE等变更请求必须校验Token。个别公开的POST接口比如注册、找回密码这些接口在用户尚未登录时就应该允许无Token访问不过如果攻击者可以在未登录状态下提交这些接口需要考虑的是接口的滥用防护限流、验证码而不是CSRF。设计CSRF校验范围时建议按接口分组维护白名单做到变更接口默认开启校验特殊接口显式声明豁免。5.4 完整的请求链路串一遍把上面的逻辑串起来看一次正常的登录后请求是这样的浏览器访问登录页服务端发现没有XSRF-TOKEN Cookie生成一个安全随机数写入Set-Cookie响应。用户提交登录表单前端发起POST请求如果这个POST发生在登录前Token还没被前端读取到那请求就带不上X-XSRF-TOKEN头。这个矛盾怎么解实践里通常把登录接口本身从CSRF校验白名单剔除或者让前端在加载页面时先调一个GET接口触发Token生成。登录成功后前端拿到XSRF-TOKEN Cookie的值设置到axios默认Header。用户后续发起修改类请求axios自动带上X-XSRF-TOKEN头Cookie也由浏览器自动带上。服务端从Header取出Token和Cookie里的Token做常量时间比较一致则放行。如果请求里没有Header服务端直接403攻击者的跨站伪造请求在这里被拦截。这条链路里HttpOnly会话Cookie完全不需要前端参与浏览器自动处理前端参与的只是XSRF-TOKEN的读取与注入。而且攻击者的恶意请求虽然会自动携带Cookie却带不上X-XSRF-TOKEN头浏览器不会帮它从Cookie里取Token所以会在第6步被拦下。6. 实战中反复踩过的坑与排查思路6.1 设置HttpOnly后前端读不到Token这个是最早暴露的问题。有人顺手把所有Cookie都加了HttpOnly包括XSRF-TOKEN结果前端document.cookie里干干净净请求Header里永远没有Token后端一直403。排查半天才反应过来CSRF Token Cookie不能加HttpOnly——它的存在价值就是让前端读取。这不是说要让会话Cookie裸奔而是要把“会话Cookie”和“CSRF Token Cookie”分开看。会话Cookie加HttpOnly是刚需CSRF Token Cookie不加HttpOnly是功能需要。如果对Cookie数量比较敏感不想在Cookie里多暴露任何值那就改成登录接口返回Token的方案让Cookie里始终只有会话Cookie一个敏感值。6.2 跨域配置不完整导致请求“神秘失败”前后端分离项目前端跑在一台开发服务器后端跑在另一台请求经常在Network面板里连着刷出两条第一条是OPTIONS第二条才是真正的POST。如果OPTIONS返回403或者根本没有响应第二条请求根本不会发出。排查这类问题先看后端有没有给OPTIONS请求放行再看Access-Control-Allow-Headers里有没有包含X-XSRF-TOKEN还要确认Access-Control-Allow-Credentials是true。有一个很容易漏的配置前端如果设置了withCredentials后端就不能用Access-Control-Allow-Origin: *必须明确写客户端来源域名否则浏览器会拒绝。用curl没法模拟完整的预检流程但可以用一个快捷的检测方法——直接在浏览器开发者工具的Network里看预检请求的响应头逐项核对后端返回的CORS头是否齐全。6.3 页面刷新后Token丢失登录接口返回体里带Token的方案前端刷新页面后Token跟着内存一起被清了后续第一个变更请求就没有Header直接被403。解决方法是提供一个GET /api/csrf接口后端动态生成Token返回给前端前端在应用初始化时调用一次。或者干脆回到Cookie方案Cookie天然跨刷新存活前端每次加载时都从Cookie里读Token。如果你选择了接口返回Token就要特别注意多标签页的问题两个标签页同时打开时每个标签页的Token存在各自的JS内存里互不相同。而后端无论用Session存储还是双重Cookie校验都会出现一个标签页的Token被另一个标签页覆盖导致前一个标签页的请求全部403。Cookie方案在这种场景下明显更稳。6.4 SameSiteLax对第三方登录回调的影响SameSiteLax严格限制了跨站带Cookie这本来是为了安全但在某些真实业务场景里会给用户添麻烦。最典型的是第三方授权登录用户从第三方登录页跳转回本站这个跳转是顶级导航GET按Lax规则会带本站Cookie基本没问题。但如果业务里用了iframe嵌入或者需要跨站Set-Cookie的场景Lax可能把Cookie挡掉导致登录态丢失。遇到类似情况不建议直接退到SameSiteNone因为这需要配合Secure标志并且会削弱对CSRF的浏览器层防护。先检查产品层面能否换成跳出iframe或者改成服务端中转的流程只有在确实需要跨站携带Cookie时才在受限范围内使用SameSiteNone。当前的浏览器环境里第三方Cookie本身也在逐步被收紧长远看更推荐改造认证流程而不是对抗浏览器的策略。6.5 多实例部署时Session不一致导致Token校验失败同步令牌模式下Token存在Session里后端如果部署多个实例用户第一次请求命中了实例AToken存到了A的Session里下一个请求被负载均衡转发到实例BB的Session里没有这个Token校验就失败。项目一上集群这种问题就冒出来了。两个常见的解法。一个是把Session抽到Redis等集中式存储里让所有实例共享同一份Session数据。另一个是放弃Session存储Token用双重Cookie方案Token存在Cookie里服务端只是校验“请求头上的Token”和“Cookie中的Token”是否一致不依赖任何服务端存储。如果实例配置了相同的Cookie校验密钥还能对Token做签名进一步增强安全性。6.6 快速自测CSRF防护是否生效最后给一个我常用的验证方法。用curl模拟一个“带Cookie但无Token”的请求curl -i -X POST https://your-site.com/api/change-password \ -H Cookie: SESSIONxxx; XSRF-TOKENyyy \ -d oldPwd123456newPwdabcdef如果返回403说明CSRF校验生效了。再手动加一个X-XSRF-TOKEN头并且值和Cookie里的XSRF-TOKEN一致重新发一次如果接口正常放行说明前后端通信链路完整。这两条curl一打整套CSRF防线是否通顺就一目了然。别忘了SESSION的值要用真实登录后的值去替换否则会被认证环节拦掉测不到CSRF这一层。最后说点我自己的体会这些年处理过的CSRF问题大部分都不是方案本身不行而是前后端对“Cookie能做什么、不能做什么”的预期不一致。HttpOnly把会话Cookie藏起来之后前端很容易陷入“我什么都读不到怎么做登录判断、怎么带Token”的恐慌然后有人就想着把HttpOnly去掉或者把所有东西都塞进请求里反而把安全边界搞模糊了。我个人经验是在设计前后端接口规范时就明确约定三条会话Cookie一律HttpOnly且前端不得读取CSRF Token必须有专门的下发通道所有变更类接口默认启用Token校验。把这三条写成接口文档前端照着做后端照着验之后基本不会因为CSRF的问题来回扯皮。至于选择Cookie方案还是接口返回Token方案我的建议是同源部署优先Cookie方案跨域部署优先接口返回方案并且不管哪种别忘记SameSite和CORS这两道容易出幺蛾子的配置。安全和便利从来都是一个平衡想清楚了再动手比写代码本身更重要。