Springboot 手搓 后端 滑块验证码生成

目录

一、效果演示

二、后端滑块验证码生成思路

三、原理解析

四、核心代码拿走


滑块验证码react前端实现,见我的这篇博客:前端 React 弹窗式 滑动验证码实现_react中使用阿里云滑块验证码2.0前端接入及相关视觉-CSDN博客

一、效果演示

生成的案例图片:

视频演示:

滑块验证码演示

二、后端滑块验证码生成思路

1、后端需要生成对应的两个图片(拼图图片和拼图背景图片,图片内存尽量小一点)和对应位置(x和y, 等高拼图只需要记录x即可);

2、验证码生成服务,生成唯一的标识uuid(可考虑雪花算法生成),将生成图片生成后得到的位置信息即x(非登高拼图x和y)记录到缓存中,建议使用redis存储,即使分布式也能使用;

3、将验证码数据返回给前端,格式参考如下:

三、原理解析

要想生成拼图形状的拼图,我们需要运用到一些数学知识,核心代码如下:

通过 圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆,>=的在外侧,<的内侧。

简单来看就是这样的一个模型:

 /**
     * 随机生成拼图图轮廓数据
     *
     * @param randomR1 圆点距离随机值
     * @return 0和1,其中0表示没有颜色,1有颜色
     */
    private static int[][] createTemplateData(int randomR1) {
        // 拼图轮廓数据
        int[][] data = new int[puzzleWidth][puzzleHeight];

        // 拼图去掉凹凸的白色距离
        int xBlank = puzzleWidth - distance;
        int yBlank = puzzleHeight - distance;

        // 记录圆心的位置值
        int topOrBottomX = puzzleWidth / 2;
        int leftOrRightY = puzzleHeight / 2;
        // 凹时对应的位置
        int topYOrLeftX = distance - randomR1 + radius;
        int rightX = puzzleWidth - topYOrLeftX;
        int bottomY = puzzleHeight - topYOrLeftX;
        // 凸时对应的位置
        int topYOrLeftXR = distance + randomR1 - radius;
        int rightXR = puzzleWidth - topYOrLeftXR;
        int bottomYR = puzzleHeight - topYOrLeftXR;
        double rPow = Math.pow(radius, 2);

        /* 随机获取判断条件 */
        Random random = new Random();
        Integer[] randomCondition = new Integer[]{
                random.nextInt(3),
                random.nextInt(3),
                random.nextInt(3),
                random.nextInt(3)
        };

        /*
          计算需要的拼图轮廓(方块和凹凸),用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
          圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
         */
        for (int i = 0; i < puzzleWidth; i++) {
            for (int j = 0; j < puzzleHeight; j++) {
                /* 凹时对应的圆点 */
                // 顶部的圆心位置为(puzzleWidth / 2, topYOrLeftX)
                double top = Math.pow(i - topOrBottomX, 2) + Math.pow(j - topYOrLeftX, 2);
                // 底部的圆心位置为(puzzleWidth / 2, puzzleHeight - topYOrLeftX)
                double bottom = Math.pow(i - topOrBottomX, 2) + Math.pow(j - bottomY, 2);
                // 左侧的圆心位置为(topYOrLeftX, puzzleHeight / 2)
                double left = Math.pow(i - topYOrLeftX, 2) + Math.pow(j - leftOrRightY, 2);
                // 右侧的圆心位置为(puzzleWidth - topYOrLeftX, puzzleHeight / 2)
                double right = Math.pow(i - rightX, 2) + Math.pow(j - leftOrRightY, 2);

                /* 凸时对应的圆点 */
                // 顶部的圆心位置为(puzzleWidth / 2, topYOrLeftXR)
                double topR = Math.pow(i - topOrBottomX, 2) + Math.pow(j - topYOrLeftXR, 2);
                // 底部的圆心位置为(puzzleWidth / 2, puzzleHeight - topYOrLeftXR)
                double bottomR = Math.pow(i - topOrBottomX, 2) + Math.pow(j - bottomYR, 2);
                // 左侧的圆心位置为(topYOrLeftXR, puzzleHeight / 2)
                double leftR = Math.pow(i - topYOrLeftXR, 2) + Math.pow(j - leftOrRightY, 2);
                // 右侧的圆心位置为(puzzleWidth - topYOrLeftXR, puzzleHeight / 2)
                double rightR = Math.pow(i - rightXR, 2) + Math.pow(j - leftOrRightY, 2);

                /* 随机获取条件 */
                Boolean[][] conditions = new Boolean[][]{
                        new Boolean[]{
                                (j <= distance && topR >= rPow),
                                (j <= distance || top <= rPow),
                                (j <= distance)
                        },
                        new Boolean[]{
                                (j >= yBlank && bottomR >= rPow),
                                (j >= yBlank || bottom <= rPow),
                                (j >= yBlank)
                        },
                        new Boolean[]{
                                (i <= distance && leftR >= rPow),
                                (i <= distance || left <= rPow),
                                (i <= distance)
                        },
                        new Boolean[]{
                                (i >= xBlank && rightR >= rPow),
                                (i >= xBlank || right <= rPow),
                                (i >= xBlank)
                        }
                };
                boolean hide = false;
                for (int c = 0; c < randomCondition.length; c++) {
                    if (conditions[c][randomCondition[c]]) {
                        hide = true;
                        break;
                    }
                }

                if (hide) {
                    // 不显示的像素
                    data[i][j] = 0;
                } else {
                    data[i][j] = 1;
                }
            }
        }
        return data;
    }

绘制好需要截取的数据位置后,再来进行剪切:

 /**
     * 裁剪拼图
     *
     * @param bgImg             - 原图规范大小之后的大图
     * @param puzzleImg         - 小图
     * @param slideTemplateData - 拼图轮廓数据
     * @param x                 - 坐标x
     * @param y                 - 坐标y
     */
    private static void cutByTemplate(BufferedImage bgImg, BufferedImage puzzleImg, int[][] slideTemplateData, int x, int y) {
        int[][] matrix = new int[3][3];
        int[] values = new int[9];

        // 虚假的x坐标
        int fakeX = getRandomFakeX(x);

        // 创建shape区域,即原图抠图区域模糊和抠出小图
        /*
          遍历小图轮廓数据,创建shape区域。即原图抠图处模糊和抠出小图
         */
        for (int i = 0; i < puzzleImg.getWidth(); i++) {
            for (int j = 0; j < puzzleImg.getHeight(); j++) {
                // 获取大图中对应位置变色
                int rgb_ori = bgImg.getRGB(x + i, y + j);

                // 0和1,其中0表示没有颜色,1有颜色
                int rgb = slideTemplateData[i][j];
                if (rgb == 1) {
                    // 设置小图中对应位置变色
                    puzzleImg.setRGB(i, j, rgb_ori);
                    // 大图抠图区域高斯模糊
                    readPixel(bgImg, x + i, y + j, values);
                    fillMatrix(matrix, values);
                    bgImg.setRGB(x + i, y + j, avgMatrix(matrix));

                    // 抠虚假图
                    readPixel(bgImg, fakeX + i, y + j, values);
                    fillMatrix(matrix, values);
                    bgImg.setRGB(fakeX + i, y + j, avgMatrix(matrix));
                } else {
                    // 这里把背景设为透明
                    puzzleImg.setRGB(i, j, rgb_ori & 0x00ffffff);
                }
            }
        }
    }

四、核心代码拿走

SliderCaptchaUtil:

package com.xloda.common.tool.captcha.util;

import com.xloda.common.tool.captcha.constant.SliderCaptchaConfig;
import com.xloda.common.tool.captcha.pojo.SliderCaptcha;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/**
 * @author Dragon Wu
 * @since 2025/04/23 10:52
 * 滑块验证码生成器
 */

public class SliderCaptchaUtil implements SliderCaptchaConfig {

    /**
     * 生成滑块验证码
     *
     * @param bgImg     1. 传入随机背景图
     * @param accordant 是否生成登高拼图
     * @return SliderCaptcha 验证码结果
     * @throws IOException IO异常
     */
    public static SliderCaptcha generateCaptcha(BufferedImage bgImg, boolean accordant) throws IOException {
        // 2. 随机生成离左上角的(X,Y)坐标,上限为 [width-puzzleWidth, height-puzzleHeight]。最好离大图左边远一点,上限不要紧挨着大图边界
        Random random = new Random();
        // X范围:[puzzleWidth, width - puzzleWidth)
        int x = random.nextInt(width - 2 * puzzleWidth) + puzzleWidth;
        // Y范围:[puzzleHeight, height - puzzleHeight)
        int y = random.nextInt(height - 2 * puzzleHeight) + puzzleHeight;

        // 3. 创建拼图图像
        BufferedImage puzzleImg = new BufferedImage(puzzleWidth, puzzleHeight, BufferedImage.TYPE_4BYTE_ABGR);

        // 4. 随机获取位置数据
        int randomR1 = getRandomR1();

        // 5. 随机生成拼图轮廓数据
        int[][] slideTemplateData = createTemplateData(randomR1);

        // 6. 从大图中裁剪拼图。抠原图,裁剪拼图
        cutByTemplate(bgImg, puzzleImg, slideTemplateData, x, y);

        // 7. 给拼图加边框
        puzzleImg = ImageUtil.addBorderWithOutline(puzzleImg, borderSize, Color.white);

        // 8. 判断是否为登高拼图
        if (accordant) {
            puzzleImg = reshapeAccordant(puzzleImg, y);
            return new SliderCaptcha(ImageUtil.toBase64(bgImg),
                    ImageUtil.toBase64(puzzleImg), x);
        }

        // 非登高拼图,记录x和y
        return new SliderCaptcha(ImageUtil.toBase64(bgImg),
                ImageUtil.toBase64(puzzleImg), x, y);
    }

    // 随机获取小圆距离点
    private static int getRandomR1() {
        Integer[] r1List = new Integer[]{
                radius * 3 / 2,
                radius,
                radius / 2,
        };
        int index = new Random().nextInt(r1List.length);
        return r1List[index];
    }

    /**
     * 随机生成拼图图轮廓数据
     *
     * @param randomR1 圆点距离随机值
     * @return 0和1,其中0表示没有颜色,1有颜色
     */
    private static int[][] createTemplateData(int randomR1) {
        // 拼图轮廓数据
        int[][] data = new int[puzzleWidth][puzzleHeight];

        // 拼图去掉凹凸的白色距离
        int xBlank = puzzleWidth - distance;
        int yBlank = puzzleHeight - distance;

        // 记录圆心的位置值
        int topOrBottomX = puzzleWidth / 2;
        int leftOrRightY = puzzleHeight / 2;
        // 凹时对应的位置
        int topYOrLeftX = distance - randomR1 + radius;
        int rightX = puzzleWidth - topYOrLeftX;
        int bottomY = puzzleHeight - topYOrLeftX;
        // 凸时对应的位置
        int topYOrLeftXR = distance + randomR1 - radius;
        int rightXR = puzzleWidth - topYOrLeftXR;
        int bottomYR = puzzleHeight - topYOrLeftXR;
        double rPow = Math.pow(radius, 2);

        /* 随机获取判断条件 */
        Random random = new Random();
        Integer[] randomCondition = new Integer[]{
                random.nextInt(3),
                random.nextInt(3),
                random.nextInt(3),
                random.nextInt(3)
        };

        /*
          计算需要的拼图轮廓(方块和凹凸),用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
          圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
         */
        for (int i = 0; i < puzzleWidth; i++) {
            for (int j = 0; j < puzzleHeight; j++) {
                /* 凹时对应的圆点 */
                // 顶部的圆心位置为(puzzleWidth / 2, topYOrLeftX)
                double top = Math.pow(i - topOrBottomX, 2) + Math.pow(j - topYOrLeftX, 2);
                // 底部的圆心位置为(puzzleWidth / 2, puzzleHeight - topYOrLeftX)
                double bottom = Math.pow(i - topOrBottomX, 2) + Math.pow(j - bottomY, 2);
                // 左侧的圆心位置为(topYOrLeftX, puzzleHeight / 2)
                double left = Math.pow(i - topYOrLeftX, 2) + Math.pow(j - leftOrRightY, 2);
                // 右侧的圆心位置为(puzzleWidth - topYOrLeftX, puzzleHeight / 2)
                double right = Math.pow(i - rightX, 2) + Math.pow(j - leftOrRightY, 2);

                /* 凸时对应的圆点 */
                // 顶部的圆心位置为(puzzleWidth / 2, topYOrLeftXR)
                double topR = Math.pow(i - topOrBottomX, 2) + Math.pow(j - topYOrLeftXR, 2);
                // 底部的圆心位置为(puzzleWidth / 2, puzzleHeight - topYOrLeftXR)
                double bottomR = Math.pow(i - topOrBottomX, 2) + Math.pow(j - bottomYR, 2);
                // 左侧的圆心位置为(topYOrLeftXR, puzzleHeight / 2)
                double leftR = Math.pow(i - topYOrLeftXR, 2) + Math.pow(j - leftOrRightY, 2);
                // 右侧的圆心位置为(puzzleWidth - topYOrLeftXR, puzzleHeight / 2)
                double rightR = Math.pow(i - rightXR, 2) + Math.pow(j - leftOrRightY, 2);

                /* 随机获取条件 */
                Boolean[][] conditions = new Boolean[][]{
                        new Boolean[]{
                                (j <= distance && topR >= rPow),
                                (j <= distance || top <= rPow),
                                (j <= distance)
                        },
                        new Boolean[]{
                                (j >= yBlank && bottomR >= rPow),
                                (j >= yBlank || bottom <= rPow),
                                (j >= yBlank)
                        },
                        new Boolean[]{
                                (i <= distance && leftR >= rPow),
                                (i <= distance || left <= rPow),
                                (i <= distance)
                        },
                        new Boolean[]{
                                (i >= xBlank && rightR >= rPow),
                                (i >= xBlank || right <= rPow),
                                (i >= xBlank)
                        }
                };
                boolean hide = false;
                for (int c = 0; c < randomCondition.length; c++) {
                    if (conditions[c][randomCondition[c]]) {
                        hide = true;
                        break;
                    }
                }

                if (hide) {
                    // 不显示的像素
                    data[i][j] = 0;
                } else {
                    data[i][j] = 1;
                }
            }
        }
        return data;
    }

    /**
     * 裁剪拼图
     *
     * @param bgImg             - 原图规范大小之后的大图
     * @param puzzleImg         - 小图
     * @param slideTemplateData - 拼图轮廓数据
     * @param x                 - 坐标x
     * @param y                 - 坐标y
     */
    private static void cutByTemplate(BufferedImage bgImg, BufferedImage puzzleImg, int[][] slideTemplateData, int x, int y) {
        int[][] matrix = new int[3][3];
        int[] values = new int[9];

        // 虚假的x坐标
        int fakeX = getRandomFakeX(x);

        // 创建shape区域,即原图抠图区域模糊和抠出小图
        /*
          遍历小图轮廓数据,创建shape区域。即原图抠图处模糊和抠出小图
         */
        for (int i = 0; i < puzzleImg.getWidth(); i++) {
            for (int j = 0; j < puzzleImg.getHeight(); j++) {
                // 获取大图中对应位置变色
                int rgb_ori = bgImg.getRGB(x + i, y + j);

                // 0和1,其中0表示没有颜色,1有颜色
                int rgb = slideTemplateData[i][j];
                if (rgb == 1) {
                    // 设置小图中对应位置变色
                    puzzleImg.setRGB(i, j, rgb_ori);
                    // 大图抠图区域高斯模糊
                    readPixel(bgImg, x + i, y + j, values);
                    fillMatrix(matrix, values);
                    bgImg.setRGB(x + i, y + j, avgMatrix(matrix));

                    // 抠虚假图
                    readPixel(bgImg, fakeX + i, y + j, values);
                    fillMatrix(matrix, values);
                    bgImg.setRGB(fakeX + i, y + j, avgMatrix(matrix));
                } else {
                    // 这里把背景设为透明
                    puzzleImg.setRGB(i, j, rgb_ori & 0x00ffffff);
                }
            }
        }
    }

    /**
     * 随机获取虚假x坐标的值
     *
     * @param x 真正的x坐标
     * @return fakeX
     */
    private static int getRandomFakeX(int x) {
        int puzzleRealWidth = puzzleWidth + 2 * borderSize + 2;
        Random random = new Random();
        int fakeX = random.nextInt(width - 2 * puzzleRealWidth) + puzzleRealWidth;
        if (Math.abs(fakeX - x) <= puzzleRealWidth) {
            fakeX = width - x;
        }
        return fakeX;
    }

    /**
     * 通过拼图图片生成登高拼图图片
     *
     * @param puzzleImg 拼图图片
     * @param offsetY   随机生成的y
     * @return 登高拼图图片
     */
    private static BufferedImage reshapeAccordant(BufferedImage puzzleImg, int offsetY) {
        BufferedImage puzzleBlankImg = new BufferedImage(puzzleWidth + 2 * borderSize + 2, height, BufferedImage.TYPE_4BYTE_ABGR);
        Graphics2D graphicsPuzzle = puzzleBlankImg.createGraphics();
        graphicsPuzzle.drawImage(puzzleImg, 1, offsetY, null);
        graphicsPuzzle.dispose();
        return puzzleBlankImg;
    }

    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 int avgMatrix(int[][] matrix) {
        int r = 0;
        int g = 0;
        int b = 0;
        for (int[] x : matrix) {
            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();
    }

    private static void fillMatrix(int[][] matrix, int[] values) {
        int filled = 0;
        for (int[] x : matrix) {
            for (int j = 0; j < x.length; j++) {
                x[j] = values[filled++];
            }
        }
    }
}

RandomUtil:

package com.xloda.common.tool.captcha.util;

import com.xloda.common.tool.captcha.constant.CaptchaConstants;

import java.util.Random;

/**
 * @author Dragon Wu
 * @since 2025/04/23 18:07
 * 随机生成器工具
 */

public class RandomUtil {

    // 随机获取背景图路径
    public static String randomBgImgPath() {
        int index = new Random().nextInt(CaptchaConstants.BG_IMAGES.length);
        return CaptchaConstants.BG_IMAGES[index];
    }
}

ImageUtil:

package com.xloda.common.tool.captcha.util;

import com.xloda.common.tool.captcha.constant.CaptchaConstants;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

/**
 * @author Dragon Wu
 * @since 2025/04/25 10:18
 * 图片处理工具
 */

public class ImageUtil {
    public static BufferedImage addBorderWithOutline(BufferedImage image, int borderWidth, Color borderColor) {
        // 创建新图像,尺寸扩大以容纳边框
        BufferedImage result = new BufferedImage(
                image.getWidth() + borderWidth * 2,
                image.getHeight() + borderWidth * 2,
                BufferedImage.TYPE_INT_ARGB
        );

        Graphics2D g2d = result.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // 获取图像的非透明区域
        Area area = new Area();
        for (int y = 0; y < image.getHeight(); y++) {
            for (int x = 0; x < image.getWidth(); x++) {
                if ((image.getRGB(x, y) >> 24) != 0x00) {
                    area.add(new Area(new Rectangle(x, y, 1, 1)));
                }
            }
        }

        // 绘制边框
        g2d.setColor(borderColor);
        g2d.setStroke(new BasicStroke(borderWidth * 2));
        g2d.translate(borderWidth, borderWidth);
        g2d.draw(area);

        // 绘制原始图像
        g2d.drawImage(image, 0, 0, null);
        g2d.dispose();

        return result;
    }

    // 图片转Base64
    public static String toBase64(BufferedImage image) throws IOException {
        // 创建一个字节数组输出流
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        // 将BufferedImage写入到输出流中,这里指定图片格式为"png"或"jpg"等
        ImageIO.write(image, CaptchaConstants.IMG_FORMAT, os);
        // 将输出流的字节数组转换为Base64编码的字符串
        String imageBase64 = Base64.getEncoder().encodeToString(os.toByteArray());
        // 关闭输出流
        os.close();
        return CaptchaConstants.BASE64_PREFIX + imageBase64;
    }
}

SliderCaptcha:

package com.xloda.common.tool.captcha.pojo;

import lombok.*;

/**
 * @author Dragon Wu
 * @since 2025/04/23 10:49
 * 滑块验证码
 */
@AllArgsConstructor
@Getter
@ToString
public class SliderCaptcha {
    // 验证码背景图
    private String bgImg;

    // 验证码滑块
    private String puzzleImg;

    // 验证码正确的x位置(此值需自行存入缓存,用于验证码判断)
    private int x;

    // 等高拼图时,返回0(非登高拼图,此值需自行存入缓存,用于验证码判断)
    private int y;

    public SliderCaptcha(String bgImg, String puzzleImg, int x) {
        this.bgImg = bgImg;
        this.puzzleImg = puzzleImg;
        this.x = x;
    }
}

CaptchaConstants:

package com.xloda.common.tool.captcha.constant;

/**
 * @author Dragon Wu
 * @since 2025/04/23 10:53
 */
public interface CaptchaConstants {
    // 图片格式
    String IMG_FORMAT = "png";

    // base64前缀
    String BASE64_PREFIX = "data:image/" + IMG_FORMAT + ";base64,";

    // 图片存储的目录
    String FOLDER = "/static/img/captcha/";

    // 背景图列表(引入依赖后,记得在项目资源目录的该路径下添加对应图片)
    String[] BG_IMAGES = new String[]{
            FOLDER + "bg01.png",
            FOLDER + "bg01.png"
    };
}

SliderCaptchaConfig

package com.xloda.common.tool.captcha.constant;

/**
 * @author Dragon Wu
 * @since 2025/04/23 11:09
 * 滑块验证码的配置
 */

public interface SliderCaptchaConfig {
    // 大图宽度(原图裁剪拼图后的背景图)
    int width = 280;
    // 大图高度
    int height = 173;
    // 小图宽度(滑块拼图),前端拼图的实际宽度:puzzleWidth + 2 * borderSize + 2
    int puzzleWidth = 66;
    // 小图高度,前端拼图的实际高度:puzzleHeight + 2 * borderSize + 2
    int puzzleHeight = 66;
    // 边框厚度
    int borderSize = 1;
    // 小圆半径,即拼图上的凹凸轮廓半径
    int radius = 8;
    // 图片一周预留的距离,randomR1最大值不能超过radius * 3 / 2
    int distance = radius * 3 / 2;
}

接下来继续手搓旋转验证码前后端。

本节,总结到此,学点数学挺有用的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值