Spring Security 5 双因素认证 (短信验证码 + 图片验证码 )

在Spring Security 5中集成短信验证码 + AJ-Captcha(anji-plus/captcha) 双因素认证,且要求先通过图片验证码验证,再发送短信验证码,最后认证,需严格按顺序控制流程。以下是完整实现方案:


1. 添加依赖

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- AJ-Captcha 滑动验证码 -->
<dependency>
    <groupId>com.anji-plus</groupId>
    <artifactId>spring-boot-starter-captcha</artifactId>
    <version>1.3.0</version>
</dependency>

<!-- 短信服务SDK(以阿里云为例) -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
    <version>4.5.0</version>
</dependency>

2. 配置AJ-Captcha

application.yml中配置滑动验证码参数:

aj:
  captcha:
    captcha-type: slider
    img-width: 310
    img-height: 160
    expire-in: 5  # 验证码有效期5分钟
    watermark: 验证码保护
    aes-key: your_aes_key_123  # 可选加密密钥

3. 生成AJ-Captcha验证码

@RestController
@RequestMapping("/captcha")
public class CaptchaController {
    @Autowired
    private CaptchaService captchaService;

    @GetMapping("/generate")
    public ResponseEntity<?> generateCaptcha(HttpServletRequest request) {
        // 生成唯一Token(建议用Redis存储)
        String captchaToken = UUID.randomUUID().toString();
        request.getSession().setAttribute("CAPTCHA_TOKEN_" + captchaToken, captchaToken);
        
        // 返回Token给前端(用于后续校验)
        Map<String, String> result = new HashMap<>();
        result.put("token", captchaToken);
        return ResponseEntity.ok(result);
    }

    @PostMapping("/validate")
    public ResponseEntity<?> validateCaptcha(
        @RequestParam String token,
        @RequestParam String pointJson  // 前端返回的滑动轨迹参数
    ) {
        // 1. 从Redis/Session获取Token
        String storedToken = (String) redisTemplate.opsForValue().get("CAPTCHA_TOKEN_" + token);
        if (storedToken == null) {
            return ResponseEntity.badRequest().body("验证码已过期");
        }

        // 2. 调用AJ-Captcha服务验证轨迹
        boolean success = captchaService.validation(storedToken, pointJson);
        if (!success) {
            return ResponseEntity.badRequest().body("验证码错误");
        }

        // 3. 验证通过,清理Token
        redisTemplate.delete("CAPTCHA_TOKEN_" + token);
        return ResponseEntity.ok("验证成功");
    }
}

4. 短信验证码服务

@Service
public class SmsService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public void sendSms(String phone, String captchaToken) {
        // 1. 先验证AJ-Captcha是否已通过(从Redis/Session检查)
        String storedToken = (String) redisTemplate.opsForValue().get("CAPTCHA_TOKEN_" + captchaToken);
        if (storedToken == null) {
            throw new RuntimeException("请先通过图形验证码验证");
        }

        // 2. 生成短信验证码
        String smsCode = generateSmsCode();

        // 3. 调用阿里云发送短信
        sendSmsToAliyun(phone, smsCode);

        // 4. 存储到Redis(示例键:sms:手机号)
        redisTemplate.opsForValue().set("sms:" + phone, smsCode, 5, TimeUnit.MINUTES);
    }

    private String generateSmsCode() {
        return String.valueOf((int)((Math.random()*9+1)*100000));
    }

    private void sendSmsToAliyun(String phone, String code) {
        // 阿里云短信服务实现(需配置AccessKey)
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "AK_ID", "AK_SECRET");
        IAcsClient client = new DefaultAcsClient(profile);
        CommonRequest request = new CommonRequest();
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        request.putQueryParameter("PhoneNumbers", phone);
        request.putQueryParameter("SignName", "你的签名");
        request.putQueryParameter("TemplateCode", "SMS_123456789");
        request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");
        client.getAcsResponse(request);
    }
}

5. 自定义双因素认证流程

5.1 定义双步骤Token
// 第一步Token(仅携带手机号,待短信验证)
public class Step1Token extends AbstractAuthenticationToken {
    private String phone;
    // 构造方法、Getter/Setter
}

// 第二步Token(携带短信验证码)
public class Step2Token extends AbstractAuthenticationToken {
    private String smsCode;
    // 构造方法、Getter/Setter
}
5.2 自定义过滤器链
public class TwoFactorAuthenticationFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String uri = httpRequest.getRequestURI();

        if (uri.equals("/login/step1")) {
            // 处理第一步:手机号提交(需已通过AJ-Captcha)
            chain.doFilter(new Step1Filter(), response, chain);
        } else if (uri.equals("/login/step2")) {
            // 处理第二步:短信验证码提交
            chain.doFilter(new Step2Filter(), response, chain);
        } else {
            chain.doFilter(request, response);
        }
    }

    private class Step1Filter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
            String phone = request.getParameter("phone");
            String captchaToken = request.getParameter("captchaToken");  // 前端需传递此Token

            // 1. 验证AJ-Captcha是否已通过(从Redis/Session检查)
            String storedToken = (String) redisTemplate.opsForValue().get("CAPTCHA_TOKEN_" + captchaToken);
            if (storedToken == null) {
                throw new BadCredentialsException("请先通过图形验证码验证");
            }

            // 2. 创建临时Token(标记第一步通过)
            Step1Token token = new Step1Token(phone);
            token.setAuthenticated(true);
            SecurityContextHolder.getContext().setAuthentication(token);

            // 3. 继续过滤器链(可跳转到发送短信页面)
            filterChain.doFilter(request, response);
        }
    }

    private class Step2Filter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
            String smsCode = request.getParameter("smsCode");
            Authentication currentAuth = SecurityContextHolder.getContext().getAuthentication();
            String phone = currentAuth.getName();  // 从第一步Token获取手机号

            // 1. 验证短信验证码
            String storedCode = redisTemplate.opsForValue().get("sms:" + phone);
            if (!smsCode.equals(storedCode)) {
                throw new BadCredentialsException("短信验证码错误");
            }

            // 2. 认证成功,创建最终Token
            UsernamePasswordAuthenticationToken finalToken = 
                new UsernamePasswordAuthenticationToken(phone, null, AuthorityUtils.NO_AUTHORITIES);
            SecurityContextHolder.getContext().setAuthentication(finalToken);

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

6. 安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/captcha/generate", "/captcha/validate", "/sms/send").permitAll()
                .anyRequest().authenticated()
                .and()
            .addFilterBefore(twoFactorAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .csrf().disable();  // 实际生产环境需启用CSRF保护
    }

    @Bean
    public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() {
        return new TwoFactorAuthenticationFilter();
    }
}

7. 前端交互流程

  1. 获取AJ-Captcha配置

    • 调用/captcha/generate获取Token。
    • 初始化AJ-Captcha组件(需传递Token)。
  2. 用户滑动验证

    • 验证通过后,调用/captcha/validate提交轨迹参数。
  3. 提交手机号

    • 调用/login/step1(携带手机号和AJ-Captcha Token)。
  4. 发送短信验证码

    • 调用/sms/send(需携带AJ-Captcha Token,确保已通过图形验证)。
  5. 提交短信验证码

    • 调用/login/step2完成最终认证。

关键注意事项

  1. Token生命周期

    • AJ-Captcha的Token在验证后需立即清理(Redis/Session)。
    • 短信验证码Token需与手机号绑定,防止重复使用。
  2. 安全增强

    • 限制短信发送频率(如1分钟/次)。
    • 登录失败锁定账号(需自定义UserDetailsService)。
  3. 日志记录

    • 记录所有验证码请求和认证尝试,便于审计。

通过以上步骤,即可在Spring Security 5中实现严格的AJ-Captcha + 短信验证码双因素认证流程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值