spring框架实现滑动验证码功能
- [1. 整体描述](#1. 整体描述)
- [2. 具体实现](#2. 具体实现)
-
- [2.1 滑动验证码实体类](#2.1 滑动验证码实体类)
- [2.2 滑动验证码登录VO](#2.2 滑动验证码登录VO)
- [2.3 滑动验证码接口返回类](#2.3 滑动验证码接口返回类)
- [2.4 滑动验证码工具类](#2.4 滑动验证码工具类)
- [2.5 滑动验证码Service](#2.5 滑动验证码Service)
- [2.6 滑动验证码Controller](#2.6 滑动验证码Controller)
- [3 工程源码](#3 工程源码)
- [4 总结](#4 总结)
1. 整体描述
之前项目需要在验证码模块,增加滑动验证码,用来给手机端使用的,大概看了下,主要方法就是将图片切割,然后记住偏移量,进行滑动,前端验证的时候,需要用前端传入的偏移量和生成的偏移量进行对比,如果在阈值之内,就验证通过,否则就不通过。具体实现方式见下文。之前没时间写,最近记录一下。
2. 具体实现
本工程主要依赖springboot框架,并且需要redis存验证码的信息,还需要几个图片,用来生成验证码。
2.1 滑动验证码实体类
java
package com.thcb.captchademo.captcha.domain;
import lombok.Data;
/**
* 滑动验证码
*
* @author thcb
* @date 2023-05-25
*/
@Data
public class Captcha {
/**
* 随机字符串
**/
private String nonceStr;
/**
* 验证值
**/
private String value;
/**
* 生成的画布的base64
**/
private String canvasSrc;
/**
* 画布宽度
**/
private Integer canvasWidth;
/**
* 画布高度
**/
private Integer canvasHeight;
/**
* 生成的阻塞块的base64
**/
private String blockSrc;
/**
* 阻塞块宽度
**/
private Integer blockWidth;
/**
* 阻塞块高度
**/
private Integer blockHeight;
/**
* 阻塞块凸凹半径
**/
private Integer blockRadius;
/**
* 阻塞块的横轴坐标
**/
private Integer blockX;
/**
* 阻塞块的纵轴坐标
**/
private Integer blockY;
/**
* 图片获取位置
**/
private Integer place;
}
2.2 滑动验证码登录VO
java
package com.thcb.captchademo.captcha.domain;
import lombok.Data;
/**
* 滑动验证码登录Vo
*
* @author thcb
* @date 2023-05-25
*/
@Data
public class LoginVo {
/**
* 随机字符串
**/
private String nonceStr;
/**
* 验证值
**/
private String value;
}
2.3 滑动验证码接口返回类
java
package com.thcb.captchademo.captcha.utils;
import java.util.HashMap;
/**
* 操作消息提醒
*
* @author thcb
*/
public class AjaxResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
public static final String CODE_TAG = "code";
/**
* 返回内容
*/
public static final String MSG_TAG = "msg";
/**
* 数据对象
*/
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public AjaxResult() {
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
*/
public AjaxResult(int code, String msg) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(int code, String msg, Object data) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (data != null && !data.equals("")) {
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success() {
return AjaxResult.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static AjaxResult success(Object data) {
return AjaxResult.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static AjaxResult success(String msg) {
return AjaxResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static AjaxResult success(String msg, Object data) {
return new AjaxResult(200, msg, data);
}
/**
* 返回错误消息
*
* @return
*/
public static AjaxResult error() {
return AjaxResult.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(String msg) {
return AjaxResult.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult error(String msg, Object data) {
return new AjaxResult(500, msg, data);
}
/**
* 返回错误消息
*
* @param code 状态码
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(int code, String msg) {
return new AjaxResult(code, msg, null);
}
}
2.4 滑动验证码工具类
此类是核心类,主要实现了图片的切割功能。
java
package com.thcb.captchademo.captcha.utils;
import com.thcb.captchademo.captcha.domain.Captcha;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Base64;
import java.util.Objects;
import java.util.Random;
/**
* 滑动验证码工具类
*
* @author thcb
* @date 2023-05-25
*/
public class CaptchaUtils {
/**
* 网络图片地址
**/
private final static String IMG_URL = "https://loyer.wang/view/ftp/wallpaper/%s.jpg";
/**
* 本地图片地址
**/
private final static String IMG_PATH = "E:\\caphcha\\%s.jpg";
/**
* 入参校验设置默认值
**/
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(1);
}
}
/**
* 获取指定范围内的随机数
**/
public static int getNonceByRange(int start, int end) {
Random random = new Random();
return random.nextInt(end - start + 1) + start;
}
/**
* 获取验证码资源图
**/
public static BufferedImage getBufferedImage(Integer place) {
try {
//随机图片
//获取网络资源图片
if (0 == place) {
int nonce = getNonceByRange(0, 1000);
String imgUrl = String.format(IMG_URL, nonce);
URL url = new URL(imgUrl);
return ImageIO.read(url.openStream());
}
//获取本地图片
else {
int nonce = getNonceByRange(0, 20);
String imgPath = String.format(IMG_PATH, nonce);
File file = new File(imgPath);
return ImageIO.read(file);
}
} catch (Exception e) {
System.out.println("获取拼图资源失败");
//异常处理
return null;
}
}
/**
* 调整图片大小
**/
public static BufferedImage imageResize(BufferedImage bufferedImage, int width, int height) {
Image image = bufferedImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage resultImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = resultImage.createGraphics();
graphics2D.drawImage(image, 0, 0, null);
graphics2D.dispose();
return resultImage;
}
/**
* 抠图,并生成阻塞块
**/
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);
}
/**
* 构建拼图轮廓轨迹
**/
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
Random random1 = new Random();
int face1 = random1.nextInt(4);
//凸/凹2
int face2;
//保证两个凸/凹不在同一位置
do {
Random random2 = new Random();
face2 = random2.nextInt(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的圆
//计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
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;
}
/**
* 根据朝向获取圆心坐标
*/
private static int[] getCircleCoords(int face, int blockWidth, int blockHeight, int blockRadius) {
//上
if (0 == face) {
return new int[]{blockWidth / 2 - 1, blockRadius};
}
//左
else if (1 == face) {
return new int[]{blockRadius, blockHeight / 2 - 1};
}
//下
else if (2 == face) {
return new int[]{blockWidth / 2 - 1, blockHeight - blockRadius - 1};
}
//右
else if (3 == face) {
return new int[]{blockWidth - blockRadius - 1, blockHeight / 2 - 1};
}
return null;
}
/**
* 在画布上添加阻塞块水印
*/
private static void addBlockWatermark(BufferedImage canvasImage, BufferedImage blockImage, int x, int y) {
Graphics2D graphics2D = canvasImage.createGraphics();
graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f));
graphics2D.drawImage(blockImage, x, y, null);
graphics2D.dispose();
}
/**
* BufferedImage转BASE64
*/
public static String toBase64(BufferedImage bufferedImage, String type) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, type, byteArrayOutputStream);
String base64 = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return String.format("data:image/%s;base64,%s", type, base64);
} catch (IOException e) {
System.out.println("图片资源转换BASE64失败");
//异常处理
return null;
}
}
}
2.5 滑动验证码Service
service层,,封装了工具类的一些方法,这里为了简单没写接口,正常应该是service和impl两个类。
java
package com.thcb.captchademo.captcha.service;
import com.thcb.captchademo.captcha.domain.Captcha;
import com.thcb.captchademo.captcha.utils.CaptchaUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.awt.image.BufferedImage;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 滑动验证码Service
*
* @author thcb
* @date 2023-05-25
*/
@Service
public class CaptchaService {
/**
* 拼图验证码允许偏差
**/
private static Integer ALLOW_DEVIATION = 3;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 校验验证码
*
* @param imageKey
* @param imageCode
* @return boolean
**/
public String checkImageCode(String imageKey, String imageCode) {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String key = "imageCode:" + imageKey;
String text = ops.get(key);
if (text == null || text.equals("")) {
return "验证码已失效";
}
// 根据移动距离判断验证是否成功
if (Math.abs(Integer.parseInt(text) - Integer.parseInt(imageCode)) > ALLOW_DEVIATION) {
return "验证失败,请控制拼图对齐缺口";
}
// 验证成功,删除redis缓存
stringRedisTemplate.delete(key);
return null;
}
/**
* 缓存验证码,有效期1分钟
*
* @param key
* @param code
**/
public void saveImageCode(String key, String code) {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("imageCode:" + key, code, 60, TimeUnit.SECONDS);
}
/**
* 获取验证码拼图(生成的抠图和带抠图阴影的大图及抠图坐标)
**/
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("-", "");
// 缓存
saveImageCode(nonceStr, String.valueOf(blockX));
//设置返回参数
captcha.setNonceStr(nonceStr);
captcha.setBlockY(blockY);
captcha.setBlockSrc(CaptchaUtils.toBase64(blockImage, "png"));
captcha.setCanvasSrc(CaptchaUtils.toBase64(canvasImage, "png"));
return captcha;
}
}
2.6 滑动验证码Controller
controller层有两个方法,一个是获取验证码的方法,一个是验证码校验的方法。
java
package com.thcb.captchademo.captcha.controller;
import com.thcb.captchademo.captcha.domain.Captcha;
import com.thcb.captchademo.captcha.domain.LoginVo;
import com.thcb.captchademo.captcha.service.CaptchaService;
import com.thcb.captchademo.captcha.utils.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 滑动验证码Controller
*
* @author thcb
* @date 2023-05-25
*/
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private CaptchaService captchaService;
@PostMapping("/slideCaptchaImage")
public AjaxResult getCaptcha() {
return AjaxResult.success(captchaService.getCaptcha(new Captcha()));
}
@PostMapping(value = "/login")
public AjaxResult login(@RequestBody LoginVo loginVo) {
// 验证码校验
String msg = captchaService.checkImageCode(loginVo.getNonceStr(), loginVo.getValue());
if (msg != null && !msg.equals("")) {
return AjaxResult.error(msg);
}
return AjaxResult.success();
}
}
3 工程源码
工程不算复杂,源码上传到GitCode,项目地址,上传有点问题,而且目前只能设置成私密项目,等之后可以调整的时候再改成公开的。
4 总结
滑动验证码功能不算复杂,可以和项目当前已有的验证码共存,调用不同的接口,返回不同类型的验证码,当然这个就根据项目具体情况确定了。