基于 Spring Security 实现短信验证码登录
目录
1. 核心原理
短信验证码登录的核心流程是:用户输入手机号和短信验证码,系统验证验证码有效性后,生成认证令牌(Token)并完成登录。需要自定义以下组件:
- 短信验证码过滤器:拦截登录请求
- 短信认证令牌:封装手机号、验证码等信息
- 短信认证提供者:验证令牌有效性
- 验证码存储/校验逻辑:如 Redis 存储验证码
2. 具体实现步骤
步骤1:定义短信认证令牌(SmsCodeAuthenticationToken)
继承 AbstractAuthenticationToken,封装认证信息(未认证/已认证状态)。
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
// 未认证时存储手机号,认证后存储用户信息
private final Object principal;
// 存储短信验证码
private final String credentials;
// 未认证构造器(过滤器中使用)
public SmsCodeAuthenticationToken(String mobile, String code) {
super(null);
this.principal = mobile;
this.credentials = code;
setAuthenticated(false); // 初始为未认证
}
// 已认证构造器(认证成功后使用)
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null; // 认证后无需存储验证码
setAuthenticated(true); // 标记为已认证
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
}
步骤2:实现短信验证码过滤器(SmsCodeAuthenticationFilter)
继承 AbstractAuthenticationProcessingFilter,拦截短信登录请求,提取手机号和验证码,封装为 SmsCodeAuthenticationToken 并提交给 AuthenticationManager。
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 拦截的请求路径和方法(如POST /login/sms)
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/sms", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
// 从请求中提取手机号和验证码(根据实际参数名调整)
String mobile = request.getParameter("mobile");
String code = request.getParameter("code");
// 校验参数合法性
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(code)) {
throw new BadCredentialsException("手机号或验证码不能为空");
}
// 封装为未认证的令牌
SmsCodeAuthenticationToken token = new SmsCodeAuthenticationToken(mobile, code);
// 设置请求信息(可选,供后续使用)
setDetails(request, token);
// 提交给AuthenticationManager认证
return this.getAuthenticationManager().authenticate(token);
}
// 设置请求详情(如IP、Session等)
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
步骤3:实现短信认证提供者(SmsCodeAuthenticationProvider)
实现 AuthenticationProvider,负责验证 SmsCodeAuthenticationToken 的有效性(验证码校验、用户查询等)。
@Component
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService; // 自定义用户服务(根据手机号查用户)
@Autowired
private SmsCodeService smsCodeService; // 验证码服务(校验验证码)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 1. 从令牌中获取手机号和验证码
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
String mobile = (String) token.getPrincipal();
String code = (String) token.getCredentials();
// 2. 校验短信验证码(从Redis等存储中查询并比对)
if (!smsCodeService.verifyCode(mobile, code)) {
throw new BadCredentialsException("验证码错误或已过期");
}
// 3. 根据手机号查询用户信息(需自定义UserDetailsService实现)
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
if (userDetails == null) {
throw new UsernameNotFoundException("手机号未注册");
}
// 4. 生成已认证的令牌(包含用户信息和权限)
SmsCodeAuthenticationToken authenticatedToken = new SmsCodeAuthenticationToken(
userDetails, userDetails.getAuthorities()
);
authenticatedToken.setDetails(token.getDetails()); // 传递请求详情
return authenticatedToken;
}
// 支持处理SmsCodeAuthenticationToken类型的令牌
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
步骤4:实现验证码服务(SmsCodeService)
负责生成、存储(如 Redis)、校验短信验证码。
@Service
public class SmsCodeService {
@Autowired
private StringRedisTemplate redisTemplate;
// 验证码过期时间(5分钟)
private static final long CODE_EXPIRE = 5 * 60;
// 生成并存储验证码
public String generateCode(String mobile) {
String code = RandomStringUtils.randomNumeric(6); // 生成6位数字验证码
// 存储到Redis,键:sms:code:{mobile},值:code,过期时间5分钟
redisTemplate.opsForValue().set(
"sms:code:" + mobile,
code,
CODE_EXPIRE,
TimeUnit.SECONDS
);
return code;
}
// 校验验证码
public boolean verifyCode(String mobile, String code) {
String key = "sms:code:" + mobile;
String storedCode = redisTemplate.opsForValue().get(key);
if (storedCode == null) {
return false; // 验证码不存在或已过期
}
// 校验通过后删除验证码(防止重复使用)
if (storedCode.equals(code)) {
redisTemplate.delete(key);
return true;
}
return false;
}
}
步骤5:配置 Spring Security
将自定义过滤器和认证提供者整合到 Spring Security 流程中。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;
@Autowired
private AuthenticationSuccessHandler successHandler; // 自定义登录成功处理器
@Autowired
private AuthenticationFailureHandler failureHandler; // 自定义登录失败处理器
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/sms", "/send-sms-code").permitAll() // 放行登录和发短信接口
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable()) // 简化示例,实际按需开启
.formLogin(form -> form.disable()) // 禁用默认表单登录
.apply(new SmsCodeLoginConfigurer()); // 注册短信登录配置
return http.build();
}
// 配置认证管理器,添加短信认证提供者
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
AuthenticationManager manager = config.getAuthenticationManager();
// 手动添加自定义提供者(若Spring未自动注入)
if (manager instanceof ProviderManager providerManager) {
providerManager.getProviders().add(smsCodeAuthenticationProvider);
}
return manager;
}
// 短信登录配置器(整合过滤器和处理器)
public class SmsCodeLoginConfigurer extends AbstractHttpConfigurer<SmsCodeLoginConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
// 设置AuthenticationManager
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 设置成功/失败处理器
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationFailureHandler(failureHandler);
// 将过滤器添加到UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
}
步骤6:实现发送验证码接口
提供一个接口供前端获取短信验证码(需结合实际短信服务)。
@RestController
public class SmsController {
@Autowired
private SmsCodeService smsCodeService;
@PostMapping("/send-sms-code")
public String sendSmsCode(@RequestParam String mobile) {
// 1. 校验手机号格式(省略)
// 2. 生成验证码并存储
String code = smsCodeService.generateCode(mobile);
// 3. 调用短信服务商API发送验证码(示例省略)
return "验证码已发送,5分钟内有效";
}
}
3. 登录流程说明
- 前端调用
/send-sms-code接口,传入手机号,获取验证码 - 前端调用
/login/sms接口,传入手机号和验证码 - SmsCodeAuthenticationFilter 拦截请求,封装
SmsCodeAuthenticationToken并提交给AuthenticationManager - AuthenticationManager 调用 SmsCodeAuthenticationProvider 进行认证:
- 校验验证码有效性(通过
SmsCodeService) - 查询用户信息(通过
UserDetailsService)
- 校验验证码有效性(通过
- 认证成功:生成已认证令牌,触发
AuthenticationSuccessHandler(如返回 JWT) - 认证失败:触发
AuthenticationFailureHandler(返回错误信息)
4. 关键注意事项
-
验证码安全:
- 使用 Redis 存储验证码
- 设置合理的过期时间(通常 5-10 分钟)
- 校验后立即删除,防止重复使用
-
手机号校验:
- 添加手机号格式验证
- 防止恶意请求和短信轰炸
-
限流防护:
- 对发送验证码接口添加限流(如 Redis 计数器)
- 限制同一手机号发送频率
- 防止短信轰炸攻击
-
用户信息查询:
UserDetailsService需实现根据手机号查询用户的逻辑- 考虑用户注册流程,处理未注册手机号的情况
-
其他安全考虑:
- 在生产环境中适当配置 CSRF 保护
- 考虑验证码复杂度要求
- 添加图形验证码等二次验证机制防止自动化攻击
通过以上步骤,即可在 Spring Security 中完整实现短信验证码登录功能。
9463

被折叠的 条评论
为什么被折叠?



