当前位置: 首页 > news >正文

超详细 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



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


文章转载自:
http://cactus.sxnf.com.cn
http://chimaerism.sxnf.com.cn
http://biopolymer.sxnf.com.cn
http://apish.sxnf.com.cn
http://acrocyanosis.sxnf.com.cn
http://belitoeng.sxnf.com.cn
http://asbestoidal.sxnf.com.cn
http://ballplayer.sxnf.com.cn
http://bulginess.sxnf.com.cn
http://amidah.sxnf.com.cn
http://adenocarcinoma.sxnf.com.cn
http://acciaccatura.sxnf.com.cn
http://abdicable.sxnf.com.cn
http://affectlessness.sxnf.com.cn
http://anglesite.sxnf.com.cn
http://bathybic.sxnf.com.cn
http://baddie.sxnf.com.cn
http://antibiotic.sxnf.com.cn
http://always.sxnf.com.cn
http://btu.sxnf.com.cn
http://bankroll.sxnf.com.cn
http://brains.sxnf.com.cn
http://botulinum.sxnf.com.cn
http://actually.sxnf.com.cn
http://breadline.sxnf.com.cn
http://arse.sxnf.com.cn
http://automatization.sxnf.com.cn
http://aplomb.sxnf.com.cn
http://beer.sxnf.com.cn
http://amentaceous.sxnf.com.cn
http://www.dtcms.com/a/281148.html

相关文章:

  • 如何定义一个只能在堆上或栈上生成对象的类
  • Python初学者笔记第十二期 -- (集合与字典编程练习题)
  • U-Boot 中增加 GIC-400中断服务程序
  • Copula理论:覆盖相关性分析、极值相依性、回归建模、时间序列预测、贝叶斯网络,R/Python双语言实现+AI编程辅助(科研绘图与结果呈现)
  • Nestjs框架: 数据库多租户模式与动态模块初探
  • Oracle日期时间函数说明及与MySql区别说明
  • 同济医院R语言训练营第三期开讲!上交大张维拓老师主讲
  • RabbitMQ工作流程
  • SQL学习记录01
  • 15.图像 模板轮廓检测
  • 李白周游记50篇
  • linux-develop
  • 基于Alpine构建MySQL镜像
  • 第二阶段-第二章—8天Python从入门到精通【itheima】-129节(MySQL的安装)
  • 【前后端】Node.js 模块大全
  • 巨坑检查无误还报错is not mapped MappingException: Unknown entity:@Entity
  • DeepSWE:通过强化学习扩展训练开源编码智能体
  • 多层 `while` 循环中,`break` 的行为
  • ES2023 新特性解析_数组与对象的现代化操作指南
  • 二分查找栈堆
  • 【C语言进阶】字符函数和字符串函数的内部原理
  • “ModuleNotFoundError“深度解析:Python模块导入问题的终极指南
  • PHP语言基础知识(超详细)第二节
  • OSPFv3中LSA参数
  • dbever 导出数据库表的建表语句和数据插入语句
  • 嵌入式Linux:进程间通信机制
  • AJAX 开发中的注意点
  • ASRPRO系列语音模块(第十天)
  • AI 增强大前端数据加密与隐私保护:技术实现与合规遵
  • Python 程序设计讲义(2):Python 概述