开源免费!传统项目也可以接入天爱验证码(TAC),坑我来填

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

行为验证码已经慢慢变成了开发验证的必须行为,之前分享过TIANAI-CAPTCHA天爱验证码的SpringBoot用法,非常简单。

但是,这两天需要在传统的SpringMVC项目中接入,发现使用并不简单。看看我都踩了那些坑?

02 天爱验证码简介

还是简单介绍一下tianai-captcha项目。TIANAI-CAPTCHA下简称TAC 是一个开源的行为验证码工具,支持多种验证码类型,分别有javago等语言的实现。开源版默认提供了 滑块验证码、旋转验证码、文字点选验证码、滑动还原验证码等。

后端的接入方式主要有两种:

  • SpringBoot项目
  • 传统项目

Gitee地址:gitee.com/dromara/tia...

在线体验地址:captcha.tianai.cloud/

在线文档:doc.captcha.tianai.cloud/

03 传统项目接入

SpringBoot项目接入太简单了,包括二次验证,配置一下参数即可完成。而传统项目的接入,部分功能需要自行实现。

在众多验证码类型中,我选择了常用的滑动验证码,并自定义图片。

3.1 依赖引入

xml 复制代码
<dependency>
    <groupId>cloud.tianai.captcha</groupId>
    <artifactId>tianai-captcha</artifactId>
    <version>1.5.5</version>
</dependency>

3.2 创建验证码对象

由于线程安全的缘故,官方建议ImageCaptchaApplication创建一次。

这里创建了一个ImageCaptchaProxy用来管理ImageCaptchaApplication,以保证创建一次。

java 复制代码
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。也就是将资源也就是滑动的图片信息,放在内存中的。

直接使用内存没有问题,直接存储在本地缓存。这里也可以统一放在其他介质中,如RedisSpringBoot版官方提供了RedisResourceStore的类,但是传统的项目并没有,如果想要放在缓存中统一使用,就需要实现CrudResourceStore接口即可。直接将SpringBoot版中的复制过来,改成自己的Redis客户端即可。

这个参数设置不设置不重要,就看你资源要不要共享。

setCacheStore()

用来存储被切割的背景图以及拼接的碎片,功能上主要用来验证码的验证。这个非常重要,默认还是内存,在单节点项目中没有问题,一旦涉及多节点,必然验证不通过。

这是我踩的最大的坑。

多节点的环境中,需要通过保存在其他介质中,如Redis。需要实现CacheStore接口,直接将LocalCacheStore复制过来,换成Redis客户单即可。

这里使用构造函数将Redis客户单传递进来。

java 复制代码
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

然后引入项目

初始化触发

js 复制代码
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()即可。

java 复制代码
@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 小结

单体项目测试的时候总会发现一切完好,但是到了分布式环境、多节点环境总会出现数据丢失的问题,会让框架的使用变的复杂。理解其原理,修修补补,马上完好如初!

相关推荐
葫芦和十三6 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp6 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑7 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯7 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan10 小时前
多Agent之间的区别
后端
青石路11 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充12 小时前
1.面向对象设计思想
后端
IT_陈寒12 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro13 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗13 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端