超详细 anji-captcha滑块验证springboot+uniapp微信小程序前后端组合

发布于:2025-07-16 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

1:pom文件引入jar包

2:配置文件

3:踩坑-1

4:踩坑-2

5:后端二次验证

6:自定义背景图


给用户做的一个小程序,被某局安全验证后,说登录太简单,没有验证码等行为认证。于是想着给登录页加上一个滑块验证码(数字验证码还要输入,太麻烦了),于是开始问deepseek,列举了几个,看到有anji-captcha,就开始尝试搞了。


一开始问deepseek使用方法,给我列出的简直简单得不要不要的,还以为真的很简单,按照几个步骤开始搞了,结果根本用不了,网上去搜相关案例,全部清一色照搬anji.captcha开源文档,基本一摸一样,基本没看到有人写具体使用经验,全是寥寥草草照搬,痛苦至极,简单的后端代码,查了不知道多少资料,搞了一天多,真是痛苦加倍。


anji-captcha开源项目地址:https://github.com/anji-plus/captcha

anji-captcha开源文档地址:在线体验暂时下线 !!! | AJ-Captcha


1:pom文件引入jar包

<dependency>
	<groupId>com.anji-plus</groupId>
	<artifactId>spring-boot-starter-captcha</artifactId>
	<version>1.3.0</version>
</dependency>

开源文档里面写着出了1.4.0,尝试着引入没成功,后面改回使用1.3.0


2:配置文件

#使用redis作为缓存,也可以使用local
aj.captcha.cache-type=redis

# 缓存的阈值,达到这个值,清除缓存
aj.captcha.cache-number=5000

# 定时清除过期缓存(单位秒),设置为0代表不执行
aj.captcha.timing-clear=120

# 初始化验证码类型-滑块验证码
aj.captcha.type=blockPuzzle

# 右下角水印文字,中文请使用unicode转码
aj.captcha.water-mark=

# 校验滑动拼图允许误差偏移量12px
aj.captcha.slip-offset=12

# 开启aes加密坐标
aj.captcha.aes-status=true

# 滑动干扰项(0/1/2) 0不开启,2最强干扰
aj.captcha.interference-options=1

还有其他配置,具体可以看开源文档介绍,推荐去看一下,了解都有哪些是你需要的。

使用了redis作为缓存,所以项目接入redis,不会用的话自行去其他地方查,本文不做介绍。


3:踩坑-1

网上很多资料就到这里了,轻描淡写,说引入包,配置好基本参数,就可以基本使用了。

于是开始尝试使用,根据介绍aj.captcha的jar包里面默认提供有两个controller接口,给我们前端调用,分别是:【/captcha/get  获取验证码】【/captcha/check  校验验证码】。

于是开始前端调用,首先这里是登录页使用,所以这两个接口需要放权,不验证登录权限,具体自己根据自己的项目进行配置。

调用 /captcha/get,参数如下:

{
	"captchaType": "blockPuzzle",  //验证码类型,表示使用滑块验证码
	"clientUid": "唯一标识"  //客户端UI组件id,组件初始化时设置一次,UUID(非必传参数)
}

开始尝试用postman调用,不出意外,报错了,错误忘记啥了,大概意思是没有指定aj-captcha的redis缓存配置,这一步网上有资料,开源文档也有说明,不用多久就解决了。

开源文档的说明是:对于分布式多实例部署的应用,应用必须自己实现CaptchaCacheService,比如用Redis或者memcache,参考service/springboot/src/.../CaptchaCacheServiceRedisImpl.java
在resources目录新建META-INF.services文件夹,参考resource/META-INF/services中的写法。

一开始我觉得我不是分布式系统,就觉得不用加,所以报错了,后面加上去就好了。


启动类所在模块的resources文件夹下面新建一个文件,路径就是上面说的:resources/META-INF/services,services下新建一个文件,文件名为:com.anji.captcha.service.CaptchaCacheService

文件内容为:CaptchaCacheService接口的实现类位置,比如:

com.xxx.yyy.config.CaptchaCacheServiceRedisImpl

所以需要去com.xxx.yyy.config下面建一个名为CaptchaCacheServiceRedisImpl的类,并实现CaptchaCacheService接口。

import com.anji.captcha.service.CaptchaCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class CaptchaCacheServiceRedisImpl implements CaptchaCacheService {

    private StringRedisTemplate stringRedisTemplate;

    @Override
    public String type() {
        return "redis";
    }

    @Override
    public void set(String key, String value, long expiresInSeconds) {
        stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
    }

    @Override
    public boolean exists(String key) {
        return stringRedisTemplate.hasKey(key);
    }

    @Override
    public void delete(String key) {
        stringRedisTemplate.delete(key);
    }

    @Override
    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    @Override
    public Long increment(String key, long val) {
        if (!this.stringRedisTemplate.hasKey(key)) {
            return null;
        }
        return this.stringRedisTemplate.opsForValue().increment(key, val);
    }

    @Autowired
    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
}


重启后端服务后,再次去postman调用,可以了,正确返回结果了。

其中:secretKey 是aes密匙,后面用来加密坐标,验证滑动是否正确的。

originalImageBase64:滑块背景图,310 * 155的分辨率。

jigsawImageBase64:滑块缺口图,47 * 155的分辨率。

token:验证滑动坐标时,需要提交回后端,后端需要根据这个token去找缓存,取到对应缓存信息。

图片是aj-captcha默认自带的,它默认有几张图片。


获取验证码后,查看后端redis缓存,内容是这样的,记录着aes密匙和缺口坐标信息。缓存有效期120秒。


4:踩坑-2

到这里,我已经很兴奋了,觉得后端部分已经完成了80%了,于是开始调试第二个接口,也就是验证滑动位置接口【/captcha/check】,开始按照开源文档介绍传参。

{
	 "captchaType": "blockPuzzle",  // 指定为滑块验证码
	 "pointJson": "QxIVdlJoWUi04iM+65hTow==", // aes加密后坐标信息
	 "token": "71dd26999e314f9abb0c635336976635" // token是前面的get接口返回的
}

 就这个aes加密后坐标信息,我也吃了不少苦头,aes加密我知道,但原文究竟应该是什么,什么样的格式,开源文档没有说明,硬生生网上查了很久资料才知道,问deepseek,回答模糊不清。可能也是我傻狗吧,其实就是一个json对象,里面是x和y的值。比如:{x:155,y:13},然后用JSON.stringify转成字符串,再加密就行了。

然后,加密模式是什么?ECB? CBC? 没有说明,只能一个个尝试,最后是ECB


搞好上面,觉得一切都差不多了,然后postman一调接口,返回说验证失败,位置不对。那正常,因为坐标我是随便写的,这里说一下,失败后,后端的缓存就没有了,也就是说,只能被验证一次。


然后我突然发现一个事情,验证接口竟然要传y坐标值???我直接懵逼了,滑块一直不都是横向右滑动的吗,横向滑动取到x坐标值,我哪来的y坐标值,然后扒看了【BlockPuzzleCaptchaServiceImpl】的check方法源码,确实有验证y坐标值,当场两眼一黑。

为了这个问题,我近乎疯狂,又问deepseek,又是一堆胡乱回答,已对它彻底失望,百度找答案,由于网上千篇一律都是照搬开源文档,没点自己个人经验的,根本找不到答案。于是开始尝试不传y坐标值,发现报错,或者随意传值,结果就是验证失败,差点放弃aj-captcha。最后就是自己想办法了。

最后想到几个方案:

1:多调几次接口发现,redis缓存里面的y坐标值永远都是5,那前端也直接写死5算了,但想想不太靠谱。

2:不使用aes加密,后端会返回缺口坐标给前端,里面包含了y坐标值,但这样搞就不安全了,获取验证码的同时直接把答案告诉你了,这明显不妥。

3:尝试自己新建一个类,继承【BlockPuzzleCaptchaServiceImpl】或实现其父类,重写check方法,发现后面会报错,这条路走不通。最后想到的办法是使用AOP切面,拦截【BlockPuzzleCaptchaServiceImpl】check方法。


新建一个名为【CustomizeCaptchaService】的类,如下:


import com.anji.captcha.model.common.RepCodeEnum;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.model.vo.PointVO;
import com.anji.captcha.service.impl.CaptchaServiceFactory;
import com.anji.captcha.util.AESUtil;
import com.anji.captcha.util.JsonUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Aspect
@Service
public class CustomizeCaptchaService {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private static String REDIS_SECOND_CAPTCHA_KEY = "RUNNING:CAPTCHA:second-%s";

    private static String REDIS_CAPTCHA_KEY = "RUNNING:CAPTCHA:%s";

    private static String cacheType = "redis";

    private static Long EXPIRESIN_THREE = 3 * 60L;

    @Value("${aj.captcha.slip-offset:10}")
    private Integer slipOffset;

    @Around("execution(* com.anji.captcha.service.impl.BlockPuzzleCaptchaServiceImpl.check(..))")
    public Object aroundCheckPoint(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        CaptchaVO captchaVO = (CaptchaVO) args[0];

		// ################################# 位置标记 #############################################
        // 原来方法里,这个位置是处理校验次数是否超过限制的,由于我不需要验证,这里没加,但这个位置先标记一下,后面再讲
        // ResponseModel r = super.check(captchaVO);
        // if(!validatedReq(r)){
        //     return r;
        // }
        // ################################# 位置标记 #############################################

        String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken());
        if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
        }
        String s = CaptchaServiceFactory.getCache(cacheType).get(codeKey);
        CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
        PointVO point = null;
        PointVO point1 = null;
        String pointJson = null;
        try {
            point = JsonUtil.parseObject(s, PointVO.class);
            //aes解密
            pointJson = AESUtil.aesDecrypt(captchaVO.getPointJson(), point.getSecretKey());
            point1 = JsonUtil.parseObject(pointJson, PointVO.class);
        } catch (Exception e) {
            logger.error("验证码坐标解析失败", e);
            return ResponseModel.errorMsg(e.getMessage());
        }
        if (point.x - slipOffset > point1.x || point1.x > point.x + slipOffset) {
            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR);
        }
        //校验成功,将信息存入缓存
        String secretKey = point.getSecretKey();
        String value = null;
        try {
            value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(pointJson), secretKey);
        } catch (Exception e) {
            logger.error("AES加密失败", e);
            return ResponseModel.errorMsg(e.getMessage());
        }
        String secondKey = String.format(REDIS_SECOND_CAPTCHA_KEY, value);
        CaptchaServiceFactory.getCache(cacheType).set(secondKey, captchaVO.getToken(), EXPIRESIN_THREE);
        captchaVO.setResult(true);
        captchaVO.resetClientFlag();
        return ResponseModel.successData(captchaVO);
    }
}

类里打上了AOP类注解,并切面拦截【BlockPuzzleCaptchaServiceImpl】的check方法,自己重写此方法,其实我也是去原类方法里面复制出来,然后稍微改动一下。

改动点:1:去掉检查验证次数是否短时间频繁。2:去掉y坐标验证。

我不需要检查验证次数是否频繁,所以没搞这个,如果确实需要,那就有点麻烦了,因为这个验证方法是【BlockPuzzleCaptchaServiceImpl】的父类【AbstractCaptchaService】的check方法写的,【BlockPuzzleCaptchaServiceImpl】已经重写了此方法,看【BlockPuzzleCaptchaServiceImpl】的check方法源码就会知道,它先执super.check(captchaVO)调用了父类的验证方法,所以我们切面拦截后,是没法直接调用【AbstractCaptchaService】的check方法的。

最后又是花时间处理,真麻,想到的办法是,再建一个【AjCaptchaVerify】类继承【AbstractCaptchaService】。

import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.impl.AbstractCaptchaService;

public class AjCaptchaVerify extends AbstractCaptchaService {

    public Boolean superCheck(CaptchaVO captchaVO) {
        ResponseModel r = super.check(captchaVO);
        if(!validatedReq(r)){
            return false;
        }
        return true;
    }

    @Override
    public String captchaType() {
        return null;
    }
}

然后AOP切面方法里面,那段位置标记注释,可以改成:

// ################################# 位置标记 #############################################
ResponseModel verifyResponseModel = new AjCaptchaVerify().superCheck(captchaVO);
if (Objects.nonNull(verifyResponseModel)) {
	return verifyResponseModel;
}
// ################################# 位置标记 #############################################

这样就可以校验短时间内验证是否频繁。


真是多坑,踩得我怀疑人生。


5:后端二次验证

anji-captcha自带有后端二次验证,至于为什么要用后端二次验证,就以登录来说,用户选择短信登录,那么获取短信验证码的时候,给他来一个滑块的行为认证,必须滑对才能获取短信验证码,这是个正常操作了,很多系统都有。那按照之前讲的,使用【/captcha/check】验证滑动是否正确,这个是anji-captcha自带的验证接口,滑动完成,调用【/captcha/check】验证是否正确,正确的话,再调用【获取短信验证码接口】,为了防止越过行为认证,直接调取【获取短信验证码接口】,所以需要到这个后端二次验证了。当然,如果你把验证滑动行为认证和获取短信验证码集中在一个接口里面,那就不需要这个二次验证了。

这个后端二次验证文档同样没有说明,网上也没找着,还是得扒看源码了解。


扒开【BlockPuzzleCaptchaServiceImpl】的check方法,会发现最后验证成功后会设置一个缓存,用于二次认证使用。

它的缓存key是用aes加密过的,密匙还是用回【/captcha/get】返回的。

加密内容是:token---坐标点json字符串(解密过的)

于是到缓存里面可以看到验证成功后加密是这样的,值是token,后面没啥用,主要看key。


知道他的加密方式后,前端就可以根据这样加密出一串密文,也就是上面这个缓存的key,传给后端,后端二次验证方法:

// 先注入
@Autowired
private CaptchaService captchaService;

// 具体使用
CaptchaVO captchaVO = new CaptchaVO();
captchaVO.setCaptchaType('blockPuzzle');
captchaVO.setCaptchaVerification('前端加密后的密文(token---坐标点json字符串)');
ResponseModel verification = this.captchaService.verification(captchaVO);
if (Objects.isNull(verification) || !verification.isSuccess()) {
	// 认证失败
} else {
	// 认证通过
}

6:自定义背景图

如果不想要anji-captcha自带的滑块背景图,也可以自己配置。

当然,这个配置,也是一个坑,我狠狠的踩了。开源文档没有过多介绍这块,网上的更加零碎,还是摸着石头过河,整理网上零碎的信息,最后一点点确认出来了。


首先,背景图要求是310 * 155的分辨率,如果到了前端觉得显示模糊,可以自己选择*2或者*3加大分辨率,然后前端显示固定成310 * 155,不过前端验证的时候可能得/2或者/3了,具体没试,因为太大会影响滑块验证码图片加载的速度。


其次,配置文件配置背景图和缺口图路径:

aj.captcha.jigsaw=classpath:images/jigsaw

背景图和缺口图放在启动类所在模块

背景图路径【resources/images/jigsaw/original

缺口图路径【resources/images/slidingBlock

上面的/images/jigsaw这个路径随便写,但最后一个文件夹名称必须使用【original】和【slidingBlock】,因为anji-captcha源码里面就是写死了。


 然后original文件夹下面的背景图,放个几张进去,你看着来,三四张也行,五六张也行,反正是随机取的,图片文件名称也是随便自己命名。注意:这里说的背景图,是一张完整的背景图,没有被扣出缺口的。


然后slidingBlock文件夹下面放几张缺口图,这个就头疼了,完全不知道这个缺口图应该是怎么样,找了很久都没有具体说明,最后找到了一个网友的项目代码,他没有具体说明这个缺口图有什么要注意的,就说把项目下面的缺口图复制到自己项目就行了。我一看也是一脸疑惑,你的缺口图能适配我的背景吗,虽然很纳闷,但还是抱着心态试了一下,还真的成功。

看了一下源码,应该是根据给定的缺口图形状以及位置(是y坐标固定),去背景图里面随机x坐标扣出一块相同形状缺口图,然后最终形成了属于本次滑块验证码的缺口图。

这里放出来一下,大家直接保存到项目里面使用就行了。缺口图是:47 * 155的分辨率。

图片放出来被自动打上水印了,自行去掉或者去【点击这里获取】


到这里,后端部分就结束了,说了这么多没说到前端的,前端点击下面的另一篇文章看吧。

超详细 anji-captcha滑块验证uniapp微信小程序前端组件https://blog.csdn.net/new_public/article/details/149336921



码字不易,与你有利,勿忘点赞


网站公告

今日签到

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