Spring Security CSRF攻防实战:从原理到前后端分离落地

发布时间:2026/10/22 20:18:57
Spring Security CSRF攻防实战:从原理到前后端分离落地 做Java后端的朋友应该都有过这种经历临近上线安全扫描报告把CSRF标成中危漏洞然后被要求“把Spring Security的CSRF防护打开”。但很多人对CSRF的理解就停在“配一个过滤器”的层面至于漏洞为什么存在、攻击请求是怎么打到服务端的、前后端分离项目到底怎么配合很多文章要么只讲理论要么只贴配置真正能让人从头到尾跑一遍攻防流程的少之又少。这一篇是Spring Security实战系列的第十一篇我把标题里的“CSRF攻防实战”完整展开先从攻击者视角复现一次真实的CSRF攻击再顺着Spring Security的底层校验逻辑看它到底在防什么最后结合实际项目给出前后端分离场景下可直接落地的配置方案。无论你是刚接触Spring Security的初学者还是在维护老系统的开发者这篇都能让你以后再面对CSRF时不是“照着配一遍”而是真正理解它遇到问题能自己排查。1. 从“被改密码”开始CSRF到底是怎么发生的1.1 浏览器自动带Cookie不是Bug而是功能CSRF的根子在浏览器的Cookie机制上。很多新手有个误解觉得“跨站请求不能带Cookie带了就是同源策略没生效”这个理解是反的。同源策略限制的是页面里的JavaScript读取另一个源的数据但它不限制浏览器在发送请求时自动附加Cookie——这是浏览器最基础的功能用户登录之后后续每个HTTP请求都应该带上身份凭证否则登录就白做了。打个比方你在公司办了一张门禁卡保安看到卡就放行。CSRF就是有人做了个网页让你无意中走到公司门口门禁卡自动感应门就开了但你想去的并不是这扇门后面的房间。浏览器那个自动携带Cookie的动作就是这张门禁卡的自动感应。所以CSRF攻击能成立本质上不是浏览器有漏洞而是服务端没能区分“用户主动发起的请求”和“浏览器替用户自动发出的请求”。攻击者不需要拿到你的Cookie也不需要破解密码他只负责构造一个会自动触发请求的页面剩下的脏活浏览器全干了。1.2 一次典型攻击的三步流程拆解假设用户已经登录了某个电商系统网址是shop.example.com登录状态保存在Session Cookie里。攻击者的目标是在用户不知情的情况下把用户的收货地址改成自己的地址。攻击流程分三步。第一步用户登录了电商系统浏览器里留存着有效的Session Cookie。这个状态别人看不到但浏览器自己知道。第二步用户访问了攻击者准备好的恶意页面可能是邮箱里的一封HTML邮件、某个论坛帖子里的外链图片也可能是一个带诱导性的广告页面。这个页面里不放明显恶意的内容只放一个隐藏的表单。第三步恶意页面在加载时通过JavaScript自动提交表单表单的action指向电商系统的“修改收货地址”接口。浏览器发出这个跨站请求时会自动把电商系统域名的Cookie一并带上。服务端看到请求带着合法Session认为就是用户本人操作于是地址被改掉了。用户全程什么都没点甚至可能都不知道自己访问过那个页面。这就是CSRF让人防不胜防的地方。下面是一段典型的恶意页面代码只有几行HTML和JavaScripthtml body iframe nameframe styledisplay:none/iframe form actionhttps://shop.example.com/account/address methodPOST targetframe input typehidden namereceiver value攻击者 input typehidden namephone value13800000000 input typehidden nameaddress value某小区1号楼 /form script document.forms[0].submit(); /script /body /html这里用隐藏iframe接收表单提交后的响应页面用户看不到任何提交过程。如果是GET请求的接口攻击者甚至不需要表单一个img srchttps://shop.example.com/account/delete?id123标签就够了。所以在设计接口时凡是会引起数据变化的操作都不能用GET方法暴露。1.3 攻击成立的四要素与典型受害场景不是所有系统都受CSRF影响一个CSRF攻击能成功通常要同时满足四个条件。第一身份认证依赖Cookie或Session。如果服务端用的是JWT且保存在localStorage请求头里的Token需要JavaScript手动添加跨站页面根本读不到localStorageCSRF就自动失效了。这也是很多纯REST API项目选择无状态JWT的原因之一。第二目标接口有副作用。查商品列表、搜订单这种只读操作就算被攻击也不会造成什么损失但改密码、转账、删数据、发文章这类“写操作”就是高危目标。第三请求参数可以被攻击者预测。攻击者的恶意表单要预先知道接口需要提交哪些字段。如果接口需要一个动态生成的随机参数攻击就难了——但从实践看绝大多数接口字段都是固定的。第四服务端不校验请求来源。没有自定义Header校验没有Origin/Referer校验没有Token校验。我整理了一个表格方便对照自己项目里哪些接口需要重点防护业务场景攻击方式后果修改密码表单提交新密码参数账号被篡改密码原用户无法登录转账/支付表单提交收款方和金额资金被转移到攻击者账户发布内容表单提交标题正文账号被利用发布垃圾内容退出登录表单提交logout请求强制用户退出配合钓鱼攻击绑定手机/邮箱表单提交新联系方式账号绑定关系被更换2. 本地复现CSRF攻击搭建一个可运行的攻防Demo2.1 准备一个能被攻击的登录系统理解原理最快的方式是自己动手打一遍。我在本机搭建了一个极简的Spring Boot项目用Thymeleaf做服务端渲染Spring Security管登录提供一个“修改昵称”的接口完全模拟老式MVC应用。项目依赖只需要三个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency安全配置先保持Spring Security的默认行为。注意一点只要引入了Spring Security依赖CSRF防护默认就是开启的我们稍后再看开启状态下的效果。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth - auth .requestMatchers(/login).permitAll() .anyRequest().authenticated() ); http.formLogin(form - form .loginPage(/login) .defaultSuccessUrl(/account/profile) ); return http.build(); } }控制器里提供一个POST接口用于修改昵称Controller RequestMapping(/account) public class AccountController { PostMapping(/profile) public String updateProfile(RequestParam String nickname) { // 实际项目里这里会写数据库 System.out.println(nickname has been updated to: nickname); return redirect:/account/profile; } }页面文件profile.html里放一个普通的表单Thymeleaf模板引擎会自动把Spring Security生成的CSRF Token渲染成隐藏字段!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org body h1用户中心/h1 form th:action{/account/profile} methodpost input typetext namenickname placeholder新昵称/ button typesubmit保存/button /form /body /html启动项目用浏览器访问登录页输入用户名密码Spring Security默认用户名是user密码在启动日志里登录成功后能看到修改昵称的表单。打开浏览器开发者工具查看页面源码会发现表单里多了一个隐藏输入框input typehidden name_csrf value一串随机Token/这个Token就是Spring Security的防护基础。2.2 构造恶意页面一个静态HTML就够了被攻击的系统准备好了现在来扮演攻击者。我要构造一个钓鱼页面让它自动向被攻击系统提交“修改昵称”请求。这里有一个非常关键的细节。如果直接用file://协议打开攻击页面跨站提交请求时Chrome等浏览器的SameSite策略默认是Lax跨站POST请求不会携带Cookie攻击就演示不出来——但这不代表系统是安全的这只是浏览器层面的缓解。为了不受SameSite干扰我选择把攻击页面放到本机另一个端口上服务比如8081端口。注意localhost:8080和localhost:8081在SameSite定义里属于“同站”SameSite规则中端口不参与计算Cookie会正常携带但两者又不是同源恰好符合跨站攻击的模型。攻击页面的完整代码!DOCTYPE html html body iframe nameresult styledisplay:none/iframe form actionhttp://localhost:8080/account/profile methodPOST targetresult input typehidden namenickname value已被攻击者篡改/ /form script window.onload function() { document.forms[0].submit(); }; /script /body /html在8081端口起一个静态文件服务Python一条命令就行python3 -m http.server 8081准备就绪后的复现步骤是先用浏览器登录被攻击系统确保Session存在再开一个新的浏览器标签页访问http://localhost:8081/attack.html最后切回用户中心页面刷新昵称已经变成了“已被攻击者篡改”。打开浏览器开发者工具Network面板能看到一条发往http://localhost:8080/account/profile的POST请求请求头里自动带着Cookie: JSESSIONID...响应是302重定向整个攻击过程悄无声息。2.3 关闭防护做一次对比实验为了让效果更直观我把SecurityConfig里的CSRF防护临时关掉http.csrf(csrf - csrf.disable());重启项目重新走一遍上面的流程攻击成功昵称被修改。然后把csrf.disable()去掉恢复Spring Security默认的CSRF防护再次提交恶意页面浏览器地址栏会显示403 Forbidden页面。原因很简单恶意页面是攻击者构造的它根本拿不到被攻击系统在正常页面里渲染的那个_csrf隐藏字段。浏览器虽然自动带了Cookie但请求体里没有Token服务端一比对Token缺失直接拒绝。两次实验的对比结果汇总如下配置状态攻击请求结果原因CSRF防护关闭302重定向修改成功服务端只看Cookie不校验TokenCSRF防护开启默认403 Forbidden请求体缺少Token校验失败这个对比实验做完你就明白CSRF防护的核心逻辑了——它不保护Cookie而是给每个“写操作”额外绑定一个攻击者无法预测的随机Token强迫浏览器在发出请求时必须携带这个Token而攻击者的页面构造不出来。3. Spring Security的CSRF防护机制底层到底在做什么3.1 CsrfFilter的检查流程Spring Security对CSRF的防护实现集中在一个过滤器里名字叫CsrfFilter。它不是拦截所有请求而是有一套固定的处理流程我把它拆成四步。第一步加载Token。CsrfFilter会调用CsrfTokenRepository.loadToken(request)尝试从当前请求中加载已有的CSRF Token。如果Token不存在就会生成一个全新的随机Token并通过saveToken保存到指定位置同时暴露到请求属性里方便页面渲染。第二步判断是否需要校验。默认情况下只有方法为POST、PUT、PATCH、DELETE的请求才会被校验GET、HEAD、OPTIONS、TRACE请求直接放行。这样做是有道理的CSRF攻击的核心是诱导用户触发有副作用的写操作而GET请求按HTTP语义应该是安全的只读请求。第三步比对Token。需要校验的请求进入后CsrfFilter会从请求参数_csrf或者请求头中取出客户端携带的Token与第一步加载的Token做比对。Spring Security这里用的是常量时间比较避免通过响应时间差异侧信道猜测Token。第四步校验失败处理。Token缺失或不匹配时CsrfFilter会抛出AccessDeniedException最终由AccessDeniedHandler返回403响应。把主流程用简化代码表示就是这样// 简化后的CsrfFilter核心流程仅用于理解思路 CsrfToken csrfToken tokenRepository.loadToken(request); if (csrfToken null) { csrfToken tokenRepository.generateToken(request); tokenRepository.saveToken(csrfToken, request, response); } if (requiresValidation(request)) { // POST / PUT / PATCH / DELETE String actualToken obtainToken(request); // 从参数或Header取 if (!Objects.equals(actualToken, csrfToken.getToken())) { accessDeniedHandler.handle(...); // 403 } }这四步里最容易被忽略的是“Token是新生成的”这个动作。首次访问页面的匿名用户也会拿到一个Token这导致后面要遇到的“登录后403”问题原因就出在这个环节上后面专门讲。3.2 Token从哪来两种Token仓库的取舍Spring Security把Token的生成、存储、加载封装成了CsrfTokenRepository接口。框架自带两个实现一是HttpSessionCsrfTokenRepositoryToken保存在服务端HttpSession中页面需要把Token渲染成隐藏字段放进表单。这是Spring Security默认使用的实现适合传统的服务端渲染MVC项目。二是CookieCsrfTokenRepositoryToken保存在浏览器Cookie里默认Cookie名是XSRF-TOKEN前端JavaScript可以读取这个Cookie把它放到请求Header中传给服务端。适合前后端分离的SPA项目。两者选型对比对比项HttpSessionCsrfTokenRepositoryCookieCsrfTokenRepositoryToken存储位置服务端Session浏览器Cookie前端如何获取服务端渲染隐藏字段_csrfJS读取XSRF-TOKEN Cookie请求时放在哪表单参数_csrf请求头X-XSRF-TOKEN或X-CSRF-TOKEN适合场景服务端渲染、Thymeleaf/JSPSPA、前后端分离多实例部署Session共享方案需额外配置无状态天然适合集群选择依据很简单如果你的页面是服务端渲染出来的表单里直接嵌Token最方便如果你的项目是前后端分离前端没有任何服务端渲染能力那就必须用Cookie方式。3.3 哪些请求需要被校验哪些不需要默认规则是仅校验PATCH、POST、PUT、DELETE这四类方法。这意味着GET和HEAD请求完全绕过CSRF Token校验。这个设计有它的合理性但也埋了一个隐患。如果项目里有人把删除操作做成了GET请求比如/api/delete?id1CSRF防护默认对GET不生效这时攻击者只需要一个img标签就能触发删除。所以最佳实践是双管齐下一方面依赖Spring Security的默认校验规则另一方面在接口设计层面强制规定——所有状态变更操作一律使用POST/PUT/DELETE不允许用GET。这是RESTful设计的基本要求也是CSRF防护能生效的前提。另外自定义配置时可以用ignoringRequestMatchers指定某些路径不做校验比如登录接口、回调接口。这个后面实操部分会展开。3.4 Token如何安全地送到前端传统MVC模式下Thymeleaf模板引擎会在表单里自动插入_csrf隐藏字段这是通过RequestDataValueProcessor实现的开发者不需要手动处理。JSP项目则需要Spring Security的标签库来渲染。input typehidden name${_csrf.parameterName} value${_csrf.token}/前后端分离模式下后端返回的响应里会有一个XSRF-TOKEN的Cookie前端JS读取后放进请求头。这里有个关键点CookieCsrfTokenRepository默认创建的Cookie是HttpOnly的JavaScript读不到必须显式调用withHttpOnlyFalse()。这个细节是我见过前端同事问得最多的问题后面实操部分会专门演示。4. 前后端分离项目配置CSRF的完整实操4.1 先搞清楚SPA项目的问题在哪前后端分离项目用CSRF防护难点不在后端配置而在“前端根本拿不到Token”。传统MVC模式下服务端渲染出HTML时顺手把Token放进了表单隐藏域。但SPA项目里HTML是静态资源Token总不能写死在静态文件里吧那就必须让后端通过接口或者Cookie把Token下发到前端。既然要下发用CookieCsrfTokenRepository是最自然的方案。后端生成Token后写进Cookie前端JavaScript读取Cookie的值之后每次请求用自定义Header带上它。有个安全细节需要说明有人担心Token放在Cookie里不安全攻击者能不能直接读取跨域页面是读不到的因为Cookie的读写受同源策略限制。攻击者能做的只是让浏览器自动带Cookie但浏览器的forbidden to set cookie头并不允许JavaScript跨域读取Cookie所以Token放在Cookie里攻击者确实拿不到值。这也是“双重提交Cookie”策略的基础。4.2 后端配置CookieToken仓库与忽略路径先看完整的后端配置代码Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/auth/logout).permitAll() .anyRequest().authenticated() ); http.csrf(csrf - csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers(/api/auth/login, /api/auth/logout) ); return http.build(); } }CookieCsrfTokenRepository.withHttpOnlyFalse()这个工厂方法会创建一个Token仓库并把写Cookie的HttpOnly属性设为false目的是允许前端JS读取。如果漏掉这个方法前端永远读不到XSRF-TOKEN请求头带不上Token所有写操作都会403。ignoringRequestMatchers里放的是登录和登出接口。为什么这两个接口要豁免因为用户在登录之前还没有拿到Token如果登录接口也校验Token就会出现“为了登录得先有Token但没有Token无法登录”的死循环。登出接口也是同理用户Session可能已经过期强制校验Token会让用户无法登出。Spring Security版本差异在这里很坑。5.x时代用的是http.csrf().ignoringAntMatchers(/api/auth/login, /api/auth/logout);Spring Security 6.x里ignoringAntMatchers被废弃了改成ignoringRequestMatchers。旧代码直接升级会编译报错而且antMatchers在6.x中也被requestMatchers彻底取代。如果你接手的老项目要升级这地方必须改。4.3 前端配合把Token从Cookie塞进Header后端配置好之后前端需要进行两步配合。第一步从Cookie里读取XSRF-TOKEN。第二步在每个写请求的Header里带上这个TokenHeader名必须是X-XSRF-TOKEN。这是Spring Security的默认约定CsrfFilter在比对时会先从参数里找再从Header里找Header支持X-CSRF-TOKEN和X-XSRF-TOKEN两个名字。axios项目里用拦截器统一处理import axios from axios; function getCookieValue(name) { const match document.cookie.match(new RegExp((^|; ) name ([^;]*))); return match ? decodeURIComponent(match[2]) : null; } axios.interceptors.request.use(config { const method config.method.toLowerCase(); // 非简单请求才需要带Token if ([post, put, patch, delete].includes(method)) { const csrfToken getCookieValue(XSRF-TOKEN); if (csrfToken) { config.headers[X-XSRF-TOKEN] csrfToken; } } return config; }); axios.interceptors.response.use( response response, error { if (error.response error.response.status 403) { // 这里要考虑Token失效后的处理见4.4 } return Promise.reject(error); } );这里有一个细节X-XSRF-TOKEN这个Header不是CORS白名单里的“简单请求头”所以跨域场景下浏览器会先发OPTIONS预检请求。后端CORS配置需要把X-XSRF-TOKEN加进允许的Header列表里否则前端请求根本发不出去。Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.setAllowedOriginPatterns(List.of(http://localhost:5173)); config.setAllowedMethods(List.of(GET, POST, PUT, DELETE, OPTIONS)); config.setAllowedHeaders(List.of(Content-Type, X-XSRF-TOKEN)); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; }如果漏配X-XSRF-TOKEN到allowedHeaders前端GET请求正常POST请求会一直卡在CORS预检阶段页面控制台报错“Request header field x-xsrf-token is not allowed by Access-Control-Allow-Headers”。这是我见过的最典型的CORSCSRF联调问题。4.4 多标签页与Token刷新的坑CSRF Token不是永久的。Spring Security默认在每次请求后都会生成新的Token并保存到Cookie中。这在单页面里没事因为页面每次交互后会同步刷新Token。但多标签页场景下问题就来了。用户在标签页A登录了系统又在标签页B打开了同一系统。标签页B初次加载时从Cookie里读到了Token T1。用户先在标签页A里提交了一个表单服务端校验通过后生成了新Token T2并写到Cookie。此时标签页B的Cookie也被更新成了T2但页面B内存里的Token还是T1。接下来用户在标签页B提交表单请求头里带的是T1服务端当前Cookie里的Token是T2比对失败返回403。这不是配置错误是Token轮换机制在多标签页下的固有冲突。解决思路有好几种我推荐比较实用的一种是前端在收到403响应时从响应头或者新Cookie里取出最新Token自动重发一次请求。Spring Security的CookieCsrfTokenRepository在Token不匹配时响应里会带上新的XSRF-TOKENCookie前端可以读取后重试。后端也可以做调整不每次轮换Token只在登录前和登录后生成一次。但这种做法降低了安全性生产环境不建议。5. 常见问题与排查技巧实录我踩过的五个坑5.1 登录之后立刻403这是前后端分离模式里最经典的问题。现象是登录接口正常返回用户信息也拿到了但紧接着调第一个业务接口就返回403。原因出在Session和Token的生命周期错配。用户未登录时服务端创建了一个匿名SessionCsrfFilter在首次访问时给这个匿名Session生成了Token T1前端Cookie里存的是T1。用户提交登录请求后Spring Security创建了新的认证SessionSession ID变了Token T1跟着旧Session一起失效。前端还在用T1服务端当前Session里已经没有这个Token了比对必然失败。解决方式分两种情况。如果项目能接受登录后刷新页面让页面重新从服务端拿Token最省事。如果不想刷新页面前端需要在登录接口成功后重新读取一次Cookie里的Token登录成功后服务端会写了新Token更新请求头再发业务请求。这里要给一个自定义认证成功处理器的示例确保登录成功后服务端主动刷新TokenComponent public class LoginSuccessHandler implements AuthenticationSuccessHandler { private final ObjectMapper objectMapper new ObjectMapper(); Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { response.setContentType(application/json;charsetUTF-8); response.getWriter().write(objectMapper.writeValueAsString( Map.of(code, 0, message, 登录成功) )); } }配合CookieCsrfTokenRepository登录成功后Spring Security会自动写入新的XSRF-TOKENCookie前端登录成功回调里重新读取即可。5.2 Postman、命令行脚本永远403用Postman调试带CSRF防护的接口时会发现直接填username和password发POST请求永远返回403。原因很简单Postman发出的请求既没有表单里的_csrf参数也没有X-XSRF-TOKEN请求头。解决方法是先在浏览器里登录一次从Cookie里把XSRF-TOKEN复制出来在Postman请求头里手动加上X-XSRF-TOKEN: 复制的值。如果你在用curl做接口测试也是同样的思路curl -X POST http://localhost:8080/api/account/profile \ -H Cookie: JSESSIONIDxxx; XSRF-TOKENxxx \ -H X-XSRF-TOKEN: xxx \ -d nicknametest有个自动化测试的小技巧Postman的Tests脚本里可以这样动态提取Token省去手动复制const csrfToken pm.cookies.get(XSRF-TOKEN); pm.globals.set(csrfToken, csrfToken);然后把请求头的X-XSRF-TOKEN值设为{{csrfToken}}后续请求会自动带上。5.3 配置了豁免路径却不生效很多人在ignoringRequestMatchers里配置了/api/**但请求/api/account/profile时依然403。最常见的原因是Spring Security的requestMatchers匹配的是“请求路径”如果你的项目有context-path比如/myapp/api/**那配/api/**永远不会命中。这时候要在配置里加上context-path前缀。另一个原因容易忽略requestMatchers可以指定请求方法之前Spring Security 5里ignoringAntMatchers(HttpMethod.POST, /api/login)只豁免POST请求如果你配的时候没写方法默认匹配所有方法但某些写法可能和你预期不一致。建议豁免时显式限定方法和路径http.csrf(csrf - csrf.ignoringRequestMatchers( new AntPathRequestMatcher(/api/auth/login, POST), new AntPathRequestMatcher(/api/auth/logout, POST) ));5.4 网关或代理层丢失Cookie和Header前后端分离项目很少有浏览器直连后端服务的中间通常隔着一层Nginx或者网关。这层代理如果配置不对会直接把CSRF链路切断。最典型的是Nginx没有转发请求头X-XSRF-TOKEN到了Nginx就丢了。Nginx配置里需要显式透传location /api/ { proxy_pass http://backend-server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-XSRF-TOKEN $http_x_xsrf_token; }还有一种情况是代理路径重写导致Cookie路径不匹配。比如后端是/apiNginx对外是/app/apiCookie返回到浏览器时路径不对导致后续请求带不上Cookie。这一类问题需要结合浏览器Developer Tools里的Application面板和Network面板一起排查看响应里Cookie到底有没有写成功、写到了哪个路径下。5.5 前端读不到XSRF-TOKEN前端document.cookie里看不到XSRF-TOKEN这是最常见的配置遗漏。排查顺序如下。第一步确认后端用的是CookieCsrfTokenRepository.withHttpOnlyFalse()而不是默认构造方法。默认构造方法创建的Cookie带HttpOnly标记JS读取不到这是代码层面最常踩的坑。第二步确认前端访问域名和后端Cookie的Domain匹配如果Cookie设置的是.example.com前端在admin.example.com下能读到但如果前端是localhost:5173后端是localhost:8080Cookie Domain是localhost不同端口之间可以读前提是Secure标记没开。第三步确认Cookie有没有Secure属性如果配置了Secure非HTTPS环境下浏览器不会写这个Cookie。我把这个问题的排查步骤整理成下面这个速查表实际遇到403时可以对照着查现象可能原因排查与解决前端读不到XSRF-TOKENCookie设置了HttpOnly使用withHttpOnlyFalse()前端读不到XSRF-TOKEN非HTTPS下Cookie带Secure属性去掉Secure或改用HTTPS环境POST请求403请求头没带Token在前端拦截器统一添加X-XSRF-TOKEN登录后首次请求403登录前后Session切换Token失效登录成功后重新读取Token特定接口403接口没在豁免列表检查ignoringRequestMatchers配置跨域POST被CORS拦截未允许X-XSRF-TOKEN请求头CORS配置的allowedHeaders加上它6. 最佳实践安全基线不是“开启CSRF”就完了做完攻防实验和配置之后最后聊一聊真正落地的安全基线。我的建议可以概括成几条硬规矩。第一保持Spring Security默认的CSRF防护开启不要因为前端觉得麻烦就全局csrf.disable()。任何一个接口如果必须豁免必须写清楚理由。登录和登出接口豁免是合理的但那些出于“前端调用不方便”就豁免的内部管理接口就是在给攻击者开后门。第二登录接口豁免后一定要有配套的限流和失败处理策略。登录接口不做CSRF校验那就等于开放了一个可以被暴力刷的入口至少要加上IP维度或账号维度的尝试次数限制。第三Cookie层的SameSite属性是浏览器提供的免费防御层能开就开。Spring Security写Cookie时可设置Bean public CookieCsrfTokenRepository csrfTokenRepository() { CookieCsrfTokenRepository repo CookieCsrfTokenRepository.withHttpOnlyFalse(); repo.setCookieCustomizer(cookie - cookie .sameSite(Lax) .secure(true)); return repo; }注意SameSiteLax对跨站POST已经默认拦截了所以现在的真实攻击面更多集中在同站子域或者被攻击页面与目标站点属于同一有效顶级域的情况。不能因为浏览器默认Lax就放松服务器端防护。第四最重要的操作不可只依赖CSRF Token。修改密码、更换手机号、解绑邮箱这类高敏感操作项目里至少应该加一道二次验证比如旧密码、验证码或者短信确认。CSRF防的是“请求伪造”如果用户账号已经被攻击者完全控制CSRF就无能为力了多层防御才是稳定可靠的姿势。第五如果项目本身是纯API、基于无状态JWT认证Token放在Authorization Header由JavaScript显式添加CSRF的利用条件已经不成立可以结合团队评估后关闭。但关之前要确认所有认证信息都不在Cookie里也没有任何接口会接受Cookie方式认证。做过认证迁移的项目最容易在这里留尾巴——老接口还认Cookie新接口走JWT这时候关掉CSRF就是在裸奔。我在实际项目中就处理过一次类似的返工。某管理后台为了省事把所有CSRF校验都关了结果安全测试用一个隐藏表单直接改掉了普通用户的手机号绑定。后来恢复默认防护把登录接口单独豁免并加上验证码和限流这才重新过了安全评审。那次之后我得出的经验是安全配置一定要先按默认值来豁免每一个接口都要问一句“为什么它不能带Token”。CSRF这块内容说难不难但它把浏览器机制、过滤器链路、前后端协作方式串在一起任何一个环节理解不到位都会在线上环境冒烟。希望这篇实战拆解能帮你把整条链路理清楚。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询