自己动手写一个滑动验证码组件(后端为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 {@Autowiredprivate StringRedisTemplate redisTemplate;private String[] images;int puzzlePieceWidth = 40;int puzzlePieceHeight = 40;@PostConstructpublic void init() throws IOException {PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();Resource[] resources = resolver.getResources("classpath:/images/*.jpg"); // 修改为 *.jpgimages = 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<>();// 生成唯一的 tokenString 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);// 将图像转换为 Base64ByteArrayOutputStream 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
相关文章:

自己动手写一个滑动验证码组件(后端为Spring Boot项目)
近期参加的项目,主管丢给我一个任务,说要支持滑动验证码。我身为50岁的软件攻城狮,当时正背着双手,好像一个受训的保安似的,中规中矩地参加每日站会,心想滑动验证码在今时今日已经是标配了,司空…...
keepalive脑裂
keepalive脑裂 调度器的高可用 vip地址主备之间的切换,主在工作时,p地址只在主上,主停止工作,ip飘移到备服务器。 在主备的优先级不变的情况下,主恢复工作,vip会飘回到主服务器。 1、配优先级 2、配置…...

STM32Cubemx配置生成 Keil AC6支持代码
文章目录 一、前言二、AC 6配置2.1 ARM ComPiler 选择AC62.2 AC6 UTF-8的编译命令会报错 三、STM32Cubemx 配置3.1 找到stm32cubemx的模板位置3.2 替换文件内核文件3.3 修改 cmsis_os.c文件3.4 修改本地 四、编译对比 一、前言 使用keil ARM compiler V5的时候,编译…...
Perl基础入门指南:从零开始掌握Perl编程
Perl是一种功能强大且灵活的编程语言,广泛应用于系统管理、Web开发、网络编程和文本处理等领域。如果你是编程新手或者想学习一种新的编程语言,Perl是一个不错的选择。本文将带你了解Perl的基础知识,并通过简单的示例代码帮助你快速入门。 什…...
Mybatis SQL注解使用场景
MyBatis 提供了几种常用的注解,主要用于简化 XML 映射文件的编写,使得 SQL 查询和操作可以直接在 Java 接口中定义。下面列出了主要的注解以及它们在被调用时的写法示例: 1. Select Select 注解用于执行查询操作,并将查询结果映…...

Dataset for Stable Diffusion
1.Dataset for Stable Diffusion 笔记来源: 1.Flickr8k数据集处理 2.处理Flickr8k数据集 3.Github:pytorch-stable-diffusion 4.Flickr 8k Dataset 5.dataset_flickr8k.json 1.1 Dataset 采用Flicker8k数据集,该数据集有两个文件ÿ…...

近期matlab学习笔记,学习是一个记录,反复的过程
近期matlab学习笔记,学习是一个记录,反复的过程 matlab的mlx文件在运行的时候,不需要在文件夹路径下,也能运行,但是需要调用子函数时,就需要在文件所在路径下运行 那就先运行子函数,把路径换过来…...
Elasticsearch7.5.2 常用rest api与elasticsearch库
目录 一、rest api 1. 新建索引 2. 删除索引 3. 插入单条数据 4. 更新单条数据 5. 删除单条数据 6. 查询数据 二、python elasticsearch库 1. 新建索引 一、rest api 1. 新建索引 请求方式:PUT 请求URL:http://ip/(your_index_nam…...

Autosar Dcm配置-0x28服务ComControl-基于ETAS软件
文章目录 前言DcmDcmDsdDcmDspBswMBswMModeRequestPortBswMModeConditionBswMLogicalExpressionBswMActionBswMActionListBswMRule总结前言 0x28服务主要用来控制非诊断报文的通讯,一般在刷写预编程过程中,用来禁止APP的通信报文,可以减少总线负载率,提高刷写成功率。本文…...
平安养老险厦门分公司:提升金融服务,发挥金融力量
为向社会公众普及金融保险知识,传递消费者权益保护理念,平安养老保险股份有限公司厦门分公司(以下简称“分公司”)积极开展“78保险公众宣传日”系列教育宣传活动。分公司紧扣“保险,让每一步前行更有底气”主题&#…...

【开源合规】开源许可证风险场景详细解读
文章目录 前言关于BlackDuck许可证风险对比图弱互惠型许可证举个例子具体示例LGPL系列LGPL-2.0-onlyLGPL-2.0-or-laterLGPL-2.1-onlyLGPL-2.1-or-laterLGPL-3.0-onlyLGPL-3.0-or-laterMPL系列MPL-1.0MPL-1.1MPL-2.0EPL系列EPL-1.0EPL-2.0互惠型许可证GPL系列GPL-1.0GPL-2.0GPL-…...

Redis持久化RDB,AOF
目 录 CONFIG动态修改配置 慢查询 持久化 在上一篇主要对redis的了解入门,安装,以及基础配置,多实例的实现:redis的安装看我上一篇: Redis安装部署与使用,多实例 redis是挡在MySQL前面的,运行在内存…...

【持续集成_03课_Linux部署Sonar+Gogs+Jenkins】
一、通过虚拟机搭建Linux环境-CnetOS 1、安装virtualbox,和Vmware是一样的,只是box更轻量级 1)需要注意内存选择,4G 2、启动完成后,需要获取服务器IP地址 命令 ip add 服务器IP地址 通过本地的工具,进…...
mvcc 速读
MVCC(Multi-Version Concurrency Control,多版本并发控制)是MySQL中一种用于实现数据库并发控制的方法,尤其在InnoDB存储引擎中得到了广泛应用。它的主要作用是提高数据库在高并发场景下的性能,并确保数据的一致性。 …...

美容仪维修过程记录
近期维修的家用射频美容仪,发一些维修过程的拆机图片...

STM32入门开发操作记录(一)——新建工程
目录 一、课程准备1. 课程资料2. 配件清单3. 根目录 二、环境搭建三、新建工程1. 载入器件支持包2. 添加模块3. ST配置4. 外观设置5. 主函数文件 一、课程准备 1. 课程资料 本记录操作流程参考自b站视频BV1th411z7snSTM32入门教程-2023版 细致讲解 中文字幕,课程资…...

QT实现自定义带有提示信息的透明环形进度条
1. 概述 做界面开发的童鞋可能都会遇到这样的需求,就是有一些界面点击了之后比较耗时的操作,需要界面给出一个环形进度条的进度反馈信息. 如何来实现这样的需求呢,话不多说,上效果 透明进度条 2. 代码实现 waitfeedbackprogressba…...
金币程序题
昨天,小孩问了我一个python编程竞赛题,我看了一下题目,是一个数列编程的问题,我在想,小学五年级的学生能搞得懂吗?反正我家小孩是没有搞懂,不知道别人家的小孩能不能搞明白。所以我花了一点时间…...

《Windows API每日一练》9.13资源-鼠标位图和字符串
鼠标指针位图(Mouse Cursor Bitmap)是用于表示鼠标指针外观的图像。在 Windows 窗口编程中,可以使用自定义的鼠标指针位图来改变鼠标的外观,并提供更加个性化的用户体验。 ■以下是一些与鼠标指针位图相关的要点: ●…...

【保姆级教程】CenterNet的目标检测、3D检测、关键点检测使用教程
一、代码下载 仓库地址:https://github.com/xingyizhou/CenterNet?tab=readme-ov-file 二、目标检测 2.1 下载预训练权重 下载预训练权重ctdet_coco_dla_2x.pth放到models文件夹下 下载链接:https://drive.google.com/file/d/18Q3fzzAsha_3Qid6mn4jcIFPeOGUaj1d/edit …...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...

逻辑回归暴力训练预测金融欺诈
简述 「使用逻辑回归暴力预测金融欺诈,并不断增加特征维度持续测试」的做法,体现了一种逐步建模与迭代验证的实验思路,在金融欺诈检测中非常有价值,本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...