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

news2024/12/26 11:57:30

近期参加的项目,主管丢给我一个任务,说要支持滑动验证码。我身为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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jenkins系列-06.harbor

https://github.com/goharbor/harbor/releases?page2 https://github.com/goharbor/harbor/releases/download/v2.3.4/harbor-offline-installer-v2.3.4.tgz harbor官网&#xff1a;https://goharbor.io/ 点击 Download now 链接&#xff0c;会自动跳转到上述github页面&am…

采用自动微分进行模型的训练

自动微分训练模型 简单代码实现&#xff1a; import torch import torch.nn as nn import torch.optim as optim# 定义一个简单的线性回归模型 class LinearRegression(nn.Module):def __init__(self):super(LinearRegression, self).__init__()self.linear nn.Linear(1, 1) …

python:使用matplotlib库绘制图像(四)

作者是跟着http://t.csdnimg.cn/4fVW0学习的&#xff0c;matplotlib系列文章是http://t.csdnimg.cn/4fVW0的自己学习过程中整理的详细说明版本&#xff0c;对小白更友好哦&#xff01; 四、条形图 1. 一个数据样本的条形图 条形图&#xff1a;常用于比较不同类别的数量或值&…

DockerCompose介绍,安装,使用

DockerCompose 1、Compose介绍 将单机服务-通过Dockerfile 构建为镜像 -docker run 成为一个服务 user 8080 net 7000 pay 8181 admin 5000 监控 .... docker run 单机版、一个个容器启动和停止问题&#xff1a; 前面我们使用Docker的时候&#xff0c;定义 Dockerfil…

深入理解Java泛型:概念、用法与案例分析

个人名片** &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4…

Transformer模型:Encoder的self-attention mask实现

前言 这是对Transformer模型的Word Embedding、Postion Embedding内容的续篇。 视频链接&#xff1a;19、Transformer模型Encoder原理精讲及其PyTorch逐行实现_哔哩哔哩_bilibili 文章链接&#xff1a;Transformer模型&#xff1a;WordEmbedding实现-CSDN博客 Transformer模型…

docker-compose安装PolarDB-PG数据库

文章目录 一. Mac1.1 docker-compose.yaml1.2 部署1.3 卸载4. 连接 二. Win102.1 docker-compose.yaml2.2 部署2.3 卸载 参考官方文档 基于单机文件系统部署 一. Mac 1.1 docker-compose.yaml mkdir -p /Users/wanfei/docker-compose/polardb-pg && cd /Users/wanfei…

Linux - 综合使用shell脚本,输出网站有效数据

综合示例: shell脚本实现查看网站分数 使用编辑器编辑文件jw.sh为如下内容: #!/bin/bash save_file"score" # 临时文件 semester20102 # 查分的学期, 20102代表2010年第二学期 jw_home"http://jwas3.nju.edu.cn:8080/jiaowu" # 测试网站首页地址 jw_logi…

zigbee开发工具:2、zigbee工程建立与配置

本文演示基于IAR for 8051&#xff08;版本10.10.1&#xff09;如何建立一个开发芯片cc2530的zigbee的工程&#xff0c;并配置这个工程&#xff0c;使其能够将编译的代码进行烧录&#xff0c;生成.hex文件。IAR for 8051&#xff08;版本10.10.1&#xff09;支持工程使用C语言&…

STM32智能交通灯系统教程

目录 引言环境准备智能交通灯系统基础代码实现&#xff1a;实现智能交通灯系统 4.1 数据采集模块 4.2 数据处理与控制模块 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;交通管理与优化问题解决方案与优化收尾与总结 1. 引言 智能交通灯系统通过STM…

Python游戏开发:四连珠(内附完整代码)

四连珠&#xff08;Connect Four&#xff09;是一款经典的棋类游戏&#xff0c;由两名玩家在7列6行的网格上轮流下棋。玩家的目标是将自己的棋子在垂直、水平或对角线上连成一条线&#xff0c;通常是四个棋子。如果一方成功做到这一点&#xff0c;那么他就赢得了游戏。如果所有…

视频监控汇聚平台:通过SDK接入大华DSS视频监控平台的源代码解释和分享

目录 一、视频监控汇聚平台 1、概述 2、视频接入能力 3、视频汇聚能力 二、大华DSS平台 1、DSS平台概述 2、大华DSS平台的主要特点 &#xff08;1&#xff09;高可用性 &#xff08;2&#xff09;高可靠性 &#xff08;3&#xff09;易维护性 &#xff08;4&#xf…

《昇思25天学习打卡营第2天|02快速入门》

课程目标 这节课准备再学习下训练模型的基本流程&#xff0c;因此还是选择快速入门课程。 整体流程 整体介绍下流程&#xff1a; 数据处理构建网络模型训练模型保存模型加载模型 思路是比较清晰的&#xff0c;看来文档写的是比较连贯合理的。 数据处理 看数据也是手写体数…

【算法】平衡二叉树

难度&#xff1a;简单 题目 给定一个二叉树&#xff0c;判断它是否是 平衡二叉树 示例&#xff1a; 示例1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;true 示例2&#xff1a; 输入&#xff1a;root [1,2,2,3,3,null,null,4,4] 输出&…

炒鸡清晰的防御综合实验(内含区域划分,安全策略,用户认证,NAT认证,智能选路,域名访问)

实验拓扑图如下&#xff1a; 前面六个条件在之间的实验中做过了&#xff0c;详细步骤可以去之前的文章看 这里简写一下大致步骤 第一步&#xff1a; 先将防火墙之外的配置给配置好&#xff0c;比如&#xff0c;PC的IP,交换上的Vlan划分。 第二步&#xff1a; 在浏览器上登…

用SurfaceView实现落花动画效果

上篇文章 Android子线程真的不能刷新UI吗&#xff1f;(一&#xff09;复现异常 中可以看出子线程更新main线程创建的View&#xff0c;会抛出异常。SurfaceView不依赖main线程&#xff0c;可以直接使用自己的线程控制绘制逻辑。具体代码怎么实现了&#xff1f; 这篇文章用Surfa…

【算法专题】快速排序

1. 颜色分类 75. 颜色分类 - 力扣&#xff08;LeetCode&#xff09; 依据题意&#xff0c;我们需要把只包含0、1、2的数组划分为三个部分&#xff0c;事实上&#xff0c;在我们前面学习过的【算法专题】双指针算法-CSDN博客中&#xff0c;有一道题叫做移动零&#xff0c;题目要…

小公司的Git工作流程

项目初始化 git init并添加.gitignore文件 Git使用 通过git add . 把代码推到暂存区通过git commit -m “你的说明”&#xff0c;将暂存区的代码推到本地仓库将本地仓库的代码通过git push 推到远程仓库远程仓库(gitee/gitlab/github)同事就可以通过命令git pull将你推上去的…

信息学奥赛初赛天天练-46-CSP-J2020阅读程序2-进制转换、十进制转k进制、等比数列通项公式、等比数列求和公式应用

PDF文档公众号回复关键字:20240713 2020 CSP-J 阅读程序2 1阅读程序(程序输入不超过数组或字符串定义的范围&#xff1b;判断题正确填 √&#xff0c;错误填 。除特殊说明外&#xff0c;判断题 1.5 分&#xff0c;选择题 3 分&#xff0c;共计 40 分) 01 #include <iostre…

java各种锁介绍

在 Java 中&#xff0c;锁是用来控制多个线程对共享资源进行访问的机制。主要有以下几种类型的锁&#xff1a; 1.互斥锁&#xff08;Mutex Lock)&#xff1a;最简单的锁&#xff0c;一次只允许一个线程访问共享资源。如果一个线程获得了锁&#xff0c;其他线程必须等待锁被释放…