使用TIANAI-CAPTCHA进行行为验证码的生成和缓存的二次校验

发布于:2025-07-18 ⋅ 阅读:(21) ⋅ 点赞:(0)
1.导入依赖:
<dependency>
    <groupId>cloud.tianai.captcha</groupId>
    <artifactId>tianai-captcha-springboot-starter</artifactId>
    <version>1.5.2</version>
</dependency>
2.在application.yml中配置验证码相关配置:
# 滑块验证码配置, 详细请看 cloud.tianai.captcha.autoconfiguration.ImageCaptchaProperties 类
captcha:
  # 如果项目中使用到了redis,滑块验证码会自动把验证码数据存到redis中, 这里配置redis的key的前缀,默认是captcha:slider
  prefix: captcha
  # 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整
  expire:
    # 默认缓存时间 2分钟
    default: 10000
    # 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些
    WORD_IMAGE_CLICK: 20000
  # 使用加载系统自带的资源, 默认是 false(这里系统的默认资源包含 滑动验证码模板/旋转验证码模板,如果想使用系统的模板,这里设置为true)
  init-default-resource: true
  # 缓存控制, 默认为false不开启
  local-cache-enabled: false
  # 缓存开启后,验证码会提前缓存一些生成好的验证数据, 默认是20
  local-cache-size: 20
  # 缓存开启后,缓存拉取失败后等待时间 默认是 5秒钟
  local-cache-wait-time: 5000
  # 缓存开启后,缓存检查间隔 默认是2秒钟
  local-cache-period: 2000
  # 配置字体包,供文字点选验证码使用,可以配置多个,不配置使用默认的字体
  font-path:
    - classpath:font/SimHei.ttf
  secondary:
    # 二次验证, 默认false 不开启
    enabled: false
    # 二次验证过期时间, 默认 2分钟
    expire: 120000
    # 二次验证缓存key前缀,默认是 captcha:secondary
    keyPrefix: "captcha:secondary"
3.接入springboot进行验证码的开发:
controller:
@RestController
@RequestMapping("/system/captcha")
public class CaptchaController {

    @Resource
    private CaptchaService captchaService;

    @GetMapping("/get-slider-image")
    @ApiOperation("生成滑块验证码图片")
    public CommonResult<CaptchaResponse<ImageCaptchaVO>> getSliderCaptchaImage() {
        return success(captchaService.getSliderCaptchaImage());
    }

    @PostMapping("/check")
    @ApiOperation("滑块验证码确认")
    public CommonResult<Boolean> checkCaptchaImage(HttpServletRequest request,
                                                   @Valid @RequestBody CaptchaImageVo captchaImageVo) {
        return success(captchaService.checkCaptchaImage(captchaImageVo));
    }

}
service:
/**
 * 验证码 Service 接口
 */
public interface CaptchaService {

    /**
     * 是否开启图片验证码
     *
     * @return 是否
     */
    Boolean isCaptchaEnable();

    /**
     * 获得 uuid 对应的验证码
     *
     * @param uuid 验证码编号
     * @return 验证码
     */
    String getCaptchaCode(String uuid);

    /**
     * 删除 uuid 对应的验证码
     *
     * @param uuid 验证码编号
     */
    void deleteCaptchaCode(String uuid);

    /**
     * 图片验证码验证
     *
     */
    Boolean checkCaptchaImage(CaptchaImageVo captchaImageVo);

    /**
     * 获得验证码图片
     *
     * @return 验证码图片
     */
    CaptchaResponse<ImageCaptchaVO> getSliderCaptchaImage();

    /**
     * 判断对应的滑动验证码是否通过
     *
     */
    Boolean alreadyValid(String uuid) ;
}
impl:
/**
 * 验证码 Service 实现类
 */
@Service
@Slf4j
public class CaptchaServiceImpl implements CaptchaService {

    @Value("${captcha.expire.defult}")
    private Duration timeout;

    @Resource
    private MyResourceStoreProperties myResourceStoreProperties;

    @Resource
    private CaptchaRedisDAO captchaRedisDAO;

    @Resource
    private ImageCaptchaApplication application;

    @Resource
    private CacheStore cacheStore;

    @Resource
    private ImageCaptchaProperties imageCaptchaProperties;


    @Override
    public String getCaptchaCode(String uuid) {
        return captchaRedisDAO.get(uuid);
    }

    @Override
    public void deleteCaptchaCode(String uuid) {
        captchaRedisDAO.delete(uuid);
    }

    @Override
    public Boolean checkCaptchaImage(CaptchaImageVo captchaImageVo) {
        Boolean isPass = application.matching(captchaImageVo.getId(), captchaImageVo.getImageCaptchaTrack());
        captchaRedisDAO.set(captchaImageVo.getId(), isPass.toString(), timeout);
        return isPass;
    }

    @Override
    public Boolean alreadyValid(String uuid) {
        if (captchaRedisDAO.get(uuid) != null) {
            boolean result = Boolean.parseBoolean(captchaRedisDAO.get(uuid));
            captchaRedisDAO.delete(uuid);
            return result;
        }
        return false;
    }

    @Override
    public CaptchaResponse<ImageCaptchaVO> getSliderCaptchaImage() {
        //加载模板
        myResourceStoreProperties.MyResourceStore();
        CaptchaResponse<ImageCaptchaVO> response = application.generateCaptcha(CaptchaTypeConstant.SLIDER);
        Map<String, Object> data = cacheStore.getCache(imageCaptchaProperties.getPrefix().concat(":").concat(response.getId()));
        //动态设置偏移容错
        data.put("tolerant", 0.2);
        cacheStore.setCache(imageCaptchaProperties.getPrefix().concat(":").concat(response.getId()), data, 20000L, TimeUnit.MILLISECONDS);
        return response;
    }


}
注入的自定义类:
@Component
public class MyResourceStoreProperties extends DefaultResourceStore {

    public void MyResourceStore() {
        // 滑块验证码 模板 (系统内置)
        Map<String, Resource> template1 = new HashMap<>(4);
        template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
        template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
        template1.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/matrix.png")));
        Map<String, Resource> template2 = new HashMap<>(4);
        template2.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
        template2.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
        template2.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/matrix.png")));
        // 1. 添加一些模板
        addTemplate(CaptchaTypeConstant.SLIDER, template1);
        addTemplate(CaptchaTypeConstant.SLIDER, template2);

        // 2. 添加自定义背景图片
        int dayOfWeek=DateUtil.dayOfWeek(new Date())-1;
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/slider.jpg"));
        //addResource(CaptchaTypeConstant.SLIDER, new Resource("URL", "https://soarway-fangpiao-backup-dev.oss-cn-hangzhou.aliyuncs.com/slider-"+dayOfWeek+".jpg"));
    }

}
@Repository
public class CaptchaRedisDAO {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public String get(String uuid) {
        String redisKey = formatKey(uuid);
        return stringRedisTemplate.opsForValue().get(redisKey);
    }

    public void set(String uuid, String code, Duration timeout) {
        String redisKey = formatKey(uuid);
        stringRedisTemplate.opsForValue().set(redisKey, code, timeout);
    }

    public void delete(String uuid) {
        String redisKey = formatKey(uuid);
        stringRedisTemplate.delete(redisKey);
    }

    private static String formatKey(String uuid) {
        return String.format(CAPTCHA_CODE.getKeyTemplate(), uuid);
    }

}
 4.验证码的双重验证:

防止恶意劫持和重放攻击

第一次校验:

@Override
    public Boolean checkCaptchaImage(CaptchaImageVo captchaImageVo) {
        Boolean isPass = application.matching(captchaImageVo.getId(), captchaImageVo.getImageCaptchaTrack());
        captchaRedisDAO.set(captchaImageVo.getId(), isPass.toString(), timeout);
        return isPass;
    }

    用户->>前端:  1.拖动滑块完成验证
    前端->>后端: 发送滑块ID+轨迹数据 (checkCaptchaImage)
    后端->>后端: 计算轨迹匹配度
    后端->>Redis: 存储验证结果 (key:滑块ID, value:true/false)
    后端->>前端: 返回实时结果

第二次校验:

@Override
    public Boolean alreadyValid(String uuid) {
        if (captchaRedisDAO.get(uuid) != null) {
            boolean result = Boolean.parseBoolean(captchaRedisDAO.get(uuid));
            captchaRedisDAO.delete(uuid);
            return result;
        }
        return false;
    }

    用户->>前端: 2. 提交登录表单
    前端->>后端: 发送表单数据+滑块ID (alreadyValid)
    后端->>Redis: 读取验证结果
    Redis->>后端: 返回预存结果
    后端->>Redis: 删除该滑块ID的缓存
    后端->>前端: 返回最终验证结果

这样即使黑客获取了第一次验证成功的请求,也无法重复使用该滑块ID进行二次验证,因为结果在 alreadyValid 调用后已被删除。这种设计有效防止了重放攻击,同时优化了系统性能


网站公告

今日签到

点亮在社区的每一天
去签到