最近有个需求,需要添加滑块拼图验证码,网上了解了一些生成校验方式,下面写个 demo实现一下。
一、滑块拼图验证码生成
1、生成思路
滑块拼图验证码生成思路:
- 在若干原图中随机一张原图,然后改变原图大小为规范的大图对象
- 随机生成(X,Y)坐标
- 创建小图对象
- 随机生成拼图轮廓数据
- 从大图中裁剪拼图。抠原图,裁剪拼图
- 返回滑块拼图验证码信息:两个Base64字符串图片信息和(X,Y)坐标。
注意:
- 1、随机生成拼图轮廓数据是重点,然后裁剪拼图时根据需要它来抠原图,裁剪拼图。
- 2、拼图的凹凸信息,有两种处理方式
- 以小图的四边为边缘,外加凸圆弧或者内挖凹圆弧。
- 以小图的四边为最终边界,内加凸圆弧或者内挖凹圆弧,并且处理掉凹凸之外的颜色。
2、生成代码
这里使用 以小图的四边为最终边界,上凹下凸,左无由凸,左边框高亮阴暗简单处理。
定义一个滑块拼图信息实体类:
@Data
public class SliderPuzzleInfo {
/**
* 大图宽度
*/
private Integer bigWidth;
/**
* 大图高度
*/
private Integer bigHeight;
/**
* 大图转BASE64字符串
*/
private String bigImageBase64;
/**
* 大图
*/
private BufferedImage bigImage;
/**
* 随机坐标Y
*/
private Integer posY;
/**
* 随机坐标X
*/
private Integer posX;
/**
* 小图宽度
*/
private Integer smallWidth;
/**
* 小图高度
*/
private Integer smallHeight;
/**
* 小图转BASE64字符串
*/
private String smallImageBase64;
/**
* 小图
*/
private BufferedImage smallImage;
}
生成具体代码如下:
public class SlidePuzzleUtil {
static Logger logger = LoggerFactory.getLogger(SlidePuzzleUtil.class);
// 大图宽度(原图裁剪拼图后的背景图)
private static final Integer bigWidth = 320;
// 大图高度
private static final Integer bigHeight = 160;
// 小图宽度(滑块拼图)
private static int smallWidth = 40;
// 小图高度
private static int smallHeight = 40;
// 小圆半径,即拼图上的凹凸轮廓半径
private static final Integer smallCircle = 8;
// 小圆距离点
private static int smallCircleR1 = smallCircle / 2;
public static void main(String[] args) throws IOException {
int i = 3;
File file = new File("D:/TempFiles/slide/slidebase" + i + ".png");
SliderPuzzleInfo sliderPuzzleInfo = SlidePuzzleUtil.createImage(new FileInputStream(file));
if (sliderPuzzleInfo == null) {
System.out.println("图片验证码生成失败");
}
File file1 = new File("D:/TempFiles/slide/demo2BigImage.png");
File file2 = new File("D:/TempFiles/slide/demo2SmallImage.png");
ImageIO.write(sliderPuzzleInfo.getBigImage(), "png", file1);
ImageIO.write(sliderPuzzleInfo.getSmallImage(), "png", file2);
}
/**
* 生成滑块拼图验证码
*
* @param input
* @return 返回null,表示生成滑块拼图验证码异常
*/
public static SliderPuzzleInfo createImage(InputStream input) {
SliderPuzzleInfo sliderPuzzleInfo = new SliderPuzzleInfo();
try {
// 1.获取原图对象
BufferedImage originalImage = ImageIO.read(input);
// 规范原图的大小
BufferedImage bigImage = resizeImage(originalImage, bigWidth, bigHeight, true);
// 2.随机生成离左上角的(X,Y)坐标,上限为 [bigWidth-smallWidth, bigHeight-smallHeight]。最好离大图左边远一点,上限不要紧挨着大图边界
Random random = new Random();
int randomX = random.nextInt(bigWidth - 4 * smallWidth - smallCircle) + 2 * smallWidth; // X范围:[2*smallWidth, bigWidth - 2*smallWidth - smallCircle)
int randomY = random.nextInt(bigHeight - smallHeight - 2 * smallCircle) + smallCircle; // Y范围:[smallCircle, bigHeight - smallHeight - smallCircle)
logger.info("原图大小:{} x {},大图大小:{} x {},随机生成的坐标:(X,Y)=({},{})", originalImage.getWidth(), originalImage.getHeight(), bigImage.getWidth(), bigImage.getHeight(),
randomX, randomY);
// 3.创建小图对象
BufferedImage smallImage = new BufferedImage(smallWidth, smallHeight, BufferedImage.TYPE_4BYTE_ABGR);
// 4.随机生成拼图轮廓数据
int[][] slideTemplateData = getSlideTemplateData(smallWidth, smallHeight, smallCircle, smallCircleR1);
// 5.从大图中裁剪拼图。抠原图,裁剪拼图
cutByTemplate(bigImage, smallImage, slideTemplateData, randomX, randomY);
sliderPuzzleInfo.setPosX(randomX);
sliderPuzzleInfo.setPosY(randomY);
sliderPuzzleInfo.setBigWidth(bigWidth);
sliderPuzzleInfo.setBigHeight(bigHeight);
sliderPuzzleInfo.setBigImage(bigImage);
sliderPuzzleInfo.setBigImageBase64(getImageBASE64(bigImage));
sliderPuzzleInfo.setSmallWidth(smallWidth);
sliderPuzzleInfo.setSmallHeight(smallHeight);
sliderPuzzleInfo.setSmallImage(smallImage);
sliderPuzzleInfo.setSmallImageBase64(getImageBASE64(smallImage));
} catch (Exception e) {
sliderPuzzleInfo = null;
logger.info("创建生成滑块拼图验证码异常,e=", e);
} finally {
return sliderPuzzleInfo;
}
}
/**
* 获取拼图图轮廓数据
* @param smallWidth
* @param smallHeight
* @param smallCircle
* @param r1
* @return 0和1,其中0表示没有颜色,1有颜色
*/
private static int[][] getSlideTemplateData(int smallWidth, int smallHeight, int smallCircle, int r1) {
// 拼图轮廓数据
int[][] data = new int[smallWidth][smallHeight];
//拼图去掉凹凸的白色距离
int xBlank = smallWidth - smallCircle - smallCircleR1; // 不写smallCircleR1时,凹凸为半圆
int yBlank = smallHeight - smallCircle - smallCircleR1;
// 圆的位置
int rxa = xBlank / 2;
int ryb = smallHeight - smallCircle;
double rPow = Math.pow(smallCircle, 2);
/**
* 计算需要的拼图轮廓(方块和凹凸),用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
* 圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
*/
for (int i = 0; i < smallWidth; i++) {
for (int j = 0; j < smallHeight; j++) {
// 圆在拼图下方内
double topR = Math.pow(i - rxa, 2) + Math.pow(j - 2, 2);
// 圆在拼图下方外
double downR = Math.pow(i - rxa, 2) + Math.pow(j - ryb, 2);
// 圆在拼图左侧内 || (i <= xBlank && leftR <= rPow)
//double leftR = Math.pow(i - 2, 2) + Math.pow(j - rxa, 2);
// 圆在拼图右侧外
double rightR = Math.pow(i - ryb, 2) + Math.pow(j - rxa, 2);
if ((j <= yBlank && topR <= rPow) || (j >= yBlank && downR >= rPow)
|| (i >= xBlank && rightR >= rPow)) {
data[i][j] = 0;
} else {
data[i][j] = 1;
}
}
}
return data;
}
/**
* 裁剪拼图
* @param bigImage - 原图规范大小之后的大图
* @param smallImage - 小图
* @param slideTemplateData - 拼图轮廓数据
* @param x - 坐标x
* @param y - 坐标y
*/
private static void cutByTemplate(BufferedImage bigImage, BufferedImage smallImage, int[][] slideTemplateData, int x, int y) {
int[][] martrix = new int[3][3];
int[] values = new int[9];
//拼图去掉凹凸的白色距离
int xBlank = smallWidth - smallCircle - smallCircleR1; // 不写smallCircleR1时,凹凸为半圆
int yBlank = smallHeight - smallCircle - smallCircleR1;
// 创建shape区域,即原图抠图区域模糊和抠出小图
/**
* 遍历小图轮廓数据,创建shape区域。即原图抠图处模糊和抠出小图
*/
for (int i = 0; i < smallImage.getWidth(); i++) {
for (int j = 0; j < smallImage.getHeight(); j++) {
// 获取大图中对应位置变色
//logger.info("随机生成的坐标:(X,Y)=({},{}),(i,j=({},{}),获取原图大小:{} x {}", x, y, i, j, x + i, y + j);
int rgb_ori = bigImage.getRGB(x + i, y + j);
//0和1,其中0表示没有颜色,1有颜色
int rgb = slideTemplateData[i][j];
if (rgb == 1) {
// 设置小图中对应位置变色
smallImage.setRGB(i, j, rgb_ori);
// 大图抠图区域高斯模糊
readPixel(bigImage, x + i, y + j, values);
fillMatrix(martrix, values);
bigImage.setRGB(x + i, y + j, avgMatrix(martrix));
//边框颜色
Color white = new Color(230,230,230);
Color black = new Color(20,20,20);
//左侧边界,加重高亮阴暗
if (j < yBlank) {
bigImage.setRGB(x, y + j, black.getRGB());
//smallImage.setRGB(0, j, white.getRGB());
}
} else {
// 这里把背景设为透明
smallImage.setRGB(i, j, rgb_ori & 0x00ffffff);
}
}
}
}
/**
* 图片转BASE64
*
* @param image
* @return
* @throws IOException
*/
public static String getImageBASE64(BufferedImage image) throws IOException {
byte[] imagedata = null;
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ImageIO.write(image, "png", bao);
imagedata = bao.toByteArray();
String BASE64IMAGE = Base64.getEncoder().encodeToString(imagedata);
return BASE64IMAGE;
}
/**
* 改变图片大小
*
* @param image
* 原图
* @param width
* 目标宽度
* @param height
* 目标高度
* @return 目标图
*/
public static BufferedImage resizeImage(final Image image, int width, int height, boolean type) {
BufferedImage bufferedImage;
if (type) {
bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
} else {
bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}
final Graphics2D graphics2D = bufferedImage.createGraphics();
graphics2D.setComposite(AlphaComposite.Src);
// below three lines are for RenderingHints for better image quality at cost of
// higher processing time
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.drawImage(image, 0, 0, width, height, null);
graphics2D.dispose();
return bufferedImage;
}
private static void readPixel(BufferedImage img, int x, int y, int[] pixels) {
int xStart = x - 1;
int yStart = y - 1;
int current = 0;
for (int i = xStart; i < 3 + xStart; i++) {
for (int j = yStart; j < 3 + yStart; j++) {
int tx = i;
if (tx < 0) {
tx = -tx;
} else if (tx >= img.getWidth()) {
tx = x;
}
int ty = j;
if (ty < 0) {
ty = -ty;
} else if (ty >= img.getHeight()) {
ty = y;
}
pixels[current++] = img.getRGB(tx, ty);
}
}
}
private static void fillMatrix(int[][] matrix, int[] values) {
int filled = 0;
for (int i = 0; i < matrix.length; i++) {
int[] x = matrix[i];
for (int j = 0; j < x.length; j++) {
x[j] = values[filled++];
}
}
}
private static int avgMatrix(int[][] matrix) {
int r = 0;
int g = 0;
int b = 0;
for (int i = 0; i < matrix.length; i++) {
int[] x = matrix[i];
for (int j = 0; j < x.length; j++) {
if (j == 1) {
continue;
}
Color c = new Color(x[j]);
r += c.getRed();
g += c.getGreen();
b += c.getBlue();
}
}
return new Color(r / 8, g / 8, b / 8).getRGB();
}
}
二、Java实现滑块拼图验证码校验
1、校验思路
Java实现滑块拼图验证码校验思路:
1、后端-获取滑块拼图验证码接口
- 在若干原图中随机一张原图,生成滑块拼图验证码信息。
- 随机一个token,把(X,Y)坐标值保存到 Redis中。
- 返回前端,两个Base64字符串图片信息,Y坐标值和 token。
2、后端-校验滑块拼图验证码接口
- 获取前端传入的 moveX坐标值和 token。
- 通过token,获取 Redis中的 (X,Y)坐标值,注意有效期。
- 校验 moveX与X的差的绝对值是否在阈值误差范围内(比如阈值误差为5)。
- 在阈值误差范围内,返回前端验证通过,否则返回前端验证不通过。
3、 前端
- 调用获取滑块拼图验证码接口,展示图片信息。
- 滑动拼图结束后,调用校验滑块拼图验证码接口。
2、Java接口代码
简单实现代码如下:
/**
* 生成滑块拼图验证码
*
* @param index
* - 本地照片
*/
@RequestMapping(value = "/getImageCode.json", method = RequestMethod.GET)
@ResponseBody
public SliderPuzzleInfo getImageCode(int index, HttpServletRequest request) throws Exception {
int i = index <= 0 ? 1 : index;
File file = new File("D:/TempFiles/slide/slidebase" + i + ".png");
SliderPuzzleInfo sliderPuzzleInfo = SlidePuzzleUtil.createImage(new FileInputStream(file));
if (sliderPuzzleInfo == null) {
System.out.println("图片验证码生成失败");
return sliderPuzzleInfo;
}
File file1 = new File("D:/TempFiles/slide/demo2BigImage.png");
File file2 = new File("D:/TempFiles/slide/demo2SmallImage.png");
ImageIO.write(sliderPuzzleInfo.getBigImage(), "png", file1);
ImageIO.write(sliderPuzzleInfo.getSmallImage(), "png", file2);
HttpSession session = request.getSession();
// 保存到Redis,这里临时存
session.setAttribute("posX", sliderPuzzleInfo.getPosX());
sliderPuzzleInfo.setBigImage(null);
sliderPuzzleInfo.setSmallImage(null);
return sliderPuzzleInfo;
}
/**
* 校验滑块拼图验证码
*
* @param movePosX
* 移动距离
*/
@ResponseBody
@RequestMapping(value = "/verifyImageCode.json", method = RequestMethod.GET)
public Map<String, Object> verifyImageCode(@RequestParam(value = "movePosX") Integer movePosX, HttpServletRequest request) {
Map<String, Object> resultMap = new HashMap<>();
HttpSession session = request.getSession();
try {
if (movePosX == null) {
resultMap.put("errcode", 1);
resultMap.put("errmsg", "参数缺失");
return resultMap;
}
Integer posX = (Integer) session.getAttribute("posX");
if (posX == null) {
resultMap.put("errcode", 1);
resultMap.put("errmsg", "验证过期,请重试");
return resultMap;
}
if (Math.abs(posX - movePosX) > 5) {
resultMap.put("errcode", 1);
resultMap.put("errmsg", "验证不通过");
} else {
resultMap.put("errcode", 0);
resultMap.put("errmsg", "验证通过");
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
} finally {
session.removeAttribute("posX");
}
return resultMap;
}
– 求知若饥,虚心若愚。