使用TIANAI-CAPTCHA进行行为验证码的生成和缓存的二次校验
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:sliderprefix: captcha# 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整expire:# 默认缓存时间 2分钟default: 10000# 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些WORD_IMAGE_CLICK: 20000# 使用加载系统自带的资源, 默认是 false(这里系统的默认资源包含 滑动验证码模板/旋转验证码模板,如果想使用系统的模板,这里设置为true)init-default-resource: true# 缓存控制, 默认为false不开启local-cache-enabled: false# 缓存开启后,验证码会提前缓存一些生成好的验证数据, 默认是20local-cache-size: 20# 缓存开启后,缓存拉取失败后等待时间 默认是 5秒钟local-cache-wait-time: 5000# 缓存开启后,缓存检查间隔 默认是2秒钟local-cache-period: 2000# 配置字体包,供文字点选验证码使用,可以配置多个,不配置使用默认的字体font-path:- classpath:font/SimHei.ttfsecondary:# 二次验证, 默认false 不开启enabled: false# 二次验证过期时间, 默认 2分钟expire: 120000# 二次验证缓存key前缀,默认是 captcha:secondarykeyPrefix: "captcha:secondary"
3.接入springboot进行验证码的开发:
controller:
@RestController
@RequestMapping("/system/captcha")
public class CaptchaController {@Resourceprivate 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;@Resourceprivate MyResourceStoreProperties myResourceStoreProperties;@Resourceprivate CaptchaRedisDAO captchaRedisDAO;@Resourceprivate ImageCaptchaApplication application;@Resourceprivate CacheStore cacheStore;@Resourceprivate ImageCaptchaProperties imageCaptchaProperties;@Overridepublic String getCaptchaCode(String uuid) {return captchaRedisDAO.get(uuid);}@Overridepublic void deleteCaptchaCode(String uuid) {captchaRedisDAO.delete(uuid);}@Overridepublic Boolean checkCaptchaImage(CaptchaImageVo captchaImageVo) {Boolean isPass = application.matching(captchaImageVo.getId(), captchaImageVo.getImageCaptchaTrack());captchaRedisDAO.set(captchaImageVo.getId(), isPass.toString(), timeout);return isPass;}@Overridepublic Boolean alreadyValid(String uuid) {if (captchaRedisDAO.get(uuid) != null) {boolean result = Boolean.parseBoolean(captchaRedisDAO.get(uuid));captchaRedisDAO.delete(uuid);return result;}return false;}@Overridepublic 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 {@Resourceprivate 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.验证码的双重验证:
防止恶意劫持和重放攻击
第一次校验:
@Overridepublic 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)
后端->>前端: 返回实时结果
第二次校验:
@Overridepublic 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 调用后已被删除。这种设计有效防止了重放攻击,同时优化了系统性能