滑动验证码(前后端完整逻辑与代码)

滑动验证码(前后端完整逻辑与代码)

验证码的作用

1、为了防止机器冒充人类做账号密码的暴力破解:调用接口或者模仿用户行为,大批量的尝试账号密码登录,就是为了获取真实的账号密码

2、防止大规模在线注册滥用服务:批量注册大量无用的账号信息,给服务器增加压力

3、防止滥用在线批量化操作:比如在投票的时候,有些恶意刷票软件就可以实现批量化投票功能

4、防止信息被大量采集聚合:互联网时代,最有价值的就是内容生产,精心创作的原创文章,而验证码可以防止机器批量的爬取数据

效果图

在这里插入图片描述

本文需要用到的关键技术

node 16.20.2、antd、react、jdk 1.8、springboot、redis

前端

一、依赖rc-slider-captcha

需要先添加滑动验证码生成的库

yarn add rc-slider-captcha

二、前端代码(该自定义组件主要用于登录,如果用于其他验证请适当修改)

1、思路

  • 用户输入登录信息后,点击登录

  • 打开验证码弹窗,保存用户输入的登录信息传入验证码弹窗组件

  • 请求后端获取验证码的信息(图片)

  • 在验证码弹窗中滑动滑块至目标x坐标,有三种情况

    • 1、如果登录成功 则跳转

    • 2、如果验证码验证失败 则刷新验证码

    • 3、如果验证码验证成功,但输入用户信息有误 则关闭弹窗,让用户自己再输入登录信息

2、自定义滑动验证码弹窗组件,可直接复制使用

/* eslint-disable @typescript-eslint/ban-types */
import React from "react";
// 滑块验证码的组件
import SliderCaptcha from "rc-slider-captcha";
// 弹窗组件 如果没有用到 antd 也可以自己写个弹窗组件
import { Modal } from "antd";

// 后端返回的验证码结构,并且也是 SliderCaptcha 组件所需要的参数
export type CaptchaInfo = {
    bgUrl: string;      // 背景图
    puzzleUrl: string;  // 拼图
};

// 登录信息
export type LoginInfo = {
    // 登录名
    loginId: string;
    // 密码
    password: string;
    // x轴偏移量
    x: string;
    // uuid 用于获取redis保存的x轴偏移量
    uuid: string;
};

// 后端返回的验证码结构
export type ResultCaptchaInfo = {
    code: string;
    obj: { bgUrl: string; puzzleUrl: string; uuid: string; };
    msg: string;
}


// 后端返回的登录信息
export type ResultLoginIno = {
    // 返回的 自定义code
    code: string;
    // 返回的参数
    obj: any;
    // 错误\成功 信息
    msg: string;
}

const ModalSliderCaptcha: React.FC<{
    // 是否打开
    open: boolean,
    // 关闭时调用
    onCancel: () => void,
    // 登录信息(从登录页面获取)
    // 该组件(ModalSliderCaptcha)只有UUID、X轴偏移量,其他信息需要通过该对象传递
    loginData: LoginInfo,
    // 向后端请求
    // true代表 向后端请求后 登录成功或者登录账号密码有误(验证码验证成功,不管登录失败与否都需要关闭弹窗)
    // false或者为空代表 向后端请求后 验证码失败(不关闭弹窗)
    onVerify: (data?: LoginInfo) => Promise<boolean | undefined>,
    // 请求后端 发送验证码图片
    request: () => Promise<ResultCaptchaInfo>,
    // Modal组件的属性,见:https://ant.design/components/modal-cn#api
    modalProps?: {},
    // SliderCaptcha的属性,详情见:https://www.npmjs.com/package/rc-slider-captcha
    sliderCaptchaProps?: {},
}> = React.memo(({
    modalProps, sliderCaptchaProps, open, onCancel, onVerify, request, loginData
}) => {
    // 图片尺寸
    const bgSize = {
        width: 380,
        height: 200,
    }
    // 拼图宽度
    const puzzleWidth = 70

    // uuid 用于向后端传递,从redis中取出值,校验验证码是否正确
    const [uuid, setUuid] = React.useState<string>("")

    // 请求验证码
    const requestCaptcha = async () => {
        return request().then((res: ResultCaptchaInfo): CaptchaInfo => {
            // 每次请求都要重新设置UUID
            setUuid(res?.obj?.uuid)
            // 返回给 SliderCaptcha 组件所需要的验证码参数
            return {
                bgUrl: res?.obj.bgUrl,
                puzzleUrl: res?.obj?.puzzleUrl,
            } as CaptchaInfo
        })
    }

    // 验证验证码(登录校验)
    const onVerifyCaptcha: any = async (data?: LoginInfo) => {
        const params = {
            // 登录页面传入的参数,包括登录名、密码等信息
            // ps:后面我想了一下,可以通过 onVerify() 把loginData这个参数包进去,你们也可以自己优化一下
            ...loginData,
            // SliderCaptcha 组件返回的参数,包括x轴偏移量等信息
            ...data,
            // uuid 为每次刷新验证码时保存,登录校验需要传给后端
           uuid,
        }
        // 后端验证
        let isOK = false
        // onVerify(params) 方法已经做了登录校验,逻辑由登录页面实现
        // .then 后面的参数 res 只是由 onVerify(params) 判断登录成功与否,再控制是否关闭弹窗
        await onVerify(params).then((res: boolean | undefined) => {
            // 判断是否关闭弹窗 onVerify(params) 方法返回
            // true代表验证成功(仅验证码验证成功,不包括登录成功,关闭弹窗),false代表验证失败(不关闭弹窗)
            if (res) {
                // 后端校验通过,验证成功
                isOK = true
            }
        })
        if (isOK) {
            // 关闭弹窗
            onCancel()
            // SliderCaptcha 组件 不需要再请求刷新验证码
            return Promise.resolve()
        }
        // 刷新验证码
        return Promise.reject(new Error("验证失败"))
    }

    // antd 弹窗组件属性
    const modelDefaultProps = {
        title: "安全验证",
        zIndex: 1024,
        style: {
            maxWidth: "100%",
        },
        styles: {
            content: {
                padding: 20
            }
        },
        centered: true,
        width: 430,
        footer: false,
        destroyOnClose: true,
        ...modalProps
    }

    return (
        // Modal组件的属性,见:https://ant.design/components/modal-cn#api
        <Modal
            {...modelDefaultProps}
            onCancel={onCancel}
            open={open}
        >
        {/* SliderCaptcha的属性,详情见:https://www.npmjs.com/package/rc-slider-captcha */}
            <SliderCaptcha
                request={requestCaptcha}
                onVerify={onVerifyCaptcha}
                bgSize={bgSize}
                tipText={{
                    default: '向右拖动完成拼图 👉',
                    loading: "👩🏻‍💻🧑‍💻努力中...",
                    moving: '继续向右拖动 →',
                    verifying: '正在验证...',
                    error: '验证失败,请重试',
                    success: '验证成功!'
                }}
                puzzleSize={{
                    width: puzzleWidth,
                }}
                style={{
                    "--rcsc-primary": "#6153FC",
                    "--rcsc-primary-light": "#efecfc"
                }}
                loadingDelay={300}
                limitErrorCount={3}
                {...sliderCaptchaProps}
            />
        </Modal>
    )
});
export default ModalSliderCaptcha;

3、登录页面传入组件ModalSliderCaptcha部分函数逻辑,不可直接复制使用

// 弹窗关闭与否
const [modalSliderCaptchaOpen, setModalSliderCaptchaOpen] = useState<boolean>(false)
// 关闭弹窗
const onCancel = () => {
    setModalSliderCaptchaOpen(false)
}
// 登录信息
const [loginInfo, setLoginInfo] = useState<LoginInfo>();
// 向后端获取验证码内容(图片)
const getCaptcha = () => {
    return request('getSliderCaptcha', {
        method: 'POST',
    });
}
// 登录
const submit = async (v: any) => {
    // v 为登录参数 password 因为需要加密 所以需要重写、覆盖
    const values = {
      ...v,
      password: encodeStr(v.password, publicKey)
    }
    // 从其他地方调用登录逻辑(请求后端、验证登录成功与否、参数保存、页面跳转等等)
    const { dispatch } = props;
    // 判断是否关闭验证码弹窗(判断滑块验证码是否有效)
    let flag = true;
    // 登录校验
    await dispatch({
      type: 'login',
      payload: { ...values, type },
    }).then((f: boolean) => {
      flag = f;
    });
	// 用于需要传给 ModalSliderCaptcha 是否需要关闭弹窗、是否需要重新请求获取验证码
    return Promise.resolve(flag); 
};
// antd登录组件 
// 从上往下数 第一个...是 ProForm 的组件参数
// 从上往下数 第二个...是 登录id 登录密码 等组件
<ProForm
    ...
    // onFinish 为 用户点击登录时的操作
    // values 为 登录id 登录密码 等组件的输入值
        onFinish={(values: any) => {
        // 打开自定义的弹窗滑动验证码组件
          setModalSliderCaptchaOpen(true)
        // 保存登录时 用户输入的信息
          setLoginInfo(values)
          return Promise.resolve();
        }}
>
    ...
</ProForm>
// 自定义的弹窗滑动验证码组件
<ModalSliderCaptcha open={modalSliderCaptchaOpen} onCancel={onCancel} request={getCaptcha} loginData={loginInfo!} onVerify={submit} />

后端

本文基于 Springboot 手搓 后端 滑块验证码生成 做改动和优化,非常感谢

一、思路

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

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

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

{
    "msg": "成功",
    "obj": {
        "bgUrl": "data:image/png;base64,img信息",
        "puzzleUrl": "data:image/png;base64,img信息",
        "uuid": "e8190fee-9787-44cd-a241-0021f61aa5f3"
    },
    "code": "0000"
}

二、原理解析

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

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

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

在这里插入图片描述

三、代码

1、获取验证码逻辑,不可直接复制

    public Results<GetCaptchaVo> getCaptcha() {
        try {
            // 获取滑块验证码图片
            SliderCaptchaUtil.SliderCaptcha sliderCaptcha = SliderCaptchaUtil.generateCaptcha();
            // 用于登录校验 存redis
            String uuid = UUID.randomUUID().toString();
            // x轴信息 存redis
            redisUtil.set(RedisUtil.SLIDER_CAPTCHA + uuid, String.valueOf(sliderCaptcha.getX()), 120);
            // 返回前端的信息 GetCaptchaVo 仅有三个参数 uuid、puzzleUrl、bgUrl
            GetCaptchaVo getCaptchaVo = new GetCaptchaVo()
                    .setUuid(uuid)
                    .setPuzzleUrl(sliderCaptcha.getPuzzleImg())
                    .setBgUrl(sliderCaptcha.getBgImg());
            return Results.success(getCaptchaVo);
        } catch (Exception e) {
            log.error("getCaptcha 报错", e);
            return Results.failed(e.getMessage());
        }
    }

2、SliderCaptchaUtil工具类,可直接复制使用

/**
 * 滑块验证码生成器 加 todo 的是需要改的
 */
@Slf4j
public class SliderCaptchaUtil {

    /**
     * 图片存储的目录 todo
     */
    private static final String IMAGE_DIR = "classpath:static/img";

    /**
     * 图片格式
     */
    private static final String IMG_FORMAT = "png";

    /**
     * base64前缀
     */
    private static final String BASE64_PREFIX = "data:image/" + IMG_FORMAT + ";base64,";

    /**
     * 图片缓存
     */
    private static final List<BufferedImage> cachedPngImages = new ArrayList<>();


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


    /**
     * 随机获取背景图
     */
    public static BufferedImage randomBgImg() throws IOException {
        try {
            if (cachedPngImages.isEmpty()) {
                // 使用Spring的资源解析器
                ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
                Resource[] resources = resolver.getResources(IMAGE_DIR + "**/*." + IMG_FORMAT);

                if (resources.length == 0) {
                    log.error("randomBgImg 在目录中找不到PNG文件: {}", IMAGE_DIR);
                    throw new IOException("在目录中找不到PNG文件: " + IMAGE_DIR);
                }

                // 读取所有图片到内存
                for (Resource resource : resources) {
                    try (InputStream is = resource.getInputStream()) {
                        // 读取原始图片
                        BufferedImage originalImage = ImageIO.read(is);
                        // 调整尺寸(例如:调整为 width x height)
                        BufferedImage bufferedImage = ImageUtil.resizeImage(originalImage, width, height);
                        cachedPngImages.add(bufferedImage);
                    }
                }
            }

            // 随机选择一张图片
            int index = new Random().nextInt(cachedPngImages.size());
            return cachedPngImages.get(index);
        } catch (IOException e) {
            log.error("randomBgImgPath 获取随机图片失败");
            throw e;
        }
    }


    /**
     * 生成滑块验证码
     */
    public static SliderCaptcha generateCaptcha() throws IOException {
        BufferedImage bufferedImage = randomBgImg();
        return generateCaptcha(bufferedImage);
    }

    /**
     * 生成滑块验证码
     *
     * @param bgImg     1、传入随机背景图
     * @return SliderCaptcha 验证码结果
     * @throws IOException IO异常
     */
    public static SliderCaptcha generateCaptcha(BufferedImage bgImg) throws IOException {
        // 2、复制一份图片,在这张图片上面改动
        bgImg = ImageUtil.deepCopy(bgImg);
        // 3、随机生成离左上角的(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;

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

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

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

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

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

        // 9、设置拼图的高度
        puzzleImg = reshapeAccordant(puzzleImg, y);
        return new SliderCaptcha(ImageUtil.toBase64(bgImg),
                ImageUtil.toBase64(puzzleImg), x);
    }

    /**
     * 随机获取小圆距离点
     */
    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);
        // 虚假的y坐标
        int fakeY = getRandomFakeY(y);

        // 创建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, false));

                    // 抠虚假图
                    readPixel(bgImg, fakeX + i, fakeY + j, values);
                    fillMatrix(matrix, values);
                    bgImg.setRGB(fakeX + i, fakeY + j, avgMatrix(matrix, true));
                } 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;

        // 计算左右有效范围
        int leftStart = puzzleRealWidth;
        int leftEnd = x - puzzleRealWidth;
        int leftAvailable = Math.max(0, leftEnd - leftStart + 1);

        int rightStart = x + puzzleRealWidth;
        int rightEnd = width - puzzleRealWidth;
        int rightAvailable = Math.max(0, rightEnd - rightStart + 1);

        int totalAvailable = leftAvailable + rightAvailable;

        if (totalAvailable == 0) {
            throw new IllegalArgumentException("无法生成不重叠的位置");
        }

        Random rand = new Random();
        int randomChoice = rand.nextInt(totalAvailable);

        // 确定最终生成的fakeX
        if (randomChoice < leftAvailable) {
            return leftStart + randomChoice;
        } else {
            return rightStart + (randomChoice - leftAvailable);
        }
    }

    /**
     * 随机获取虚假y坐标的值
     *
     * @param y 真正的y坐标
     * @return fakeY
     */
    private static int getRandomFakeY(int y) {
        int puzzleRealHeight = puzzleHeight + 2 * borderSize + 2;
        Random random = new Random();
        int fakeY = random.nextInt(height - 2 * puzzleRealHeight) + puzzleRealHeight;
        if (Math.abs(fakeY - y) <= puzzleRealHeight) {
            fakeY = height - y;
        }
        return fakeY;
    }

    /**
     * 通过拼图图片生成登高拼图图片
     *
     * @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, boolean fake) {
        // 假图片要比真图片颜色深
        int colorChange = 10;
        if (fake) {
            colorChange = 20;
        }
        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 / colorChange, g / colorChange, b / colorChange).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++];
            }
        }
    }

    /**
     * 滑动验证码输出实体类
     */
    @Getter
    @ToString
    public static class SliderCaptcha {
        /**
         * 验证码背景图
         */
        private final String bgImg;

        /**
         * 验证码滑块
         */
        private final String puzzleImg;

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

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

    /**
     * 图片处理工具
     */
    static class ImageUtil {
        /**
         * 添加带轮廓的边框
         *
         * @param image
         * @param borderWidth
         * @param borderColor
         * @return
         */
        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
         *
         * @param image
         * @return
         * @throws IOException
         */
        public static String toBase64(BufferedImage image) throws IOException {
            // 创建一个字节数组输出流
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            // 将BufferedImage写入到输出流中,这里指定图片格式为"png"或"jpg"等
            ImageIO.write(image, IMG_FORMAT, os);
            // 将输出流的字节数组转换为Base64编码的字符串
            String imageBase64 = Base64.getEncoder().encodeToString(os.toByteArray());
            // 关闭输出流
            os.close();
            return BASE64_PREFIX + imageBase64;
        }


        /**
         * 调整 BufferedImage 的尺寸
         *
         * @param originalImage 原始图片
         * @param targetWidth   目标宽度
         * @param targetHeight  目标高度
         * @return 调整后的 BufferedImage
         */
        public static BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) {
            // 创建新的 BufferedImage
            BufferedImage resizedImage = new BufferedImage(
                    targetWidth,
                    targetHeight,
                    originalImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : originalImage.getType()
            );

            // 获取 Graphics2D 对象进行缩放绘制
            Graphics2D g2d = resizedImage.createGraphics();
            g2d.drawImage(
                    originalImage,
                    0, 0, targetWidth, targetHeight,
                    null
            );
            g2d.dispose(); // 释放资源

            return resizedImage;
        }

        /**
         * 深度复制 BufferedImage
         *
         * @param original 原始图片
         * @return 复制后的 BufferedImage
         */
        public static BufferedImage deepCopy(BufferedImage original) {
            // 创建一个新的 BufferedImage,类型与原图相同
            BufferedImage copy = new BufferedImage(
                    original.getWidth(),
                    original.getHeight(),
                    original.getType()
            );

            // 绘制原图到新图
            copy.getGraphics().drawImage(original, 0, 0, null);
            return copy;
        }
    }
}

3、登录验证码校验逻辑、不可直接复制

    /**
     * 校验滑块验证码
     */
    private boolean verifySliderCaptcha(String uuid, BigDecimal x) {
        // 从 Redis 获取存储的滑块验证码坐标
        Object o = redisUtil.get(RedisUtil.SLIDER_CAPTCHA + uuid);
        // 如果 Redis 中没有记录,验证失败
        if (o == null) {
            return false;
        }
        try {
            String redisX = (String) o;
            // 将 redisX 转换为 BigDecimal 进行比较
            BigDecimal storedX = new BigDecimal(redisX);

            // 计算差值的绝对值
            BigDecimal difference = x.subtract(storedX).abs();

            // 判断差值是否小于 10
            return difference.compareTo(BigDecimal.TEN) < 0;
        } catch (NumberFormatException e) {
            // 如果 redisX 不是有效的数字格式,验证失败
            return false;
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我认不到你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值