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的配置中,并实施其他安全加固措施。
关键配置点:
- 注册过滤器:在
SecurityFilterChain配置中,使用addFilterBefore将CaptchaValidationFilter添加到UsernamePasswordAuthenticationFilter之前。 - CSRF防护:必须启用CSRF防护。对于前后端分离项目,可以将CSRF Token放在请求头(如
X-CSRF-TOKEN)中传递,并配置csrfTokenRepository。传统的表单登录则依赖_csrf参数。 - 会话管理:对于分布式系统,建议将Session存储到Redis中(使用
spring-session-data-redis),实现Session共享和无状态化管理的平衡。 - 登录失败处理:配置自定义的
AuthenticationFailureHandler,以返回结构化的JSON错误信息,而不是默认的跳转。 - 账户安全:集成账户锁定策略(如连续5次密码错误锁定15分钟)和密码强度策略,这些可以通过自定义
UserDetailsService和DaoAuthenticationProvider来实现。
示例:核心安全配置(部分)
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等)生成设备指纹,用于异常登录识别。 | 在过滤器中提取信息,并与用户历史行为对比。 |
| 安全审计与日志 | 详细记录所有登录尝试(成功/失败)、验证码请求,便于事后分析。 | 实现AuthenticationSuccessHandler、AuthenticationFailureHandler并记录日志。 |
| 前后端分离适配 | 确保验证码获取、CSRF Token传递、登录接口均支持JSON格式交互。 | 自定义AuthenticationEntryPoint和AccessDeniedHandler返回JSON。 |
总结:Spring Security中验证码的实现关键在于自定义过滤器进行前置校验,并与Redis集成实现验证码状态的分布式管理。安全加固是一个系统工程,需在验证码的基础上,结合CSRF防护、会话管理、限流、审计等多层防御手段,并根据业务需求考虑引入动态难度、MFA和风险识别等高级特性,从而构建一个健壮、安全的认证体系。