最近有个需求,需要添加滑块拼图验证码,网上了解了一些生成校验方式,下面写个 demo实现一下。
一、滑块拼图验证码生成
1、生成思路
滑块拼图验证码生成思路:
在若干原图中随机一张原图,然后改变原图大小为规范的大图对象随机生成(X,Y)坐标创建小图对象随机生成拼图轮廓数据从大图中裁剪拼图。抠原图,裁剪拼图返回滑块拼图验证码信息:两个Base64字符串图片信息和(X,Y)坐标。
注意:
1、随机生成拼图轮廓数据是重点,然后裁剪拼图时根据需要它来抠原图,裁剪拼图。2、拼图的凹凸信息,有两种处理方式 以小图的四边为边缘,外加凸圆弧或者内挖凹圆弧。以小图的四边为最终边界,内加凸圆弧或者内挖凹圆弧,并且处理掉凹凸之外的颜色。
2、生成代码
这里使用 以小图的四边为最终边界,上凹下凸,左无由凸,左边框高亮阴暗简单处理。
定义一个滑块拼图信息实体类:
@Datapublic 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 timegraphics2D.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)@ResponseBodypublic 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;}
– 求知若饥,虚心若愚。