基于REST的非MVC SpringBoot应用CSRF集成及自定义CSRF Repository相关技术疑问
嘿,结合你的无状态、多实例REST场景,我来逐个拆解你的疑问:
疑问1:用户发起登录请求时,Spring会自动调用Custom CSRF Repository中的saveToken()方法?
答案是否定的。默认情况下,Spring Security的CSRF逻辑是围绕有状态HttpSession设计的:只有当请求触发了CsrfFilter(比如POST/PUT/DELETE等修改类请求),且当前会话中没有CSRF token时,才会调用generateToken()和saveToken()。但你的场景是无状态REST登录,默认逻辑完全不适用——因为你没有HttpSession,而且登录请求本身是匿名的,Spring Security不会主动为匿名的登录请求触发CSRF token的保存逻辑。
疑问2:若上述理解正确,是否有办法指定Spring识别哪个REST API端点为登录接口?
既然疑问1的理解不对,那咱们换个思路:你需要主动配置登录接口的CSRF token生成逻辑,而不是依赖Spring自动识别。在Spring Security配置中,你可以明确指定登录接口(比如/custom-login)为匿名可访问,然后在登录成功的处理器中,手动调用自定义CSRF Repository的generateToken()和saveToken()方法,把生成的token和用户绑定后返回给前端。
疑问3:正确的实现方式是什么?
针对你的无状态、多实例场景,核心是要让CSRF token和用户身份绑定,且存储在分布式共享存储(比如Redis)中,不能依赖本地会话。具体步骤如下:
自定义CsrfTokenRepository
实现CsrfTokenRepository接口,把token存储到Redis(或数据库),关联用户ID:@Component public class RedisCsrfTokenRepository implements CsrfTokenRepository { private final StringRedisTemplate redisTemplate; private static final String CSRF_PREFIX = "csrf:"; public RedisCsrfTokenRepository(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken("X-XSRF-TOKEN", "_csrf", UUID.randomUUID().toString()); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { // 登录成功后,从认证信息中获取用户ID Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) { String userId = auth.getName(); // 假设用户名是用户唯一标识 redisTemplate.opsForValue().set(CSRF_PREFIX + token.getToken(), userId, 1, TimeUnit.HOURS); // 同时把token写入响应头,方便前端获取 response.setHeader(token.getHeaderName(), token.getToken()); } } @Override public CsrfToken loadToken(HttpServletRequest request) { String token = request.getHeader("X-XSRF-TOKEN"); if (token == null) { return null; } // 验证token是否存在,且属于当前用户 String storedUserId = redisTemplate.opsForValue().get(CSRF_PREFIX + token); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (storedUserId != null && auth != null && storedUserId.equals(auth.getName())) { return new DefaultCsrfToken("X-XSRF-TOKEN", "_csrf", token); } return null; } }配置Spring Security
指定登录接口、启用自定义CSRF Repository,并在登录成功后触发token生成:@Configuration @EnableWebSecurity public class SecurityConfig { private final RedisCsrfTokenRepository csrfTokenRepository; private final AuthenticationSuccessHandler loginSuccessHandler; public SecurityConfig(RedisCsrfTokenRepository csrfTokenRepository) { this.csrfTokenRepository = csrfTokenRepository; // 自定义登录成功处理器,调用CSRF token的保存逻辑 this.loginSuccessHandler = (request, response, authentication) -> { CsrfToken csrfToken = csrfTokenRepository.generateToken(request); csrfTokenRepository.saveToken(csrfToken, request, response); // 这里可以返回登录成功的用户信息+CSRF token response.setContentType("application/json"); response.getWriter().write("{\"success\":true,\"csrfToken\":\"" + csrfToken.getToken() + "\"}"); }; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(csrfTokenRepository) .ignoringRequestMatchers("/custom-login") // 登录接口本身不需要CSRF验证,因为是匿名请求 ) .authorizeHttpRequests(auth -> auth .requestMatchers("/custom-login").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginProcessingUrl("/custom-login") .successHandler(loginSuccessHandler) .permitAll() ); return http.build(); } }前端处理逻辑
登录成功后,从响应中获取CSRF token,后续所有非GET的REST请求,都在请求头X-XSRF-TOKEN中携带该token。
疑问4:Custom CSRF Repository中的generateToken()方法何时会被调用,以及如何与用户进行关联?
调用时机:
默认情况下,CsrfFilter会在第一次处理需要CSRF保护的请求(POST/PUT/DELETE等)时调用generateToken(),但在你的无状态场景下,我们更建议在登录成功后主动调用(如上述代码中loginSuccessHandler里的逻辑),这样能确保token和用户身份绑定后再返回给前端,避免匿名请求生成无效token。与用户关联:
在saveToken()方法中,通过SecurityContextHolder.getContext().getAuthentication()获取当前登录成功的用户身份,把token和用户ID(或用户名)绑定存储到分布式存储中。后续请求验证时,loadToken()会根据请求头中的token找到对应的用户ID,再和当前认证的用户对比,确保token属于当前用户,防止CSRF攻击。
内容的提问来源于stack exchange,提问作者Arvind Singh Rawat

