自己动手写一个滑动验证码组件(后端为Spring Boot项目)

近期参加的项目,主管丢给我一个任务,说要支持滑动验证码。我身为50岁的软件攻城狮,当时正背着双手,好像一个受训的保安似的,中规中矩地参加每日站会,心想滑动验证码在今时今日已经是标配了,司空见惯,想必网上一搜一大把,岂非手到擒来。so easy,妈妈再也不用担心我的工作与学习。

孰料在网上寻寻觅觅点点击击,结果就是凄凄惨惨戚戚。好像提的最多的就是AJ-Captcha,但居然貌似下线了,文档打不开,demo也不见。还有一个声称可能是最好的滑动验证码,但好像很复杂,并且日本少女漫画风,跟我有代沟。有一个貌似跟Ant Design有点关联的组件,叫Wetrial的,好像还比较符合我的要求。但它只有前端,没有给出后端实现,并且它的前端好像也用不了。

但是,这个Wetrial.SliderCaptcha阐述了从后端获得的数据,仿佛制订了一个滑动验证码的接口标准。加上我在搜索过程中,看到的一些具体提示,有了一些思路。考虑到这个滑动验证,不仅要给自己的web端使用,还要开放给开发手机APP的外包人员调用,因此需要可控、便利、清晰,决定自己搞一个。

一、思路

1、背景图片和拼图图片都从后端,以base64的方式返回给前端
2、一起返回给前端的是一个json对象,包括背景和拼图内容、尺寸、token。token的作用是验证时即销毁,避免重放攻击,即每张背景图只验证一次
3、准备多张相同尺寸,不同内容的背景图,每次随机选一张
4、拼图从背景图中抠,抠后的坑填上白色,然后采集背景图的颜色,生成噪点加入这个坑。为的是避免机器容易识别这个白坑。

在chapGPT的指导下,历时一天,终于搞了个demo。效果如下

在这里插入图片描述

滑动验证

二、后端

后端就2个接口,一个供数据下载,一个供验证。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;

@RestController
public class CaptchaController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private String[] images;
    int puzzlePieceWidth = 40;
    int puzzlePieceHeight = 40;

    @PostConstruct
    public void init() throws IOException {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources("classpath:/images/*.jpg");  // 修改为 *.jpg
        images = new String[resources.length];
        for (int i = 0; i < resources.length; i++) {
            images[i] = resources[i].getURI().toString();
        }
    }

    @GetMapping("/slideCaptcha")
    public Map<String, Object> getCaptcha() throws IOException {
        Map<String, Object> response = new HashMap<>();

        // 生成唯一的 token
        String token = UUID.randomUUID().toString();

        // 随机选择背景图像
        BufferedImage backgroundImage = getBgImg();

        // 生成拼图块的随机位置
        int puzzlePieceLeft = (int) (Math.random() * (backgroundImage.getWidth() - puzzlePieceWidth));
        int puzzlePieceTop = (int) (Math.random() * (backgroundImage.getHeight() - puzzlePieceHeight));

        // 创建拼图块
        BufferedImage puzzlePieceImage = new BufferedImage(puzzlePieceWidth, puzzlePieceHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D puzzleG = puzzlePieceImage.createGraphics();
        puzzleG.drawImage(backgroundImage, 0, 0, puzzlePieceWidth, puzzlePieceHeight, puzzlePieceLeft, puzzlePieceTop, puzzlePieceLeft + puzzlePieceWidth, puzzlePieceTop + puzzlePieceHeight, null);
        puzzleG.dispose();

        // 在背景图像上掩盖拼图块
        setMask(backgroundImage, puzzlePieceLeft, puzzlePieceTop);

        // 将图像转换为 Base64
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(backgroundImage, "jpg", baos);  // 保持为 "jpg"
        String backgroundImageBase64 = Base64.getEncoder().encodeToString(baos.toByteArray());

        baos.reset();
        ImageIO.write(puzzlePieceImage, "png", baos);  // 保持为 "png" 以支持透明度
        String puzzlePieceBase64 = Base64.getEncoder().encodeToString(baos.toByteArray());

        // 缓存 token 和位置
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        ops.set(token, String.valueOf(puzzlePieceLeft), 5, TimeUnit.MINUTES);

        response.put("backgroundImage", backgroundImageBase64);
        response.put("puzzlePiece", puzzlePieceBase64);
        response.put("token", token);
        //response.put("puzzlePieceLeft", puzzlePieceLeft);
        //response.put("puzzlePieceTop", puzzlePieceTop);
        response.put("backgroundWidth", backgroundImage.getWidth());
        response.put("backgroundHeight", backgroundImage.getHeight());
        response.put("puzzlePieceWidth", puzzlePieceWidth);
        response.put("puzzlePieceHeight", puzzlePieceHeight);

        return response;
    }

    @PostMapping("/slideVerify")
    public Map<String, Object> verifyCaptcha(HttpServletRequest request, @RequestBody Map<String, Object> map) {
        Map<String, Object> response = new HashMap<>();
        String token = (String) map.get("token");
        int position = (Integer) map.get("position");

        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        String correctPositionStr = ops.get(token);

        if (correctPositionStr != null) {
            int correctPosition = Integer.parseInt(correctPositionStr);
            if (Math.abs(position - correctPosition) < 10) {
                response.put("success", true);
            } else {
                response.put("success", false);
            }
            redisTemplate.delete(token);
        } else {
            response.put("success", false);
        }

        return response;
    }

    private BufferedImage getBgImg() throws IOException {
        String selectedImage = images[(int) (Math.random() * images.length)];
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource resource = resolver.getResource(selectedImage);
        InputStream inputStream = resource.getInputStream();
        return ImageIO.read(inputStream);
    }

    private void setMask(BufferedImage backgroundImage, int puzzlePieceLeft, int puzzlePieceTop) {
        Graphics2D g = backgroundImage.createGraphics();
        g.setComposite(AlphaComposite.Src);
        g.setColor(Color.WHITE);  // 使用白色填充
        g.fillRect(puzzlePieceLeft, puzzlePieceTop, puzzlePieceWidth, puzzlePieceHeight);

        // 从整幅背景图像采集颜色
        Color[][] sampledColors = new Color[backgroundImage.getWidth()][backgroundImage.getHeight()];
        for (int x = 0; x < backgroundImage.getWidth(); x++) {
            for (int y = 0; y < backgroundImage.getHeight(); y++) {
                sampledColors[x][y] = new Color(backgroundImage.getRGB(x, y));
            }
        }
        for (int i = puzzlePieceLeft; i < puzzlePieceLeft + puzzlePieceWidth; i++) {
            for (int j = puzzlePieceTop; j < puzzlePieceTop + puzzlePieceHeight; j++) {
                // 获取背景区域的颜色
                Color noiseColor = sampledColors[(int) (Math.random() * i)][(int) (Math.random() * j)];
                // 绘制扰乱元素
                g.setColor(noiseColor);
                g.fillRect(i, j, 1, 1); // 绘制单个像素点,覆盖原始的白色矩形
            }
        }
        g.dispose();
    }
}

三、前端

demo使用经典的html + js + css来编写。注意请求后台的接口路径采用了nginx进行转发,避免浏览器的跨域限制.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Captcha Verification</title>
    <style>
        .captcha-container {
            position: relative;
            width: 367px;
            height: 267px;
            margin: 50px auto;
            border: 1px solid #ddd;
            background-color: #f3f3f3;
        }
        .background-image {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }
        .puzzle-piece {
            position: absolute;
            width: 40px;
            height: 40px;
            cursor: move;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); /* 添加阴影效果 */
        }
        .slider-container {
            width: 400px;
            margin: 20px auto;
            text-align: center;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .slider {
            width: 100%;
            -webkit-appearance: none; /* 去除默认样式 */
            appearance: none;
            height: 10px; /* 设置滑道高度 */
            background: #ddd; /* 滑道背景色 */
            border-radius: 5px; /* 圆角 */
            outline: none; /* 去除聚焦时的外边框 */
            transition: background .2s; /* 过渡效果 */
        }
        .slider::-webkit-slider-thumb {
            -webkit-appearance: none; /* 去除默认样式 */
            appearance: none;
            width: 20px; /* 滑块宽度 */
            height: 20px; /* 滑块高度 */
            background: #4CAF50; /* 滑块背景色 */
            border-radius: 50%; /* 圆形 */
            cursor: pointer; /* 光标样式 */
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* 滑块阴影效果 */
        }
        .refresh-btn {
            margin-left: 10px;
            padding: 8px 16px;
            cursor: pointer;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            font-size: 14px;
        }
    </style>
    <!-- Font Awesome CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
</head>
<body>
    <div class="captcha-container">
        <img id="backgroundImage" class="background-image" src="" alt="Background Image">
        <div id="puzzlePiece" class="puzzle-piece"></div>
    </div>
    <div class="slider-container">
        <input type="range" min="0" max="327" value="0" class="slider" id="slider">
        <button class="refresh-btn" id="refreshBtn"><i class="fas fa-sync-alt"></i></button>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            let slider = document.getElementById('slider');
            let puzzlePiece = document.getElementById('puzzlePiece');
            let token = '';

            function loadCaptcha() {
                fetch('/api/slideCaptcha') // 替换为你的后端接口地址
                    .then(response => response.json())
                    .then(data => {
                        document.getElementById('backgroundImage').src = 'data:image/jpeg;base64,' + data.backgroundImage;
                        puzzlePiece.style.backgroundImage = 'url(data:image/jpeg;base64,' + data.puzzlePiece + ')';
                        puzzlePiece.style.top = data.puzzlePieceTop + 'px';
                        puzzlePiece.style.left = '0px';
                        token = data.token;
                        slider.value = 0;
                    })
                    .catch(error => console.error('Error fetching captcha:', error));
            }

            let refreshBtn = document.getElementById('refreshBtn');
            refreshBtn.addEventListener('click', function() {
                loadCaptcha();
            });

            slider.addEventListener('input', function() {
                puzzlePiece.style.left = slider.value + 'px';
            });

            slider.addEventListener('change', function() {
                fetch('/api/slideVerify', { // 替换为你的后端验证接口地址
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        token: token,
                        position: parseInt(slider.value)
                    }),
                })
                .then(response => response.json())
                .then(data => {
                    if (data.success) {
                        alert(':-) 验证成功!');
                    } else {
                        alert('验证失败,请重试!');
                    }
                    loadCaptcha();
                })
                .catch(error => console.error('Error verifying captcha:', error));
            });

            loadCaptcha();
        });
    </script>
</body>
</html>

四、小结

俄国10月革命一声炮响,送来了美国的chatGPT。chatGPT吧,已经成了我的老师和工人。上面那些代码,都是我提要求,然后chatGPT生成的,甚至包括注释。我只修改了极少的地方。功能的确强大。但它其实又还不够智能,一些算法我一下子能看出问题,需要重重复复地提要求,每次它都说:明白了。它输入了海量的资料,知识渊博,各种编程语法更是精通,提交代码给它审查找问题,最是合适不过。它一般也能按要求给出初始代码,但有时总是差那么点意思。最讨厌的,是问它一些社科历史类的问题,经常一本正经地胡说八道。

这不是我想要的生活。

参考文章:
SlideCaptcha - 滑动验证码
滑块验证 - 使用AJ-Captcha插件【超简单.jpg】
TIANAI-CAPTCHA

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/797199.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构——考研笔记(二)线性表的定义和线性表之顺序表

文章目录 二、线性表2.1 定义、基本操作2.1.1 知识总览2.1.2 线性表的定义2.1.3 线性表的基本操作2.1.4 知识回顾与重要考点 2.2 顺序表2.2.1 知识总览2.2.2 顺序表的定义2.2.3 顺序表的实现——静态分配2.2.4 顺序表的实现——动态分配2.2.5 知识回顾与重要考点2.2.6 顺序表的…

如何在Linux上如何配置虚拟主机

在Linux上配置虚拟主机可以通过使用Apache HTTP服务器来实现。Apache是一个开源的跨平台的Web服务器软件&#xff0c;可以在多种操作系统上运行并支持虚拟主机的配置。 以下是在Linux上配置虚拟主机的步骤&#xff1a; 安装Apache HTTP服务器 在终端中运行以下命令来安装Apache…

通过vm可以访问那些属性——06

1.通过vue实例都可以访问那些属性&#xff1f;&#xff08;通过vm都可以vm.什么&#xff09; vue实例中的属性很多。有的以$开始&#xff0c;有的以_开始。 所有以$开始的属性&#xff0c;可以看做是公开的属性&#xff0c;这些属性是提供给程序员使用的 所有以_开始的属性&…

Linux的世界 -- 初次接触和一些常见的基本指令

一、Linux的介绍和准备 1、简单介绍下Linux的发展史 1991年10月5日&#xff0c;赫尔辛基大学的一名研究生Linus Benedict Torvalds在一个Usenet新闻组(comp.os.minix&#xff09;中宣布他编制出了一种类似UNIX的小操作系统&#xff0c;叫Linux。新的操作系统是受到另一个UNIX的…

系统架构设计师教程(清华第2版)<第2章 计算机系统基础知识>解读

系统架构设计师教程 第二章 计算机系统基础知识-2.1计算机系统概述 2.2 计算机硬件 2.1 计算机系统概述2.2 计算机硬件2.2.1 计算机硬件组成2.2.2 处理器2.2.2.1 控制单元(CU)2.2.2.2 算术逻辑单元(ALU)2.2.2.3 指令集2.2.2.3.1 CISC的特点2.2.2.3.2 RISC的特点2.2.3 存储器2.2…

Lottery 分布式抽奖(个人向记录总结)

1.搭建&#xff08;DDDRPC&#xff09;架构 DDD——微服务架构&#xff08;微服务是对系统拆分的方式&#xff09; &#xff08;Domain-Driven Design 领域驱动设计&#xff09; DDD与MVC同属微服务架构 是由Eric Evans最先提出&#xff0c;目的是对软件所涉及到的领域进行建…

html(抽奖设计)

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>抽奖</title><style type"text/css">* {margin: 0;padding: 0;}.container {width: 800px;height: 800px;border: 1px dashed red;position: absolut…

【学术会议征稿】第三届智能电网与能源系统国际学术会议

第三届智能电网与能源系统国际学术会议 2024 3rd International Conference on Smart Grid and Energy Systems 第三届智能电网与能源系统国际学术会议&#xff08;SGES 2024&#xff09;将于2024年10月25日-27日在郑州召开。 智能电网可以优化能源布局&#xff0c;让现有能源…

C++之多态使用小结

1、多态定义 1.1 多态概念 C多态性&#xff08;Polymorphism&#xff09;是面向对象编程(OOP)的一个重要特性之一&#xff0c;它允许我们使用统一的接口来处理不同类型的对象。多态性使得程序更加灵活、可扩展并且易于维护。 通俗来说&#xff0c;就是多种形态&#xff0…

Java小白入门到实战应用教程-开发环境搭建-IDEA2024安装激huo详细教程

writer:eleven 安装IDEA2024 一、下载IDEA 推荐大家去官网下载 我这里也给大家直接准备了安装包&#xff0c;和激huo教程&#xff0c;大家可以自行下载使用。 注意&#xff1a;激huo教程只用于学习交流&#xff0c;不可商用。 IDEA2024安装包及激huo教程 说明&#xff1a…

stm32入门-----初识stm32

目录 前言 ARM stm32 1.stm32家族 2.stm32的外设资源 3.命名规则 4.系统结构 5.引脚定义 6.启动配置 7.STM32F103C8T6芯片 8.STM32F103C8T6芯片原理图与最小系统电路 前言 已经很久没跟新了&#xff0c;上次发文的时候是好几个月之前了&#xff0c;现在我是想去学习st…

35 解决单条链路故障问题-华三链路聚合

InLoopBack接口是一种虚拟接口。InLoopBack接口由系统自动创建&#xff0c;用户不能进行配置和删除&#xff0c;但是可以显示&#xff0c;其物理层和链路层协议永远处于up状态。InLoopBack接口主要用于配合实现报文的路由和转发&#xff0c;任何送到InLoopBack接口的IP报文都会…

zigbee开发工具:3、驱动安装与程序下载(更新中...)

zigbee开发工具前两篇讲解了IAR开发工具的安装与注册&#xff0c;还介绍了新建一个cc2530开发工程的建立与配置。在进行zigbee开发&#xff0c;代码编写编译好后还需要下载到zigbee节点设备上进行调试与验证&#xff0c;那么就需要安装SmartRF Flash Programmer软件 和仿真器等…

Vim使用教程

目录 引言1. Vim的基本概念1.1 模式1.2 启动和退出 2. 基础操作2.1 导航2.2 插入文本2.3 删除和复制2.4 查找和替换 3. 高级功能3.1 多文件编辑3.2 宏录制和执行3.3 使用插件3.4 自定义快捷键 4. Vim脚本和自定义配置4.1 基本配置4.2 编写Vim脚本 5. 实用技巧5.1 快速移动5.2 批…

基于复旦微JFMQL100TAI的全国产化FPGA+AI人工智能异构计算平台,兼容XC7Z045-2FFG900I

基于上海复旦微电子FMQL45T900的全国产化ARM核心板。该核心板将复旦微的FMQL45T900&#xff08;与XILINX的XC7Z045-2FFG900I兼容&#xff09;的最小系统集成在了一个87*117mm的核心板上&#xff0c;可以作为一个核心模块&#xff0c;进行功能性扩展&#xff0c;能够快速的搭建起…

Golang | Leetcode Golang题解之第233题数字1的个数

题目&#xff1a; 题解&#xff1a; func countDigitOne(n int) (ans int) {// mulk 表示 10^k// 在下面的代码中&#xff0c;可以发现 k 并没有被直接使用到&#xff08;都是使用 10^k&#xff09;// 但为了让代码看起来更加直观&#xff0c;这里保留了 kfor k, mulk : 0, 1;…

Dify中的weaviate向量数据库操作

一.安装weaviate客户端 1.Dify 0.6.9中weaviate信息 在Dify 0.6.9版本中weaviate容器信息如下: # The Weaviate vector store. weaviate:image: semitechnologies/weaviate:1.19.0restart: alwaysvolumes:# Mount the Weaviate data directory to the container.- ./volume…

数据结构(空间复杂度介绍)超详细!!!

1. 数据结构前言 1.1 数据结构 数据结构是计算机存储、组织数据的形式&#xff0c;指相互之间存在一种或多种特定关系的数据元素的集合 1.2 算法 算法&#xff1a;良好的计算过程&#xff0c;它取一个或一组的值为输入&#xff0c;并产生出一个或一组的值作为输出。即算法经…

初学SpringMVC之 Ajax 篇

Ajax&#xff08;Asynchronous JavaScript and XML&#xff09;是一种在无需重新加载整个网页&#xff0c;能够更新部分网页的技术 Ajax 不是编程语言&#xff0c;而是一种用于创建更好更快以及交互性更强的 Web 应用程序技术 使用 Ajax 技术的网页&#xff0c;通过在后台服务…

课程设计——Python+OpenCV数字图像处理[车牌识别]

Python opencv 车牌识别 数字图像处理课程设计作业Python3OpenCV使用tkinter搭建界面tmp/文件夹是数字图像处理过程chepai/文件夹是车牌图片pic/文件夹是程序界面图PPT文件是验收时要讲的程序是从网上学习的并自己弄的&#xff0c;不完善&#xff0c;识别率不高 开发环境配置…