前言
前段时间接到需求,需要给登录模块做滑块登录验证功能,要求设计尽量不复杂且能正确校验滑动图片验证码。
实现思路
网上有很多实现方案,我首先提出了两个方案一个由前端解决,参考方案:https://www.cnblogs.com/xiaohuizhang/p/17175873.html
另一种由后端解决,滑块素材图片存储在本地,校验规则存放redis
大致方案
1、通过滑块拼图和素材背景图生成滑块素材
2、生成滑块校验规则(滑动距离)并存入redis
3、编写校验接口
4、定时任务清理并重新生成素材(可选)
生成素材、生成规则
从网上找一个拼图素材,例如
放入本地项目resource目录下,另外随意找几张素材背景图,同样存入resource目录,接着通过接口裁剪生成校验素材,素材可以for循环创建指定数量。要注意图片路径的读取方式,tomcat服务器和docker容器读取方式不一样,下面代码可以同时满足两种场景读取
File bgFront;
ClassLoader classLoader = getClass().getClassLoader();
URL resource = classLoader.getResource("滑块拼图.png");
Path targetPath = Paths.get("滑块拼图.png");
try {
Files.copy(resource.openStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
bgFront = targetPath.toFile();
if (!bgFront.exists()) {
logger.error("拼图文件不存在,素材生成失败");
return;
}
} catch (IOException e) {
logger.error("生成拼图出错:", e);
return;
}
// 滑块素材类
public class SlideMaterialInfo {
private Integer x;
private Integer y;
private String materialId;
private Long createTime;
}
// 随机初始化滑块拼图缺口的位置和uuid
SlideMaterialInfo slideMaterialInfo = new SlideMaterialInfo(System.currentTimeMillis());
// x 的随机位置
int positionX = NumberUtils.getRandomNum(80, 270);
// 随机产生的Y值
int positionY = NumberUtils.getRandomNum(10, 110);
String materialId = UUIDUtil.getUUID();
slideMaterialInfo.setMaterialId(materialId);
slideMaterialInfo.setX(positionX);
slideMaterialInfo.setY(positionY);
try {
// 生成的素材上传oss
String savePath = "指定上传路径";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(buffImg, savePath, baos);
byte[] imageInByte = baos.toByteArray();
OssFileUtil.uploadFile2Oss(savePath, imageInByte);
} catch (IOException e) {
logger.error("在oss:{}路径下创建图片失败:", savePath, e);
}
// 匹配规则放入redis
redisTemplate.opsForSet().add("SLIDE_MATERIAL_ID_SET", materialId);
redisTemplate.opsForHash().put("SLIDE_MATERIAL_INFO_HASH_ID", JSON.toJSONString(slideMaterialInfo));
校验接口
前端首先请求后端随机从redis获取一个materailId接着从oss下载图片素材并展示,用户滑动后调用校验方法,主要通过前端传递滑动的距离和图片大小,后端进行计算,如果滑动误差超过指定大小则校验失败
private boolean check(String materailId, Integer start, Integer end, Integer slideDistance, Double zoomFactor) {
Object cacheVal = redisTemplate.opsForHash().get("SLIDE_MATERIAL_INFO_HASH_ID", materailId);
SlideMaterialInfo slideMaterialInfo = JSON.parseObject(cacheVal.toString(), SlideMaterialInfo.class);
// 计算距离
int calculateDis = end - start;
int calculateDeviation = Math.abs(calculateDis - slideDistance);
// 计算差距过大,验证失败
if (calculateDeviation > 5) {
return false;
}
// 缩放后的距离(和图片实际进行处理)
double zoomDis = calculateDis / zoomFactor;
// 缩放偏差
int zoomDeviation = Math.abs((int) zoomDis - slideMaterialInfo.getX());
if (zoomDeviation > 5) {
return false;
}
}
定时任务
这一步可有可无,有的项目比较简单不需要更新滑块,一次性生成几百个就够用了。我的项目里需要清理,简单描述一下,主要是设定一个定时任务清除redis和oss已有的数据,然后重新调用生成方法创建新的素材。需要注意的是定时任务可能会因为容器中服务多实例导致多线程问题。因此在任务开始时,先向redis创建分布式锁,当其他服务发现已加锁则不重复执行这个任务。