SpringBoot集成Sa-Token实现CSRF防护全解析
1. SpringBoot Sa-Token 实现 CSRF 令牌校验的核心逻辑CSRFCross-Site Request Forgery攻击是Web应用中常见的安全威胁之一。攻击者通过诱导用户访问恶意页面利用用户已登录的会话状态发起非预期的请求。在SpringBoot项目中我们可以通过Sa-Token框架提供的CSRF防护机制来有效防范这类攻击。1.1 CSRF攻击的基本原理CSRF攻击的本质是借用用户的身份凭证。假设用户已经登录了银行网站此时访问了攻击者精心构造的页面该页面中包含一个自动提交的表单向银行网站发起转账请求。由于浏览器会自动携带用户的Cookie服务器会认为这是用户的合法操作。典型的CSRF攻击流程用户登录信任网站A并保留了登录凭证Cookie/Session用户在不登出A的情况下访问危险网站B网站B中包含向网站A发起请求的恶意代码浏览器自动携带网站A的凭证执行请求1.2 Sa-Token的CSRF防护机制Sa-Token采用令牌同步模式防御CSRF服务端生成一个随机token通常存储在Session或Redis中将token嵌入到前端表单或请求头中用户提交请求时携带该token服务端校验token的有效性这种机制有效的原因是攻击者无法获取或预测这个随机token因此无法构造合法的请求。2. SpringBoot集成Sa-Token的完整配置2.1 基础环境准备首先确保你的项目已经正确配置SpringBoot和Sa-Token依赖!-- pom.xml -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.30.0/version /dependency2.2 Sa-Token基础配置在application.yml中添加基本配置sa-token: # token名称同时也是cookie名称 token-name: satoken # token有效期单位秒 默认30天 timeout: 2592000 # token临时有效期指定时间内无操作就视为token过期单位秒 默认-1 代表不限制 activity-timeout: -1 # 是否允许同一账号并发登录为true时允许一起登录为false时新登录挤掉旧登录 is-concurrent: true # 在多人登录同一账号时是否共用一个token为true时所有登录共用一个token为false时每次登录新建一个token is-share: false # token风格 token-style: uuid # 是否输出操作日志 is-log: false2.3 启用CSRF防护在配置类中启用CSRF防护Configuration public class SaTokenConfigure implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { // 注册Sa-Token的拦截器校验规则为StpUtil.checkLogin() registry.addInterceptor(new SaInterceptor(handle - { // 指定校验的接口路径 SaRouter.match(/**) .notMatch(/user/login, /user/register) .check(r - { // 校验登录状态 StpUtil.checkLogin(); // 校验CSRF token SaHolder.getRequest().checkCsrfToken(); }); })).addPathPatterns(/**); } }3. 前后端协作实现CSRF防护3.1 服务端生成CSRF Token在用户登录成功后服务端需要生成并返回CSRF TokenRestController RequestMapping(/user) public class UserController { PostMapping(/login) public Result login(RequestBody LoginDto dto) { // 验证用户名密码... User user userService.login(dto); // 登录成功后生成CSRF Token String csrfToken SaTokenCsrfUtil.getToken(); return Result.success() .data(user, user) .data(csrfToken, csrfToken); } }3.2 前端存储和使用CSRF Token前端在收到CSRF Token后需要妥善存储并在后续请求中携带// 登录成功后存储token localStorage.setItem(csrfToken, res.data.csrfToken); // 封装axios请求拦截器 axios.interceptors.request.use(config { // 从localStorage获取token const csrfToken localStorage.getItem(csrfToken); if (csrfToken) { // 将token添加到请求头 config.headers[x-csrf-token] csrfToken; } return config; });3.3 表单提交的CSRF防护对于传统的表单提交需要将CSRF Token作为隐藏字段form action/api/transfer methodpost input typehidden namecsrfToken th:value${csrfToken} !-- 其他表单字段 -- input typetext nameamount button typesubmit提交/button /form4. 高级配置与优化4.1 自定义CSRF Token生成策略Sa-Token默认使用UUID生成CSRF Token我们可以自定义生成逻辑Configuration public class MySaTokenConfig { Autowired public void configCsrf(SaTokenConfig config) { // 自定义CSRF Token生成器 SaTokenCsrfUtil.setCsrfTokenGenerator(() - { // 这里可以使用更复杂的生成逻辑 return CSRF- IdUtil.fastSimpleUUID(); }); } }4.2 分布式环境下的CSRF防护在分布式系统中CSRF Token需要存储在共享存储中如Redissa-token: # 配置token存储为Redis token-store-type: redis # Redis配置 redis: host: 127.0.0.1 port: 6379 # password: database: 04.3 防御双重提交Cookie模式除了传统的Token模式还可以启用双重提交Cookie模式Configuration public class MySaTokenConfig { Autowired public void configCsrf(SaTokenConfig config) { // 启用双重提交Cookie模式 SaTokenCsrfUtil.setDoubleSubmitCookie(true); // 设置Cookie名称 SaTokenCsrfUtil.setCookieName(x-csrf-token); } }这种模式下服务端会同时检查请求头和Cookie中的Token是否一致。5. 常见问题与解决方案5.1 Token过期问题CSRF Token默认有效期为会话期间可以通过以下方式调整// 设置CSRF Token有效期单位秒 SaTokenCsrfUtil.setTokenTimeout(60 * 30); // 30分钟5.2 前后端分离架构的特殊处理在前后端分离项目中可能需要调整CSRF Token的传递方式Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(*) .allowedHeaders(*) // 允许前端获取CSRF Token响应头 .exposedHeaders(x-csrf-token) .allowCredentials(true); } }5.3 性能优化建议对于高并发系统可以考虑以下优化措施使用JWT代替Session存储CSRF Token对静态资源和不需防护的接口禁用CSRF校验实现Token的懒加载机制SaRouter.match(/**) .notMatch(/static/**, /api/public/**) .check(r - { // 只有需要防护的接口才校验CSRF if (SaTokenCsrfUtil.isCsrfCheck()) { SaHolder.getRequest().checkCsrfToken(); } });6. 安全增强措施6.1 同源策略检查除了CSRF Token还可以加强同源策略检查Bean public FilterRegistrationBeanSameOriginFilter sameOriginFilter() { FilterRegistrationBeanSameOriginFilter registration new FilterRegistrationBean(); registration.setFilter(new SameOriginFilter()); registration.addUrlPatterns(/*); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); return registration; }6.2 请求频率限制防止CSRF Token被暴力破解Bean public FilterRegistrationBeanRateLimitFilter rateLimitFilter() { FilterRegistrationBeanRateLimitFilter registration new FilterRegistrationBean(); registration.setFilter(new RateLimitFilter()); registration.addUrlPatterns(/api/*); registration.setOrder(1); return registration; }6.3 安全头部配置添加安全相关的HTTP头部Configuration public class SecurityHeaderConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new HandlerInterceptorAdapter() { Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { response.setHeader(X-Content-Type-Options, nosniff); response.setHeader(X-Frame-Options, DENY); response.setHeader(X-XSS-Protection, 1; modeblock); } }); } }7. 测试与验证7.1 单元测试CSRF防护编写测试用例验证CSRF防护是否生效SpringBootTest AutoConfigureMockMvc class CsrfTests { Autowired private MockMvc mockMvc; Test void testCsrfProtection() throws Exception { // 模拟登录获取CSRF Token MvcResult loginResult mockMvc.perform(post(/user/login) .contentType(MediaType.APPLICATION_JSON) .content({\username\:\admin\,\password\:\123456\})) .andExpect(status().isOk()) .andReturn(); String csrfToken JsonPath.read(loginResult.getResponse().getContentAsString(), $.data.csrfToken); // 测试带有效Token的请求 mockMvc.perform(post(/api/transfer) .header(x-csrf-token, csrfToken) .contentType(MediaType.APPLICATION_JSON) .content({\amount\:100})) .andExpect(status().isOk()); // 测试不带Token的请求 mockMvc.perform(post(/api/transfer) .contentType(MediaType.APPLICATION_JSON) .content({\amount\:100})) .andExpect(status().isForbidden()); } }7.2 使用Postman测试在Postman中测试CSRF防护首先调用登录接口获取CSRF Token在后续请求的Headers中添加x-csrf-token: 获取到的token验证不带Token的请求是否被拒绝7.3 安全扫描验证使用OWASP ZAP等工具进行安全扫描确认没有CSRF漏洞所有敏感操作都要求CSRF TokenToken具有足够的随机性和强度8. 生产环境部署建议8.1 监控与告警配置监控系统跟踪CSRF验证失败的情况Aspect Component Slf4j public class CsrfMonitorAspect { AfterThrowing(pointcut execution(* cn.dev33.satoken.servlet.SaTokenCsrfFilter.doFilter(..)), throwing ex) public void afterCsrfCheckFailed(Throwable ex) { if (ex instanceof NotCsrfTokenException) { log.warn(CSRF验证失败: {}, ex.getMessage()); // 发送告警通知 alertService.sendAlert(CSRF验证失败告警, ex.getMessage()); } } }8.2 定期更换密钥如果使用加密的CSRF Token建议定期更换加密密钥Scheduled(cron 0 0 0 * * ?) // 每天凌晨执行 public void rotateCsrfKey() { String newKey SecureRandomUtil.randomString(32); SaTokenCsrfUtil.setEncryptKey(newKey); log.info(CSRF加密密钥已更新); }8.3 应急响应计划制定CSRF防护失效的应急方案立即禁用相关功能接口强制所有用户重新登录生成新的CSRF Token审查日志确认是否已被利用修复问题后逐步恢复服务在实际项目中CSRF防护只是安全体系的一部分还需要结合XSS防护、输入验证、权限控制等多层防御措施才能构建真正安全的Web应用。Sa-Token提供的CSRF防护功能既简单易用又足够灵活能够满足大多数SpringBoot项目的安全需求。