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 调用后已被删除。这种设计有效防止了重放攻击,同时优化了系统性能