基于Spring Security 的 SecurityFilterChain 增加短信验证码登录

基于 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. 登录流程说明

  1. 前端调用 /send-sms-code 接口,传入手机号,获取验证码
  2. 前端调用 /login/sms 接口,传入手机号和验证码
  3. SmsCodeAuthenticationFilter 拦截请求,封装 SmsCodeAuthenticationToken 并提交给 AuthenticationManager
  4. AuthenticationManager 调用 SmsCodeAuthenticationProvider 进行认证
    • 校验验证码有效性(通过 SmsCodeService
    • 查询用户信息(通过 UserDetailsService
  5. 认证成功:生成已认证令牌,触发 AuthenticationSuccessHandler(如返回 JWT)
  6. 认证失败:触发 AuthenticationFailureHandler(返回错误信息)

4. 关键注意事项

  • 验证码安全

    • 使用 Redis 存储验证码
    • 设置合理的过期时间(通常 5-10 分钟)
    • 校验后立即删除,防止重复使用
  • 手机号校验

    • 添加手机号格式验证
    • 防止恶意请求和短信轰炸
  • 限流防护

    • 对发送验证码接口添加限流(如 Redis 计数器)
    • 限制同一手机号发送频率
    • 防止短信轰炸攻击
  • 用户信息查询

    • UserDetailsService 需实现根据手机号查询用户的逻辑
    • 考虑用户注册流程,处理未注册手机号的情况
  • 其他安全考虑

    • 在生产环境中适当配置 CSRF 保护
    • 考虑验证码复杂度要求
    • 添加图形验证码等二次验证机制防止自动化攻击

通过以上步骤,即可在 Spring Security 中完整实现短信验证码登录功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值