Spring Security 验证码实现细节与安全加固

5 阅读6分钟

1. 验证码生成与存储

在Spring Security中集成验证码,核心在于生成、展示、验证三个环节。通常使用第三方库(如Hutool-captcha或Kaptcha)生成验证码图片,并将验证码文本临时存储以供校验。

  • 生成与存储:验证码生成后,其文本应存储在服务器端,最佳实践是存储在Redis等分布式缓存中,并设置较短的过期时间(如2分钟)。这避免了Session存储带来的集群同步问题,并提升了安全性。存储时,建议使用“业务前缀:唯一标识(如sessionId或随机token)”作为Key。
  • 接口设计:提供独立的验证码获取接口(如GET /captcha)。该接口生成验证码图片(可直接输出图片流或Base64编码)和唯一标识(如captchaKey),并将captchaKey返回给前端。验证码文本与captchaKey的映射关系存入Redis。

示例:使用Hutool-captcha生成验证码并存入Redis

import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class CaptchaController {

    private final StringRedisTemplate redisTemplate;
    // 验证码Redis Key前缀
    private static final String CAPTCHA_KEY_PREFIX = "captcha:";
    // 验证码有效期(秒)
    private static final long CAPTCHA_EXPIRE_SECONDS = 120;

    @GetMapping("/captcha")
    public void getCaptcha(HttpServletResponse response) throws IOException {
        // 1. 生成验证码(干扰线类型,宽200,高80,4位字符)
        LineCaptcha captcha = CaptchaUtil.createLineCaptcha(200, 80, 4, 50);
        String code = captcha.getCode(); // 验证码文本
        String captchaKey = UUID.randomUUID().toString(); // 生成唯一标识

        // 2. 存储到Redis,设置过期时间
        redisTemplate.opsForValue().set(
                CAPTCHA_KEY_PREFIX + captchaKey,
                code,
                CAPTCHA_EXPIRE_SECONDS,
                TimeUnit.SECONDS
        );

        // 3. 将captchaKey写入响应头(或Cookie),供后续提交时使用
        response.setHeader("Captcha-Key", captchaKey);
        // 4. 将验证码图片写入响应流
        response.setContentType("image/png");
        captcha.write(response.getOutputStream());
    }
}
  

2. 自定义认证过滤器集成验证码校验

Spring Security的核心认证流程由UsernamePasswordAuthenticationFilter处理。我们需要自定义一个过滤器,在其之前执行验证码校验。

  • 流程:自定义过滤器(如CaptchaValidationFilter)应放置在UsernamePasswordAuthenticationFilter之前。它拦截登录请求,从请求中获取用户输入的验证码和对应的captchaKey,然后与Redis中存储的值进行比对。
  • 校验失败处理:若验证码错误、过期或不存在,则直接抛出相应的异常(如AuthenticationServiceException),并返回错误信息,中断后续的认证流程。

示例:自定义验证码校验过滤器

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CaptchaValidationFilter extends OncePerRequestFilter {

    private final StringRedisTemplate redisTemplate;
    private final AuthenticationFailureHandler failureHandler;
    private static final String CAPTCHA_KEY_PREFIX = "captcha:";
    // 登录请求路径和验证码参数名,可根据实际情况配置
    private String loginProcessingUrl = "/login";
    private String captchaParam = "captcha";
    private String captchaKeyParam = "captchaKey";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 1. 仅处理登录请求
        if (!request.getMethod().equals("POST") || !loginProcessingUrl.equals(request.getServletPath())) {
            filterChain.doFilter(request, response);
            return;
        }

        // 2. 获取请求中的验证码和Key
        String inputCaptcha = request.getParameter(captchaParam);
        String inputCaptchaKey = request.getParameter(captchaKeyParam);

        // 3. 进行校验
        try {
            validateCaptcha(inputCaptchaKey, inputCaptcha);
        } catch (AuthenticationException e) {
            // 4. 校验失败,交给失败处理器
            failureHandler.onAuthenticationFailure(request, response, e);
            return;
        }

        // 5. 校验通过,删除已使用的验证码,防止重放攻击
        if (inputCaptchaKey != null) {
            redisTemplate.delete(CAPTCHA_KEY_PREFIX + inputCaptchaKey);
        }

        // 6. 继续执行过滤器链
        filterChain.doFilter(request, response);
    }

    private void validateCaptcha(String captchaKey, String inputCaptcha) throws AuthenticationException {
        if (captchaKey == null || captchaKey.trim().isEmpty()) {
            throw new AuthenticationServiceException("验证码标识不能为空");
        }
        if (inputCaptcha == null || inputCaptcha.trim().isEmpty()) {
            throw new AuthenticationServiceException("验证码不能为空");
        }

        String storedCode = redisTemplate.opsForValue().get(CAPTCHA_KEY_PREFIX + captchaKey);
        if (storedCode == null) {
            throw new AuthenticationServiceException("验证码已过期或不存在");
        }
        if (!storedCode.equalsIgnoreCase(inputCaptcha.trim())) { // 通常忽略大小写
            throw new AuthenticationServiceException("验证码错误");
        }
    }
}
  

3. Spring Security 配置与安全加固

将自定义的过滤器集成到Spring Security的配置中,并实施其他安全加固措施。

关键配置点

  1. 注册过滤器:在SecurityFilterChain配置中,使用addFilterBeforeCaptchaValidationFilter添加到UsernamePasswordAuthenticationFilter之前。
  2. CSRF防护必须启用CSRF防护。对于前后端分离项目,可以将CSRF Token放在请求头(如X-CSRF-TOKEN)中传递,并配置csrfTokenRepository。传统的表单登录则依赖_csrf参数。
  3. 会话管理:对于分布式系统,建议将Session存储到Redis中(使用spring-session-data-redis),实现Session共享和无状态化管理的平衡。
  4. 登录失败处理:配置自定义的AuthenticationFailureHandler,以返回结构化的JSON错误信息,而不是默认的跳转。
  5. 账户安全:集成账户锁定策略(如连续5次密码错误锁定15分钟)和密码强度策略,这些可以通过自定义UserDetailsServiceDaoAuthenticationProvider来实现。

示例:核心安全配置(部分)

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                           CaptchaValidationFilter captchaValidationFilter) throws Exception {
        http
            // 1. 在用户名密码认证前加入验证码过滤器
            .addFilterBefore(captchaValidationFilter, UsernamePasswordAuthenticationFilter.class)
            // 2. 授权配置
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/captcha", "/login").permitAll()
                .anyRequest().authenticated()
            )
            // 3. 表单登录配置
            .formLogin(form -> form
                .loginProcessingUrl("/login")
                .successHandler(myAuthenticationSuccessHandler()) // 自定义成功处理器
                .failureHandler(myAuthenticationFailureHandler()) // 自定义失败处理器
                .permitAll()
            )
            // 4. 启用并配置CSRF(适用于前后端分离,Token放Header)
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 忽略某些API(如有需要)
                // .ignoringRequestMatchers("/api/public/**")
            )
            // 5. 会话管理(配置最大会话数、Session固定攻击防护等)
            .sessionManagement(session -> session
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true) // 阻止新登录
            )
            // 6. 记住我功能(可选,如需则配置持久化令牌)
            .rememberMe(remember -> remember
                .tokenRepository(persistentTokenRepository())
                .userDetailsService(userDetailsService)
            )
            // 7. 异常处理(处理403、401等)
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(myAuthenticationEntryPoint()) // 未认证处理
                .accessDeniedHandler(myAccessDeniedHandler()) // 权限不足处理
            );

        return http.build();
    }

    // 注入自定义的验证码过滤器
    @Bean
    public CaptchaValidationFilter captchaValidationFilter(StringRedisTemplate redisTemplate,
                                                           AuthenticationFailureHandler failureHandler) {
        return new CaptchaValidationFilter(redisTemplate, failureHandler);
    }
    // ... 其他Bean定义(如各种Handler、PersistentTokenRepository等)
}
  

4. 高级安全加固与优化策略

策略说明参考/技术点
动态验证码难度根据风险(如IP失败次数)动态调整验证码复杂度(扭曲度、干扰线)。调用Hutool/Kaptcha API动态配置。
验证码使用次数限制一个验证码仅能使用一次,校验后立即从Redis删除,防止重放攻击。CaptchaValidationFilter校验成功后删除Key。
限流与防刷/captcha/login接口进行限流,防止暴力破解和资源耗尽。使用Spring AOP、Guava RateLimiter或Redis+Lua实现。
多因素认证(MFA)在验证码基础上,增加短信/邮箱验证码、TOTP等作为第二因素。集成Spring Security的MFA模块或自定义Provider。
风险识别与设备指纹收集客户端信息(IP、User-Agent等)生成设备指纹,用于异常登录识别。在过滤器中提取信息,并与用户历史行为对比。
安全审计与日志详细记录所有登录尝试(成功/失败)、验证码请求,便于事后分析。实现AuthenticationSuccessHandlerAuthenticationFailureHandler并记录日志。
前后端分离适配确保验证码获取、CSRF Token传递、登录接口均支持JSON格式交互。自定义AuthenticationEntryPointAccessDeniedHandler返回JSON。

总结:Spring Security中验证码的实现关键在于自定义过滤器进行前置校验,并与Redis集成实现验证码状态的分布式管理。安全加固是一个系统工程,需在验证码的基础上,结合CSRF防护、会话管理、限流、审计等多层防御手段,并根据业务需求考虑引入动态难度、MFA和风险识别等高级特性,从而构建一个健壮、安全的认证体系。