本人结合其它文章中的思路而实现的滑块验证码组件,优点:
- 开箱即用:无需除了
lombok之外的其它依赖,可直接复制到项目中 - 使用方式简单:通过静态的
from()方法即可自动生成滑块验证码信息 - 易于扩展:可以通过自定义的
SlideCanvas组件来自行绘制滑块的形状
import lombok.AllArgsConstructor;
import lombok.Getter;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.function.BiFunction;
import java.util.function.IntBinaryOperator;
/**
* 滑块验证码生成器
*
* @author NightDW 2023/6/20 14:33
*/
@Getter
public class SlideCaptcha {
/**
* 滑块的画布;可以通过该组件来自定义滑块的形状
*/
@AllArgsConstructor
public static abstract class SlideCanvas {
protected static final int BLANK = 0, SLIDE = 1, BORDER = 2;
/**
* 画布的宽度和高度
*/
@Getter
private final int width, height;
/**
* 获取该画布中指定坐标的像素的状态(横轴代表x,纵轴代表y):
* 如果是BLANK,则代表该像素不是滑块的一部分
* 如果是SLIDE,则代表该像素是滑块的内部
* 如果是BORDER,则代表该像素是滑块的边界
*/
public abstract int getSlideState(int x, int y);
}
/**
* 简单的滑块画布;整个画布都是滑块内容,且边界宽度为1px
*/
public static class SimpleSlideCanvas extends SlideCanvas {
public SimpleSlideCanvas(int sideLength) {
this(sideLength, sideLength);
}
public SimpleSlideCanvas(int width, int height) {
super(width, height);
}
@Override
public int getSlideState(int x, int y) {
return x == 0 || y == 0 || x == getWidth() - 1 || y == getHeight() - 1 ? BORDER : SLIDE;
}
}
/**
* 拼图形状的滑块画布;滑块是矩形的,且矩形的边会有半圆形的凸起/凹陷
*/
public static class PuzzleSlideCanvas extends SlideCanvas {
public static final int TOP = 1, BUTTON = 2, LEFT = 4, RIGHT = 8;
/**
* 圆形凸起(或凹陷)的半径及其平方,以及有哪些边需要有凸起
* 假设protrudeSides为5,则上边和左边有凸起,下边和右边有凹陷
*/
private final int radius, radius2, protrudeSides;
/**
* 上下左右四条边各自对应的圆的圆心
*/
private final int[][] circleCenters = new int[RIGHT + 1][];
/**
* 简单的构造器;画布为正方形,且圆形的半径为画布边长的13/84
*/
public PuzzleSlideCanvas(int sideLength, int protrudeSides) {
this(sideLength, sideLength, sideLength * 13 / 84, protrudeSides);
}
/**
* 全参构造器;需指定画布的宽高、圆形的半径、有半圆形的凸起的边
*/
private PuzzleSlideCanvas(int width, int height, int radius, int protrudeSides) {
super(width, height);
this.radius = radius;
this.radius2 = radius * radius;
this.protrudeSides = protrudeSides;
this.circleCenters[TOP] = new int[] { width >> 1, radius };
this.circleCenters[BUTTON] = new int[] { width >> 1, height - radius };
this.circleCenters[LEFT] = new int[] { radius, height >> 1 };
this.circleCenters[RIGHT] = new int[] { width - radius, height >> 1 };
}
@Override
public int getSlideState(int x, int y) {
// 判断当前点是否在圆形中;如果是,则获取到对应的圆形的下标
int circleIdx = getCircleIdx(x, y);
// 如果不在圆形上,则只需判断该点是否在滑块的矩形部分中即可
if (circleIdx == 0) {
return !isInInnerRec(x, y) ? BLANK : isInInnerBorder(x, y) ? BORDER : SLIDE;
}
// 判断该点是否位于圆形的边界,并获取到真正的圆形的下标
boolean isBorder = circleIdx < 0;
circleIdx = Math.abs(circleIdx);
// 如果该点所在的圆形位于凸起的那一侧
// 那么当该点在圆形的边界且不在内部矩形中时,该点位于滑块边界,否则位于滑块内部
if ((protrudeSides & circleIdx) != 0) {
return isBorder && (isInInnerBorder(x, y) || !isInInnerRec(x, y)) ? BORDER : SLIDE;
}
// 如果该点所在的圆形位于凹陷的那一侧
// 那么当该点在圆形的边界且在内部矩形中时,该点位于滑块边界,否则位于滑块外部
return isBorder && isInInnerRec(x, y) ? BORDER : BLANK;
}
/**
* 判断指定坐标是否位于上下左右四个圆形上
* 如果不是,则返回0;否则返回圆形的下标
* 特别地,如果正好位于圆的边上,则返回圆形的下标的负数形式
*/
private int getCircleIdx(int x, int y) {
for (int i = 0; i < circleCenters.length; i++) {
if (circleCenters[i] != null) {
int dx = circleCenters[i][0] - x;
int dy = circleCenters[i][1] - y;
int tem = dx * dx + dy * dy;
if (tem <= radius2) {
return tem == radius2 ? -i : i;
}
}
}
return 0;
}
private boolean isInInnerRec(int x, int y) {
return x >= radius && y >= radius && x <= getWidth() - radius && y <= getHeight() - radius;
}
private boolean isInInnerBorder(int x, int y) {
boolean xIsBorder = x == radius || x == getWidth() - radius;
boolean yIsBorder = y == radius || y == getHeight() - radius;
return (xIsBorder && y >= radius && y <= getHeight() - radius) || (yIsBorder && x >= radius && x <= getWidth() - radius);
}
}
/**
* 默认的用于计算滑块画布的坐标的组件:x轴随机,y轴垂直居中
*/
private static final IntBinaryOperator DEFAULT_X = (imgW, slideW) -> slideW + (int) (Math.random() * (imgW - slideW * 3));
private static final IntBinaryOperator DEFAULT_Y = (imgH, slideH) -> (imgH - slideH) >> 1;
/**
* 背景图片和滑块图片(Base64形式),以及滑块画布的坐标
*/
private final String backgroundImg;
private final String slideImg;
private final int x, y;
/**
* 私有的全参构造器
*/
private SlideCaptcha(String backgroundImg, String slideImg, int x, int y) {
this.backgroundImg = backgroundImg;
this.slideImg = slideImg;
this.x = x;
this.y = y;
}
/**
* 根据图片文件流、滑块画布生成器来生成滑块验证码信息
*/
public static SlideCaptcha from(InputStream file, BiFunction<Integer, Integer, SlideCanvas> slideCanvasGen) throws IOException {
return from(file, slideCanvasGen, DEFAULT_X, DEFAULT_Y);
}
/**
* 根据图片文件流、滑块画布生成器和滑块坐标生成器来生成滑块验证码信息
*/
public static SlideCaptcha from(InputStream file, BiFunction<Integer, Integer, SlideCanvas> slideCanvasGen,
IntBinaryOperator xGen, IntBinaryOperator yGen) throws IOException {
// 获取到原始图片(即背景图片)信息
BufferedImage background = ImageIO.read(file);
// 根据原始图片的大小来创建相应大小的画布
SlideCanvas slideCanvas = slideCanvasGen.apply(background.getWidth(), background.getHeight());
// 计算滑块画布坐标
int x = xGen.applyAsInt(background.getWidth(), slideCanvas.getWidth());
int y = yGen.applyAsInt(background.getHeight(), slideCanvas.getHeight());
// 创建一个空白的滑块图片信息
BufferedImage slide = new BufferedImage(slideCanvas.getWidth(), slideCanvas.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
// 根据滑块画布、滑块坐标来处理背景图片信息和滑块图片信息
apply(background, slide, slideCanvas, x, y);
// 将处理后的背景图片和滑块图片转成Base64形式,并返回相应的SlideCaptcha实例
return new SlideCaptcha(toBase64(background), toBase64(slide), x, y);
}
/**
* 根据滑块画布、滑块坐标来处理背景图片信息和滑块图片信息
*/
private static void apply(BufferedImage background, BufferedImage slide, SlideCanvas slideCanvas, int x, int y) {
// 遍历滑块画布中的每一个像素
for (int slideX = 0, width = slideCanvas.getWidth(); slideX < width; slideX++) {
for (int slideY = 0, height = slideCanvas.getHeight(); slideY < height; slideY++) {
// 获取到当前像素在背景图片中的坐标,并获取当前像素的状态
int bgX = x + slideX;
int bgY = y + slideY;
int slideState = slideCanvas.getSlideState(slideX, slideY);
// 如果当前位置是滑块内部,则将背景图中的该像素的颜色复制到滑块图中,并将背景图中该像素变为灰度图
if (slideState == SlideCanvas.SLIDE) {
slide.setRGB(slideX, slideY, background.getRGB(bgX, bgY));
background.setRGB(bgX, bgY, toGray(background.getRGB(bgX, bgY)));
// 如果当前位置是滑块边界,则将滑块图的该像素置为白色,并将背景图中该像素置灰
} else if (slideState == SlideCanvas.BORDER) {
slide.setRGB(slideX, slideY, Color.WHITE.getRGB());
background.setRGB(bgX, bgY, Color.GRAY.getRGB());
// 如果当前位置不属于滑块,则将滑块图的该像素置为透明
} else {
slide.setRGB(slideX, slideY, background.getRGB(bgX, bgY) & 0x00ffffff);
}
}
}
}
private static String toBase64(BufferedImage image) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image, "png", out);
return Base64.getEncoder().encodeToString(out.toByteArray());
}
private static int toGray(int rgb) {
int b = rgb & 255;
int g = (rgb >> 8) & 255;
int r = (rgb >> 16) & 255;
int avg = (g + b + r) / 8;
return avg | (avg << 8) | (avg << 16);
}
}