行业资讯
Spring Boot应用CSRF防护实战:从原理到配置的完整指南
1. 项目概述为什么Spring Boot应用必须重视CSRF防护如果你正在用Spring Boot开发一个Web应用尤其是涉及用户登录、表单提交、状态变更比如转账、修改密码、发表评论的功能那么CSRF跨站请求伪造就是一个绕不开的安全话题。我见过太多项目前端页面做得漂亮后端接口逻辑严谨却在安全防护上开了个“后门”而这个后门往往就是CSRF。简单来说CSRF攻击就是攻击者诱导用户在已经登录了目标网站比如你的银行网站的浏览器中去访问一个恶意页面。这个恶意页面会自动向目标网站发起一个请求比如转账请求因为用户的浏览器里还存着有效的登录凭证Cookie服务器就会认为这是用户的合法操作从而执行攻击者的指令。在Spring Boot的生态里得益于Spring Security这个强大的安全框架实现CSRF防护其实并不复杂甚至可以说是“开箱即用”。但问题恰恰出在这里很多开发者要么完全不知道需要开启要么开启了却因为配置不当导致功能异常最后索性全局关闭了CSRF防护这无异于因噎废食。所以这篇内容不是简单地告诉你“加个注解”或“配个过滤器”而是会深入拆解Spring Security中CSRF防护的运作机制手把手带你完成从零到一的配置并分享我在实际项目中趟过的坑和积累的调试技巧。无论你是刚接触Spring Security的新手还是想优化现有项目安全性的老手都能从这里获得可直接落地的方案。2. CSRF防护的核心原理与Spring Security的实现机制2.1 CSRF攻击的本质与经典场景要防御先得理解攻击是如何发生的。我们假设你的应用有一个修改用户邮箱的接口POST /user/updateEmail它接收一个newEmail参数。在没有任何防护的情况下攻击者可以构造一个这样的恶意页面!-- 恶意网站上的页面 -- body onloaddocument.forms[0].submit() form actionhttps://你的网站.com/user/updateEmail methodPOST input typehidden namenewEmail valuehackerevil.com/ /form /body如果用户已经登录了“你的网站”并且会话未过期那么当他访问这个恶意页面时表单会自动提交。浏览器会携带该域名下的所有Cookie包括标识用户身份的Session Cookie发送请求。你的服务器看到这个带有合法Session Cookie的POST请求就会正常处理把用户的邮箱改成了攻击者的邮箱。整个过程用户完全不知情。CSRF攻击成功的核心要素有三个1. 用户在当前网站已登录存在Cookie2. 网站的业务接口没有对请求来源做二次验证3. 攻击者可以预测或诱导用户发起请求的参数。它利用的是浏览器对Cookie的自动携带机制与XSS跨站脚本攻击注入并执行恶意脚本有本质区别。2.2 Spring Security的CSRF防护设计哲学Spring Security的CSRF防护采用了一种主流且有效的策略同步令牌模式。它的核心思想是不仅要用Cookie或Session来验证“你是谁”还要用一个额外的、不可预测的令牌来验证“这个请求确实是你自愿发起的”。具体实现上它主要包含两个部分令牌生成与存储服务器会为每一个会话生成一个唯一的、随机的CSRF令牌Token。默认情况下这个令牌会被存储在HttpSession中CsrfToken对象。同时Spring Security期望这个令牌能通过以下两种方式之一被前端获取作为请求属性对于服务端渲染如Thymeleaf, JSP的页面CsrfToken会自动被添加到请求属性中方便模板引擎将其输出到表单里。通过Cookie携带更适用于前后端分离架构。服务器可以将令牌设置在一个Cookie中通常名为XSRF-TOKEN前端JavaScript代码可以读取这个Cookie并在后续请求的Header中携带它。令牌验证对于需要防护的HTTP请求通常是除GET,HEAD,TRACE,OPTIONS之外的所有“状态变更”请求Spring Security的CsrfFilter会拦截请求并检查请求中是否包含一个有效的CSRF令牌。这个令牌必须通过以下两种方式之一传递请求参数例如表单中的一个隐藏字段input typehidden name_csrf valuetoken-value/。请求头例如在HTTP Header中添加X-CSRF-TOKEN: token-value或X-XSRF-TOKEN: token-value。服务器会比对请求携带的令牌和会话中存储的令牌是否一致。一致则放行不一致则抛出AccessDeniedException请求被拒绝默认返回403状态码。注意Spring Security默认会为GET,HEAD,TRACE,OPTIONS这些被认为是“安全”的、不改变资源状态的请求禁用CSRF检查。这是符合RFC标准的。你的删除、更新等操作务必使用POST、PUT、PATCH或DELETE方法。2.3 默认配置的潜在问题与自定义切入点Spring Boot在引入spring-boot-starter-security依赖后CSRF防护默认是启用的。这本来是好事但如果你在不了解其机制的情况下直接启动应用很可能会发现所有非GET的API调用都返回403。这时很多人的第一反应是去搜索“Spring Boot 禁用 CSRF”这其实关闭了一道重要的安全门。正确的做法是理解它并正确地适配你的前端架构。你的自定义配置主要会围绕以下几个点展开令牌存储策略默认用Session存储但在微服务或无状态架构中你可能需要自定义存储比如放到Redis中。令牌请求匹配器默认防护所有非安全方法。但你可能有一些特殊的、公开的POST接口如webhook回调不需要防护需要排除。令牌的获取与传递方式需要指导前端如何正确地获取和发送令牌这取决于你是传统MVC还是前后端分离。3. 实战配置为不同架构的Spring Boot应用集成CSRF理论讲完我们进入实战。下面我将分两种最常见的架构模式展示具体的配置和代码。3.1 场景一传统服务端渲染应用如使用Thymeleaf这是Spring Security支持得最“原生”的场景配置起来最简单。假设你的项目使用了Thymeleaf模板引擎。3.1.1 基础安全配置首先创建一个Spring Security的配置类。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import static org.springframework.security.config.Customizer.withDefaults; Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 使用新的CsrfTokenRequestAttributeHandler兼容性更好 CsrfTokenRequestAttributeHandler requestHandler new CsrfTokenRequestAttributeHandler(); requestHandler.setCsrfRequestAttributeName(null); // 设置为null让令牌从请求属性中获取 http .authorizeHttpRequests(authz - authz .requestMatchers(/, /home, /login, /css/**, /js/**).permitAll() // 公开资源 .anyRequest().authenticated() // 其他所有请求都需要认证 ) .formLogin(form - form .loginPage(/login) // 自定义登录页 .permitAll() ) .logout(logout - logout.permitAll()) // 关键CSRF配置。默认就是启用的这里显式声明并使用自定义的handler .csrf(csrf - csrf .csrfTokenRequestHandler(requestHandler) // 可以在这里排除某些不需要CSRF防护的路径例如公开的API端点 // .ignoringRequestMatchers(/public-api/**) ); return http.build(); } }3.1.2 前端表单集成在Thymeleaf模板中Spring Security会自动提供一个CsrfToken对象。你只需要在表单中插入它即可。!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head title修改邮箱/title /head body h1修改您的邮箱/h1 form methodpost action/user/updateEmail !-- 关键使用Thymeleaf的CSRF输入标签 -- input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} / label fornewEmail新邮箱地址/label input typeemail idnewEmail namenewEmail required / br/ button typesubmit提交修改/button /form /body /html${_csrf.parameterName}默认是_csrf${_csrf.token}就是令牌值。表单提交时这个隐藏字段会和其它数据一起以参数形式提交CsrfFilter就能正确验证。实操心得在Thymeleaf中如果你使用了th:action并且action URL带参数有时需要确保CSRF令牌能正确附加。最稳妥的方式就是像上面一样显式地写出隐藏域。另外确保你的form标签的method是post或putdelete等。3.2 场景二前后端分离应用如Vue/React REST API这是目前更主流的架构后端只提供JSON API。这种情况下令牌通常通过Cookie下发由前端读取并设置在请求Header中。3.2.1 后端安全配置配置的核心是让Spring Security将CSRF令牌放在Cookie中并调整验证逻辑使其从Header中读取。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; Configuration EnableWebSecurity public class SecurityConfigForSpa { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 1. 使用Cookie来存储和传递CSRF令牌 CookieCsrfTokenRepository tokenRepository CookieCsrfTokenRepository.withHttpOnlyFalse(); // 设置Cookie名称前端通常从名为XSRF-TOKEN的Cookie中读取 // httpOnlyFalse允许前端JS读取此Cookie这是必须的。 // 2. 创建请求处理器 CsrfTokenRequestAttributeHandler requestHandler new CsrfTokenRequestAttributeHandler(); // 使用XorCsrfTokenRequestAttributeHandler可以提供对BREACH攻击的额外防护 // 但注意它要求令牌必须放在Header中且对Cookie中的令牌进行了解码处理。 // 这里为了通用性我们先使用标准handler。 requestHandler.setCsrfRequestAttributeName(null); http // 前后端分离通常使用无状态会话JWT等但CSRF Cookie与会话无关可以保留。 // 如果你用JWT可以设置无状态但CSRF防护依然有效。 .sessionManagement(session - session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 示例无状态 ) .authorizeHttpRequests(authz - authz .requestMatchers(/api/auth/**, /public/**).permitAll() .anyRequest().authenticated() ) // 禁用FormLogin通常使用JSON登录 .formLogin(form - form.disable()) .httpBasic(basic - basic.disable()) // 配置CSRF .csrf(csrf - csrf .csrfTokenRepository(tokenRepository) // 指定使用Cookie仓库 .csrfTokenRequestHandler(requestHandler) // 非常重要排除不需要CSRF防护的端点例如登录接口本身 // 因为登录前用户没有有效的会话也就没有CSRF令牌。 .ignoringRequestMatchers(/api/auth/login, /api/webhook/**) ) // 添加一个过滤器确保在每次响应中都设置CSRF Cookie如果缺失 .addFilterAfter(new CsrfCookieFilter(), CsrfFilter.class); return http.build(); } }3.2.2 辅助过滤器确保Cookie下发上面的配置中引用了一个CsrfCookieFilter它的作用是在响应中确保CSRF Cookie被设置。这对于首次访问或令牌刷新很有用。import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; public class CsrfCookieFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求属性中获取CsrfToken由CsrfFilter生成并放入 CsrfToken csrfToken (CsrfToken) request.getAttribute(CsrfToken.class.getName()); // 如果令牌不为null且响应中还没有对应的Cookie则将其渲染到响应中。 // CookieCsrfTokenRepository会在生成令牌时自动设置Cookie这个过滤器是双重保险。 if (csrfToken ! null) { // 通常CookieCsrfTokenRepository已经做了这里可以留空或进行日志记录。 // 如果你自定义了逻辑可以在这里处理。 } filterChain.doFilter(request, response); } }3.2.3 前端以Axios为例的配合工作后端配置好后前端需要做两件事1. 从Cookie读取初始令牌2. 在后续请求的Header中携带它。// axiosConfig.js 或 main.js import axios from axios; // 1. 创建一个axios实例 const apiClient axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL, withCredentials: true, // 关键允许跨域请求携带Cookie包括CSRF Cookie }); // 2. 请求拦截器在每次请求前从Cookie中读取XSRF-TOKEN并设置到Header中 apiClient.interceptors.request.use( (config) { // 读取名为 XSRF-TOKEN 的Cookie const token getCookie(XSRF-TOKEN); if (token) { // Spring Security默认从X-XSRF-TOKEN Header读取与Cookie名称对应 config.headers[X-XSRF-TOKEN] token; // 或者如果你后端配置的是X-CSRF-TOKEN则改为 // config.headers[X-CSRF-TOKEN] token; } return config; }, (error) { return Promise.reject(error); } ); // 辅助函数从Cookie中获取指定名称的值 function getCookie(name) { const value ; ${document.cookie}; const parts value.split(; ${name}); if (parts.length 2) return parts.pop().split(;).shift(); return null; } export default apiClient;注意事项withCredentials: true这是跨域请求携带Cookie的必备选项。同时你的后端也需要配置CORS允许凭证Access-Control-Allow-Credentials: true和你的前端源。首次获取令牌用户首次访问应用或会话刷新后前端需要触发一个GET请求比如获取用户信息后端会在这次响应的Set-Cookie头中下发XSRF-TOKEN。之后拦截器才能读到。Header名称确保前端设置的Header名称与后端期望的一致。CookieCsrfTokenRepository默认期望X-XSRF-TOKEN。如果你自定义了headerName前后端需同步。4. 深度调优与高级配置策略基础配置能跑通但要应对复杂的生产环境还需要一些调优策略。4.1 自定义令牌存储与持久化默认的HttpSessionCsrfTokenRepository或CookieCsrfTokenRepository在单机应用中没问题。但在集群部署或使用无状态会话如JWT时你可能需要将令牌存储到外部缓存如Redis。import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.DefaultCsrfToken; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.UUID; public class RedisCsrfTokenRepository implements CsrfTokenRepository { private final RedisTemplateString, String redisTemplate; private final String keyPrefix csrf:; public RedisCsrfTokenRepository(RedisTemplateString, String redisTemplate) { this.redisTemplate redisTemplate; } Override public CsrfToken generateToken(HttpServletRequest request) { String tokenId UUID.randomUUID().toString(); // 你可以生成更复杂的令牌 return new DefaultCsrfToken(X-CSRF-TOKEN, _csrf, tokenId); } Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { String sessionId request.getSession(false) ! null ? request.getSession().getId() : resolveTokenIdentifier(request); if (sessionId ! null) { String key keyPrefix sessionId; if (token null) { // 令牌置空时删除如登出 redisTemplate.delete(key); } else { // 将令牌存入Redis设置过期时间如30分钟 redisTemplate.opsForValue().set(key, token.getToken(), Duration.ofMinutes(30)); } } } Override public CsrfToken loadToken(HttpServletRequest request) { String sessionId request.getSession(false) ! null ? request.getSession().getId() : resolveTokenIdentifier(request); if (sessionId null) { return null; } String key keyPrefix sessionId; String tokenValue redisTemplate.opsForValue().get(key); if (tokenValue ! null) { return new DefaultCsrfToken(X-CSRF-TOKEN, _csrf, tokenValue); } return null; } // 对于无状态会话你需要另一种方式标识用户比如从JWT中解析出的用户ID private String resolveTokenIdentifier(HttpServletRequest request) { // 实现你的逻辑例如从Authorization Header的JWT中提取用户ID // return userId; return null; // 示例返回null } }然后在配置中注入这个自定义的RepositoryBean public CsrfTokenRepository csrfTokenRepository(RedisTemplateString, String redisTemplate) { return new RedisCsrfTokenRepository(redisTemplate); } // 在Security配置链中使用 .csrf(csrf - csrf.csrfTokenRepository(csrfTokenRepository))4.2 精细化控制请求匹配哪些需要防护哪些不需要不是所有POST请求都需要CSRF防护。典型的例外包括登录接口用户尚未认证没有会话CSRF令牌无从谈起。公开的Webhook回调由第三方服务调用无法携带你的CSRF令牌。某些公开的API端点如果你的API设计为完全无状态且公开。使用ignoringRequestMatchers方法来排除import org.springframework.http.HttpMethod; import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; .csrf(csrf - csrf .csrfTokenRepository(tokenRepository) .ignoringRequestMatchers( antMatcher(HttpMethod.POST, /api/auth/login), // 登录 antMatcher(/api/stripe/webhook), // Stripe支付回调 antMatcher(/public/api/**) // 所有公开API ) )4.3 处理文件上传与Multipart请求这是一个经典的坑。当表单的enctype是multipart/form-data时CSRF令牌作为请求参数_csrf可能无法被CsrfFilter正确读取因为过滤器链中CsrfFilter通常在MultipartFilter之前执行此时请求体尚未被解析。解决方案将CSRF令牌放在请求头Header中而不是表单参数里。这是前后端分离架构的天然优势。对于传统表单你可以通过JavaScript在提交前将隐藏域的值添加到Header。form iduploadForm methodpost enctypemultipart/form-data action/upload input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} idcsrfToken/ input typefile namefile/ button typesubmit上传/button /form script document.getElementById(uploadForm).addEventListener(submit, function(e) { e.preventDefault(); const formData new FormData(this); const csrfToken document.getElementById(csrfToken).value; fetch(this.action, { method: POST, body: formData, headers: { // 将令牌从参数移到Header X-CSRF-TOKEN: csrfToken }, credentials: include // 如果需要携带Cookie }).then(response { // 处理响应 }); }); /script同时在后端配置中确保你的CsrfTokenRequestAttributeHandler能够处理Header。5. 问题排查、测试与安全审计配置完成后如何验证它是否生效出了问题怎么查5.1 常见问题排查清单问题现象可能原因排查步骤与解决方案所有POST请求都返回4031. CSRF防护已启用但前端未正确传递令牌。2. 令牌存储/读取不一致如Session丢失。1. 浏览器开发者工具 - “网络”标签检查请求是否携带了_csrf参数或X-CSRF-TOKEN/X-XSRF-TOKENHeader。2. 检查后端Session配置如分布式Session是否同步、Cookie域/路径是否正确。3. 临时开启Spring Security的调试日志logging.level.org.springframework.securityDEBUG。登录后第一个POST请求失败后续成功首次加载页面时CSRF令牌尚未生成或下发到前端。确保在加载主页面或任何包含表单的页面的GET请求响应中后端已经生成了令牌。对于SPA确保有一个初始化GET请求如/api/csrf来获取令牌。文件上传时403Multipart请求中表单参数未被正确解析。按照4.3节的方法将令牌通过HTTP Header传递。检查MultipartFilter和CsrfFilter的执行顺序通常MultipartFilter应更早。跨域请求失败前端设置了withCredentials: true但后端CORS配置未允许凭证。在后端CORS配置中明确设置allowCredentials(true)并且不能使用通配符*作为allowedOrigins。令牌不匹配/无效1. 前后端使用了不同的令牌存储键Key。2. 令牌已过期自定义存储时。3. 集群环境下请求被负载均衡到不同实例Session或令牌未共享。1. 检查CsrfTokenRepository的实现确保生成、保存、加载的键一致。2. 检查Redis等外部存储的TTL设置。3. 确保使用集中式的Session存储如Spring Session Redis或自定义的分布式令牌存储。5.2 如何测试CSRF防护是否生效手动测试使用浏览器正常登录你的应用。打开开发者工具复制一个需要保护的POST请求作为cURL命令。新开一个无痕窗口确保没有登录Cookie直接执行这个cURL命令。应该收到403错误。在已登录的窗口执行同样的cURL命令会携带Cookie应该成功或根据业务逻辑返回相应结果。自动化测试使用Spring Boot TestSpringBootTest AutoConfigureMockMvc class CsrfSecurityTest { Autowired private MockMvc mockMvc; Test void testProtectedEndpointWithoutCsrfTokenShouldFail() throws Exception { // 模拟一个没有CSRF令牌的POST请求 mockMvc.perform(post(/api/user/email) .with(user(testUser)) // 模拟一个认证用户 .contentType(MediaType.APPLICATION_JSON) .content({\newEmail\:\testnew.com\})) .andExpect(status().isForbidden()); // 期望403 } Test void testProtectedEndpointWithCsrfTokenShouldSucceed() throws Exception { // 首先执行一个GET请求来获取CSRF令牌 MvcResult result mockMvc.perform(get(/api/user/profile) .with(user(testUser))) .andReturn(); // 从Cookie或响应中提取CSRF令牌取决于你的配置 Cookie csrfCookie result.getResponse().getCookie(XSRF-TOKEN); String csrfToken csrfCookie ! null ? csrfCookie.getValue() : ; // 使用获取到的令牌发起POST请求 mockMvc.perform(post(/api/user/email) .cookie(csrfCookie) // 如果是Cookie方式可能需要携带 .header(X-XSRF-TOKEN, csrfToken) // 在Header中传递令牌 .with(user(testUser)) .contentType(MediaType.APPLICATION_JSON) .content({\newEmail\:\testnew.com\})) .andExpect(status().isOk()); // 期望200 } }5.3 安全审计与加固建议不要禁用CSRF防护这是底线。除非你的API是纯公开、无状态的如OpenAPI且任何状态变更都不依赖会话Cookie。结合SameSite Cookie属性为你的Session Cookie设置SameSiteStrict或Lax可以从浏览器层面阻止大部分跨站请求与CSRF防护形成纵深防御。Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer new DefaultCookieSerializer(); serializer.setSameSite(Strict); // 或 Lax return serializer; }关键操作使用二次验证对于修改密码、转账等极高风险操作除了CSRF防护应强制要求用户进行二次验证如输入短信验证码、密码再次确认这能有效防御由于用户浏览器被恶意扩展或本地恶意软件导致的“同站请求伪造”等更复杂的攻击。定期依赖更新保持Spring Boot、Spring Security及相关依赖的版本更新以获取最新的安全补丁。关注CVE列表特别是与Spring Security相关的漏洞。配置和集成的过程本质上是在安全性和开发便利性之间寻找平衡点。Spring Security提供了一套强大且灵活的机制理解其原理后你就能根据自己项目的实际情况做出最合适的配置选择而不是简单地一关了之。
郑州网站建设
网页设计
企业官网