
拼图验证码实现
拼图验证码实现详解
前言
拼图验证码是当前较为流行的行为验证方式,用户通过拖动滑块完成拼图即可验证身份。本文将详细介绍一种基于Java的拼图验证码实现方案。
效果展示
实现原理
拼图验证码的核心原理是:
- 生成一个带有缺口的背景图
- 生成一个与缺口吻合的拼图块
- 用户拖动拼图块,系统验证拖动位置与预设缺口位置的吻合度
核心代码实现
1. 参数配置
/**
* 拼图验证码允许偏差
**/
private static Integer ALLOW_DEVIATION = 3;
/**
* 网络图片地址
**/
private final static String IMG_URL = "https://api.qjqq.cn/api/Img";
/**
* 本地图片地址
**/
private final static String IMG_PATH = "";
2. 参数校验与默认值设置
public static void checkCaptcha(Captcha captcha) {
//设置画布宽度默认值
if (captcha.getCanvasWidth() == null) {
captcha.setCanvasWidth(320);
}
//设置画布高度默认值
if (captcha.getCanvasHeight() == null) {
captcha.setCanvasHeight(155);
}
//设置阻塞块宽度默认值
if (captcha.getBlockWidth() == null) {
captcha.setBlockWidth(65);
}
//设置阻塞块高度默认值
if (captcha.getBlockHeight() == null) {
captcha.setBlockHeight(55);
}
//设置阻塞块凹凸半径默认值
if (captcha.getBlockRadius() == null) {
captcha.setBlockRadius(9);
}
//设置图片来源默认值
if (captcha.getPlace() == null) {
captcha.setPlace(0);
}
}
3. 获取验证码图片资源
public static BufferedImage getBufferedImage(Integer place) {
try {
//随机图片
int nonce = getNonceByRange(0, 1000);
//获取网络资源图片
if (0 == place) {
String imgUrl = IMG_URL;
URL url = new URL(imgUrl);
return ImageIO.read(url.openStream());
}
//获取本地图片
else {
String imgPath = String.format(IMG_PATH, nonce);
File file = new File(imgPath);
return ImageIO.read(file);
}
} catch (Exception e) {
System.out.println("获取拼图资源失败");
return null;
}
}
4. 生成拼图和缺口
生成拼图块的核心在于创建一个具有凹凸形状的拼图轮廓:
private static int[][] getBlockData(int blockWidth, int blockHeight, int blockRadius) {
int[][] data = new int[blockWidth][blockHeight];
double po = Math.pow(blockRadius, 2);
//随机生成两个圆的坐标,在4个方向上 随机找到2个方向添加凸/凹
//凸/凹1
int face1 = RandomUtils.nextInt(0, 4);
//凸/凹2
int face2;
//保证两个凸/凹不在同一位置
do {
face2 = RandomUtils.nextInt(0, 4);
} while (face1 == face2);
//获取凸/凹起位置坐标
int[] circle1 = getCircleCoords(face1, blockWidth, blockHeight, blockRadius);
int[] circle2 = getCircleCoords(face2, blockWidth, blockHeight, blockRadius);
//随机凸/凹类型
int shape = getNonceByRange(0, 1);
//圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
for (int i = 0; i < blockWidth; i++) {
for (int j = 0; j < blockHeight; j++) {
data[i][j] = 0;
//创建中间的方形区域
if ((i >= blockRadius && i <= blockWidth - blockRadius && j >= blockRadius && j <= blockHeight - blockRadius)) {
data[i][j] = 1;
}
double d1 = Math.pow(i - Objects.requireNonNull(circle1)[0], 2) + Math.pow(j - circle1[1], 2);
double d2 = Math.pow(i - Objects.requireNonNull(circle2)[0], 2) + Math.pow(j - circle2[1], 2);
//创建两个凸/凹
if (d1 <= po || d2 <= po) {
data[i][j] = shape;
}
}
}
return data;
}
5. 抠图实现
public static void cutByTemplate(BufferedImage canvasImage, BufferedImage blockImage, int blockWidth, int blockHeight, int blockRadius, int blockX, int blockY) {
BufferedImage waterImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
//阻塞块的轮廓图
int[][] blockData = getBlockData(blockWidth, blockHeight, blockRadius);
//创建阻塞块具体形状
for (int i = 0; i < blockWidth; i++) {
for (int j = 0; j < blockHeight; j++) {
try {
//原图中对应位置变色处理
if (blockData[i][j] == 1) {
//背景设置为黑色
waterImage.setRGB(i, j, Color.BLACK.getRGB());
blockImage.setRGB(i, j, canvasImage.getRGB(blockX + i, blockY + j));
//轮廓设置为白色,取带像素和无像素的界点,判断该点是不是临界轮廓点
if (blockData[i + 1][j] == 0 || blockData[i][j + 1] == 0 || blockData[i - 1][j] == 0 || blockData[i][j - 1] == 0) {
blockImage.setRGB(i, j, Color.WHITE.getRGB());
waterImage.setRGB(i, j, Color.WHITE.getRGB());
}
}
//这里把背景设为透明
else {
blockImage.setRGB(i, j, Color.TRANSLUCENT);
waterImage.setRGB(i, j, Color.TRANSLUCENT);
}
} catch (ArrayIndexOutOfBoundsException e) {
//防止数组下标越界异常
}
}
}
//在画布上添加阻塞块水印
addBlockWatermark(canvasImage, waterImage, blockX, blockY);
}
6. 生成验证码
public Object getCaptcha(Captcha captcha) {
//参数校验
CaptchaUtils.checkCaptcha(captcha);
//获取画布的宽高
int canvasWidth = captcha.getCanvasWidth();
int canvasHeight = captcha.getCanvasHeight();
//获取阻塞块的宽高/半径
int blockWidth = captcha.getBlockWidth();
int blockHeight = captcha.getBlockHeight();
int blockRadius = captcha.getBlockRadius();
//获取资源图
BufferedImage canvasImage = CaptchaUtils.getBufferedImage(captcha.getPlace());
//调整原图到指定大小
canvasImage = CaptchaUtils.imageResize(canvasImage, canvasWidth, canvasHeight);
//随机生成阻塞块坐标
int blockX = CaptchaUtils.getNonceByRange(blockWidth, canvasWidth - blockWidth - 10);
int blockY = CaptchaUtils.getNonceByRange(10, canvasHeight - blockHeight + 1);
//阻塞块
BufferedImage blockImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
//新建的图像根据轮廓图颜色赋值,原图生成遮罩
CaptchaUtils.cutByTemplate(canvasImage, blockImage, blockWidth, blockHeight, blockRadius, blockX, blockY);
String nonceStr = UUID.randomUUID().toString().replaceAll("-", "");
// 缓存,有效期两分钟
redisUtil.set(IMAGE_CODE + nonceStr, String.valueOf(blockX), 2, TimeUnit.MINUTES);
//设置返回参数
captcha.setNonceStr(nonceStr);
captcha.setBlockY(blockY);
captcha.setBlockSrc(CaptchaUtils.toBase64(blockImage, "png"));
captcha.setCanvasSrc(CaptchaUtils.toBase64(canvasImage, "png"));
return captcha;
}
7. 验证码校验
public String checkImageCode(String imageKey, String imageCode) {
String text = (String) redisUtil.get(IMAGE_CODE + imageKey);
if (StringUtils.isBlank(text)) {
return "0";
}
// 根据移动距离判断验证是否成功
if (Math.abs(Integer.parseInt(text) - Integer.parseInt(imageCode)) > ALLOW_DEVIATION) {
return "0";
}
return "1";
}
技术要点解析
1. 拼图轮廓生成算法
拼图块的轮廓使用数学方程来生成。通过在拼图块的四个方向上随机选择两个方向添加凸起或凹陷,使用圆的方程 (x-a)²+(y-b)²=r² 来计算边界点,从而生成特定形状的拼图块。
2. 图像处理技术
- 图像抠图: 通过设置像素的ARGB值实现图像的抠取和透明效果
- 图像缩放: 使用Java AWT的Graphics2D功能调整图像大小
- 水印添加: 在背景图上添加半透明水印显示拼图块阴影
3. Redis缓存验证
验证码生成后,将拼图块的正确X坐标保存在Redis中,设置过期时间为2分钟:
redisUtil.set(IMAGE_CODE + nonceStr, String.valueOf(blockX), 2, TimeUnit.MINUTES);
验证时,取出保存的坐标值与用户提交的坐标进行比对,允许有±3像素的误差:
if (Math.abs(Integer.parseInt(text) - Integer.parseInt(imageCode)) > ALLOW_DEVIATION) {
return "0";
}
前端实现思路
前端实现主要包括以下步骤:
- 请求后端获取验证码图片(背景图和拼图块)
- 显示背景图和拼图块
- 监听鼠标事件,实现拖动功能
- 拖动完成后,将拖动距离发送给后端进行验证
- 根据验证结果显示成功或失败状态
前端代码示例(仅供参考):
// 拖动实现
function drag(e) {
if (!isMouseDown) return;
const moveX = e.pageX - originX;
if (moveX < 0) {
return;
}
if (moveX > maxMoveWidth) {
return;
}
// 移动滑块
slideBlock.style.left = moveX + 'px';
// 移动拼图
jigsawBlock.style.left = moveX + 'px';
}
// 验证
function verify(moveX) {
const params = {
imageKey: captchaData.nonceStr,
imageCode: moveX
};
axios.post('/api/verify-captcha', params).then(res => {
if (res.data === '1') {
// 验证成功
showSuccess();
} else {
// 验证失败
showFail();
resetCaptcha();
}
});
}
总结
本文介绍的拼图验证码实现具有以下特点:
- 安全性: 通过后端生成拼图块和验证,前端只负责展示和交互,提高了安全性
- 用户体验: 使用自然的拖动交互,比传统的字符验证码更友好
- 灵活配置: 支持自定义拼图大小、形状和图片来源
- 防机器人: 有效防止自动化程序的暴力破解
这种实现方案适用于各类需要防止机器人自动注册、登录的Web应用,能够有效提升系统安全性。
参考资料
- Java AWT图像处理: Oracle Java文档
- Redis数据缓存: Redis官方文档
- 前端拖拽实现: MDN文档 - 拖放API
- 感谢你赐予我前进的力量
赞赏者名单
因为你们的支持让我意识到写文章的价值🙏
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 程序员橙子
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果