关注我的公众号:【编程朝花夕拾】,可获取首发内容。
01 引言
行为验证码已经慢慢变成了开发验证的必须行为,之前分享过TIANAI-CAPTCHA天爱验证码的SpringBoot用法,非常简单。
但是,这两天需要在传统的SpringMVC项目中接入,发现使用并不简单。看看我都踩了那些坑?
02 天爱验证码简介
还是简单介绍一下tianai-captcha项目。TIANAI-CAPTCHA下简称TAC 是一个开源的行为验证码工具,支持多种验证码类型,分别有java、go等语言的实现。开源版默认提供了 滑块验证码、旋转验证码、文字点选验证码、滑动还原验证码等。
后端的接入方式主要有两种:
SpringBoot项目- 传统项目
Gitee地址:gitee.com/dromara/tia…
在线体验地址:captcha.tianai.cloud/
在线文档:doc.captcha.tianai.cloud/
03 传统项目接入
SpringBoot项目接入太简单了,包括二次验证,配置一下参数即可完成。而传统项目的接入,部分功能需要自行实现。
在众多验证码类型中,我选择了常用的滑动验证码,并自定义图片。
3.1 依赖引入
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.5.5</version>
</dependency>
3.2 创建验证码对象
由于线程安全的缘故,官方建议ImageCaptchaApplication创建一次。
这里创建了一个ImageCaptchaProxy用来管理ImageCaptchaApplication,以保证创建一次。
public class ImageCaptchaProxy {
private ImageCaptchaApplication imageCaptchaApplication;
@Autowired
private CacheService cacheService;
@PostConstruct
public void initialize() {
imageCaptchaApplication = TACBuilder.builder()
// 设置资源存储器,默认是 LocalMemoryResourceStore
.setResourceStore(new LocalMemoryResourceStore())
.setCacheStore(new RedisCacheStore(cacheService))
// 加载系统自带的默认资源(系统内置了几个滑块验证码缺口模板图,调用此函数加载)
.addDefaultTemplate()
// 设置验证码过期时间, 单位毫秒, default 是默认验证码过期时间,当前设置为10秒,
// 可以自定义某些验证码类型单独的过期时间, 比如把点选验证码的过期时间设置为60秒
.expire("default", 10000L)
// 设置拦截器,默认是 EmptyCaptchaInterceptor.INSTANCE
.setInterceptor(EmptyCaptchaInterceptor.INSTANCE)
// 添加验证码背景图片
// arg1 验证码类型(SLIDER、WORD_IMAGE_CLICK、ROTATE、CONCAT),
// arg2 验证码背景图片资源
// 背景图宽高为 600x360
.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "META-INF/cut-image/1.jpg"))
.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "META-INF/cut-image/2.jpg"))
// 图片转换器,默认是将图片转换成base64格式, 背景图为jpg, 模板图为png, 如果想要扩展,可替换成自己实现的
.setTransform(new Base64ImageTransform())
.build();
}
/**
* @Description: 获取验证码信息
*
* @Author: ws
* @Date: 2026/3/12 10:34
**/
public ApiResponse<ImageCaptchaVO> getImageCaptcha() {
return imageCaptchaApplication.generateCaptcha(CaptchaTypeConstant.SLIDER);
}
/**
* @Description: 验证验证码信息
*
* @Author: ws
* @Date: 2026/3/12 10:34
**/
public ApiResponse verifyImageCaptcha(String captchaId, ImageCaptchaTrack track) {
return imageCaptchaApplication.matching(captchaId, track);
}
}
这里对象的创建大家可以根据自己的实际情况删减配置项,这里有几个配置项非常重要,是官方保留的扩展。
setResourceStore()
这里设置资源存储器,默认是 LocalMemoryResourceStore。也就是将资源也就是滑动的图片信息,放在内存中的。
直接使用内存没有问题,直接存储在本地缓存。这里也可以统一放在其他介质中,如Redis。SpringBoot版官方提供了RedisResourceStore的类,但是传统的项目并没有,如果想要放在缓存中统一使用,就需要实现CrudResourceStore接口即可。直接将SpringBoot版中的复制过来,改成自己的Redis客户端即可。
这个参数设置不设置不重要,就看你资源要不要共享。
setCacheStore()
用来存储被切割的背景图以及拼接的碎片,功能上主要用来验证码的验证。这个非常重要,默认还是内存,在单节点项目中没有问题,一旦涉及多节点,必然验证不通过。
这是我踩的最大的坑。
多节点的环境中,需要通过保存在其他介质中,如Redis。需要实现CacheStore接口,直接将LocalCacheStore复制过来,换成Redis客户单即可。
这里使用构造函数将Redis客户单传递进来。
public class RedisCacheStore implements CacheStore {
private CacheService cacheService;
public RedisCacheStore(CacheService cacheService) {
this.cacheService = cacheService;
}
@Override
public AnyMap getCache(String key) {
String json = cacheService.getString(key);
return JSON.parseObject(json, AnyMap.class);
}
@Override
public AnyMap getAndRemoveCache(String key) {
AnyMap anyMap = getCache(key);
cacheService.delete(key);
return anyMap;
}
@Override
public boolean setCache(String key, AnyMap data, Long expire, TimeUnit timeUnit) {
cacheService.delete(key);
cacheService.setExpireSecV2(new BaseExpireSec(key, JSON.toJSONString(data), (int) timeUnit.toSeconds(expire)));
return true;
}
@Override
public Long incr(String key, long delta, Long expire, TimeUnit timeUnit) {
Map<String, Object> value = getAndRemoveCache(key);
if (value != null) {
Long incr = (Long) value.get("___incr___");
if (incr == null) {
incr = 0L;
}
incr += delta;
setCache(key, AnyMap.of(Collections.singletonMap("___incr___", incr)), expire, timeUnit);
return incr;
}
setCache(key, AnyMap.of(Collections.singletonMap("___incr___", delta)), expire, timeUnit);
return delta;
}
@Override
public Long getLong(String key) {
Map<String, Object> stringObjectMap = getCache(key);
if (stringObjectMap != null) {
return (Long) stringObjectMap.get("___incr___");
}
return null;
}
@Override
public void close() throws Exception {
}
addResource()
添加验证码背景图片,也就是自定义图片。滑动验证码使用CaptchaTypeConstant.SLIDER类型即可,类型后面跟图片的具体位置。这里的图片背景的宽高必须为 600x360,否则无法加载。
支持类型:
新增方法
为了对外不暴露imageCaptchaApplication,我特意增加了获取验证码和验证验证码的方法。
getImageCaptcha()verifyImageCaptcha()
3.3 前端页面
传统项目使用的是JSP技术,只能通过引入js的方式接入。
官网上说,可以使用load.js,我使用之后没有效果。这是踩的第二个坑。
没有使用load.js直接中https://gitee.com/tianai/tianai-captcha-demo静态资源,同时需要修改页面初始化的方法。
tianai-captcha-demo需要复制项目可以直接访问的文件下。
我直接放在了webapp下
然后引入项目
初始化触发
new TAC({
// 生成接口
requestCaptchaDataUrl: "${ctx}/captcha/showImageCaptcha",
// 验证接口
validCaptchaUrl: "${ctx}/captcha/verifyImageCaptcha",
// 验证码绑定的div块 (必选项,必须配置)
bindEl: "#captcha-box",
// 验证成功回调函数(必选项,必须配置)
validSuccess: (res, c, t) => {
console.log("验证码验证成功回调...");
// 销毁验证码组件
t.destroyWindow();
$.LAYER.close();
// 调用具体的业务方法
loginPreCheck(res.data.token);
},
// 刷新按钮回调事件
btnRefreshFun: (el, tac) => {
console.log("刷新按钮触发事件...");
tac.reloadCaptcha();
},
// 关闭按钮回调事件
btnCloseFun: (el, tac) => {
console.log("关闭按钮触发事件...");
tac.destroyWindow();
$.LAYER.close();
}
}, {
// 一些样式配置
logoUrl: "/images/logo.png",
btnUrl: "/images/btn.png"
}).init();
代码中$.LAYER.close()是为了关闭遮罩效果,公司里面的小插件可以忽略。
这里要说的样式的配置。配置项如图,搭建可以自由选择,我这里配置logo和按钮的图片
3.4 服务端
服务端比较简单,就是提供showImageCaptcha()和verifyImageCaptcha()即可。
@Controller
@RequestMapping("/captcha")
public class ImageCaptchaController {
@Autowired
private ImageCaptchaProxy imageCaptchaProxy;
@Autowired
private CacheService cacheService;
/**
* @Description: 显示图片验证码
*
* @Author: ws
* @Date: 2026/3/12 10:09
**/
@RequestMapping("/showImageCaptcha")
@ResponseBody
public ApiResponse<ImageCaptchaVO> showImageCaptcha() {
return imageCaptchaProxy.getImageCaptcha();
}
/**
* @Description: 验证验证码
*
* @Author: ws
* @Date: 2026/3/12 10:32
**/
@RequestMapping("/verifyImageCaptcha")
@ResponseBody
public ApiResponse verifyImageCaptcha(@RequestBody ImageCaptchaDto imageCaptchaDto, HttpServletRequest request) {
ApiResponse apiResponse = imageCaptchaProxy.verifyImageCaptcha(imageCaptchaDto.getId(), imageCaptchaDto.getData());
if (apiResponse.isSuccess()) {
// 生成唯一的token
String token = UUIDUtil.getUUID();
cacheService.sadd(AccountConstant.VERIFY_CODE_TOKEN, token);
cacheService.setDateSecJustKey(AccountConstant.VERIFY_CODE_TOKEN, DateUtils.getDateEnd(new Date()));
Map<String, Object> map = new HashMap<>();
map.put("token", token);
apiResponse.setData(map);
}
return apiResponse;
}
}
这里需要注意的有两点:
- 验证参数自定义
- 二次验证
验证参数自定义
verifyImageCaptcha方法接受参数是我们自定义的。官方也给出了Demo
自定义参数并使用@RequestBody接受,说明前端数据是一个json数据。
二次验证
校验过程我们会发现和业务没有任何关系,如登录系统。验证通过才会调用登录接口,如果别人直接调用登录接口绕过页面,那么行为验证码就形同虚设,也是等保所不能容忍的。
所以我们需要在登录后再次验证。实现也很简单,验证通过之后,生成唯一标识token并存入缓存,只要登录的时候携带此token,服务端验证缓存中是否存在即可。
04 小结
单体项目测试的时候总会发现一切完好,但是到了分布式环境、多节点环境总会出现数据丢失的问题,会让框架的使用变的复杂。理解其原理,修修补补,马上完好如初!