超详细 anji-captcha滑块验证springboot+uniapp微信小程序前后端组合
目录
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;@Overridepublic String type() {return "redis";}@Overridepublic void set(String key, String value, long expiresInSeconds) {stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);}@Overridepublic boolean exists(String key) {return stringRedisTemplate.hasKey(key);}@Overridepublic void delete(String key) {stringRedisTemplate.delete(key);}@Overridepublic String get(String key) {return stringRedisTemplate.opsForValue().get(key);}@Overridepublic Long increment(String key, long val) {if (!this.stringRedisTemplate.hasKey(key)) {return null;}return this.stringRedisTemplate.opsForValue().increment(key, val);}@Autowiredpublic 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;}@Overridepublic 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
码字不易,与你有利,勿忘点赞