1. 项目概述在当今移动互联网时代手机验证码登录已成为各类应用的标准配置。作为一名长期使用Spring Security的开发者我发现很多团队在集成手机验证码登录时都会遇到各种坑——要么是验证流程设计不合理要么是与Spring Security原有体系融合不彻底。今天我就来分享一个经过多个生产项目验证的解决方案让你在5分钟内快速实现这一功能。这个方案的核心在于理解Spring Security的认证流程扩展点。不同于传统的用户名密码认证手机验证码登录需要自定义三个关键组件验证码生成与校验过滤器Filter、验证码认证令牌Token以及对应的认证提供者Provider。下面我会详细拆解每个环节的实现要点。2. 核心组件设计与实现2.1 验证码过滤器设计首先需要创建一个继承自OncePerRequestFilter的验证码过滤器。这个过滤器需要完成以下工作public class SmsCodeFilter extends OncePerRequestFilter { private static final String SMS_LOGIN_URL /login/mobile; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (SMS_LOGIN_URL.equals(request.getRequestURI()) POST.equalsIgnoreCase(request.getMethod())) { try { validateCode(request); } catch (AuthenticationException e) { unsuccessfulAuthentication(request, response, e); return; } } filterChain.doFilter(request, response); } private void validateCode(HttpServletRequest request) { String mobile request.getParameter(mobile); String code request.getParameter(code); // 实际项目中这里应该调用验证码服务校验 if (!123456.equals(code)) { // 示例代码生产环境需要替换 throw new BadCredentialsException(验证码错误); } } private void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException { SecurityContextHolder.clearContext(); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, failed.getMessage()); } }关键点过滤器应该只拦截特定的验证码登录URL避免影响其他认证流程。验证失败时需要清除SecurityContext并返回401状态码。2.2 认证令牌与认证提供者接下来需要创建自定义的认证令牌和提供者public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; // 这里存储手机号 private Object credentials; // 这里存储验证码 public SmsCodeAuthenticationToken(Object principal, Object credentials) { super(null); this.principal principal; this.credentials credentials; setAuthenticated(false); } // 实现父类抽象方法 Override public Object getCredentials() { return this.credentials; } Override public Object getPrincipal() { return this.principal; } } Service public class SmsCodeAuthenticationProvider implements AuthenticationProvider { Autowired private UserDetailsService userDetailsService; Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken (SmsCodeAuthenticationToken) authentication; String mobile (String) authenticationToken.getPrincipal(); UserDetails user userDetailsService.loadUserByUsername(mobile); if (user null) { throw new UsernameNotFoundException(手机号未注册); } SmsCodeAuthenticationToken result new SmsCodeAuthenticationToken( user, null, user.getAuthorities()); result.setDetails(authenticationToken.getDetails()); return result; } Override public boolean supports(Class? authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } }注意认证提供者中我们只验证用户是否存在因为验证码已经在过滤器中校验过了。这是职责分离的重要设计。3. Spring Security配置集成3.1 安全配置类将上述组件集成到Spring Security配置中Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Autowired private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider; Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(smsCodeFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers(/code/sms).permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage(/login) .loginProcessingUrl(/login/form) .defaultSuccessUrl(/) .and() .logout() .logoutUrl(/logout) .logoutSuccessUrl(/login); } Bean public SmsCodeFilter smsCodeFilter() { return new SmsCodeFilter(); } Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(smsCodeAuthenticationProvider); } }3.2 验证码发送接口虽然不属于Spring Security核心流程但验证码发送是完整功能的一部分RestController public class SmsCodeController { GetMapping(/code/sms) public Result sendSmsCode(RequestParam String mobile) { // 实际项目中这里应该调用短信服务发送验证码 String code generateRandomCode(); // 将验证码存入缓存设置5分钟过期 redisTemplate.opsForValue().set(sms_code: mobile, code, 5, TimeUnit.MINUTES); return Result.success(验证码已发送); } private String generateRandomCode() { return String.valueOf(new Random().nextInt(899999) 100000); } }4. 前端对接与测试4.1 前端登录表单手机验证码登录需要特定的表单form action/login/mobile methodpost input typetext namemobile placeholder手机号 input typetext namecode placeholder验证码 button typebutton onclicksendCode()获取验证码/button button typesubmit登录/button /form script function sendCode() { const mobile document.querySelector(input[namemobile]).value; fetch(/code/sms?mobile mobile) .then(response response.json()) .then(data alert(data.message)); } /script4.2 测试要点测试时需要关注以下几个关键点验证码有效期验证5分钟后应该失效同一手机号验证码防重放攻击验证码错误次数限制与原有表单登录的兼容性5. 生产环境注意事项在实际项目中还需要考虑以下问题验证码安全验证码长度建议6位包含数字即可设置合理的发送频率限制如60秒内只能发送一次同一IP/设备发送次数限制性能优化验证码校验使用Redis等内存数据库考虑使用本地缓存减少Redis访问异常处理短信服务不可用时的降级方案验证码输入错误时的友好提示日志记录记录验证码发送和验证日志关键操作需要审计日志我在实际项目中遇到过的一个典型问题当系统同时存在表单登录和验证码登录时如果用户先尝试表单登录失败再尝试验证码登录有时会出现认证状态混乱。解决方案是在过滤器中明确区分两种认证流程确保它们互不干扰。