自己動手寫一個滑動驗證碼組件(后端為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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/45723.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/45723.shtml
英文地址,請注明出處:http://en.pswp.cn/web/45723.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

一個篇文章告訴你一個APP前端搭建有多簡單

用uni-app 1.新建uni-app項目 點擊項目 2.創建 最后點擊右下方創建 3.添加tarbar 首先你要創建幾個頁面這里比如說我有兩個頁面的tarbar首頁(home)和我的(userIndex) 在pages目錄下右鍵新建頁面即可

從庫存超賣問題分析鎖和分布式鎖的應用(二)

本文從一個經典的庫存超賣問題分析說明常見鎖的應用&#xff0c;假設庫存資源存儲在Redis里面。 假設我們的減庫存代碼如下&#xff1a; Autowired StringRedisTemplate redisTemplate;public void deduct(){String stock redisTemplate.opsForValue().get("stock"…

JavaSE從零開始到精通

1.前置知識 JVM&#xff1a;java virtrual machine, java虛擬機, 專門用于執行java代碼的一款軟件。JRE&#xff1a;java runtime enviroment, java運行時環境, java官方提供的核心類庫. jre中包含了核心類庫和jvm。JDK: java development kit, java開發工具包, javac.exe, ja…

LVS+Keepalive高可用

1、keepalive 調度器的高可用 vip地址主備之間的切換&#xff0c;主在工作時&#xff0c;vip地址只在主上&#xff0c;vip漂移到備服務器。 在主備的優先級不變的情況下&#xff0c;主恢復工作&#xff0c;vip會飄回到住服務器 1、配優先級 2、配置vip和真實服務器 3、主…

我想做信號通路分析,但我就是不想學編程

“我想做信號通路分析&#xff0c;但我就是不想學編程。” “我又不是生信狗&#xff0c;學代碼會死。” “你們這些做生信的&#xff0c;整天把數據分析搞得神神秘秘&#xff0c;不就是怕被人搶飯碗而已嘛。” “這都沒分析出我想要的結果&#xff0c;不靠譜。” “你們做…

【自學安全防御】二、防火墻NAT智能選路綜合實驗

任務要求&#xff1a; &#xff08;銜接上一個實驗所以從第七點開始&#xff0c;但與上一個實驗關系不大&#xff09; 7&#xff0c;辦公區設備可以通過電信鏈路和移動鏈路上網(多對多的NAT&#xff0c;并且需要保留一個公網IP不能用來轉換) 8&#xff0c;分公司設備可以通過總…

使用Docker創建并運行一個create-react-app應用(超簡單)

創建并運行一個使用 Create React App (CRA) 創建的應用程序的 Docker 容器涉及幾個步驟。以下是一個詳細的過程&#xff0c;包括創建一個簡單的 React 應用、編寫 Dockerfile、構建鏡像以及運行容器。 步驟 1: 創建一個新的 React 應用 如果你還沒有一個 React 應用&#xf…

Java爬蟲安全策略:防止TikTok音頻抓取過程中的請求被攔截

摘要 在當今互聯網時代&#xff0c;數據采集已成為獲取信息的重要手段。然而&#xff0c;隨著反爬蟲技術的不斷進步&#xff0c;爬蟲開發者面臨著越來越多的挑戰。本文將探討Java爬蟲在抓取TikTok音頻時的安全策略&#xff0c;包括如何防止請求被攔截&#xff0c;以及如何提高…

RK3568 安卓12 EC20模塊NOCONN沒有ip的問題(已解決)

從網上東拼西湊找了不少教程&#xff0c;但是里面沒有提到rillib.so需要替換&#xff0c;替換掉就可以上網了&#xff0c;系統也有4G圖標了。 注意&#xff0c;這個rillib.so是移遠提供的。把他們提供的文件放到rk3568_android_sdk/vendor/rockchip/common/phone/lib下&#x…

Andriod Stdio新建Kotlin的Jetpack Compose簡單項目

1.選擇 No Activity 2.選擇kotlin 4.右鍵選擇 在目錄MyApplication下 New->Compose->Empty Project 出現下面的畫面 Finish 完成

C++——類和對象(中)

文章目錄 一、類的默認成員函數二、構造函數三、析構函數四、拷?構造函數五、賦值運算符重載1. 運算符重載2. 賦值運算符重載 六、取地址運算符重載const成員函數取地址運算符重載 七、應用&#xff1a;?期類實現Date.hDate.cpptest.cpp 一、類的默認成員函數 默認成員函數就…

技術成神之路:設計模式(七)狀態模式

1.介紹 狀態模式&#xff08;State Pattern&#xff09;是一種行為設計模式&#xff0c;它允許一個對象在其內部狀態改變時改變其行為。這個模式將狀態的相關行為封裝在獨立的狀態類中&#xff0c;并將不同狀態之間的轉換邏輯分離開來。 2.主要作用 狀態模式的主要作用是讓一個…

數據結構—鏈式二叉樹-C語言

代碼位置&#xff1a;test-c-2024: 對C語言習題代碼的練習 (gitee.com) 一、前言&#xff1a; 在現實中搜索二叉樹為常用的二叉樹之一&#xff0c;今天我們就要通過鏈表來實現搜索二叉樹。實現的操作有&#xff1a;建二叉樹、前序遍歷、中序遍歷、后序遍歷、求樹的節點個數、求…

SMU Summer 2024 Contest Round 4

SMU Summer 2024 Contest Round 4 2024.7.16 9:00————11:00 過題數3/7 補題數6/7 Made Up H and V Moving Piece Sum of Divisors Red and Green Apples Rem of Sum is Num Keep Connect A - Made Up 題解&#xff1a; 給定三個數組a&#xff0c;b&#xff0c;c&#xf…

MySQL日期和時間相關函數

目錄 1. 獲取當前時間和日期 2. 獲取當前日期 3. 獲取當前時間 4. 獲取單獨的年/月/日/時/分/秒 5. 添加時間間隔 date_add ( ) 6. 格式化日期 date_format ( ) 7. 字符串轉日期 str_to_date () 8. 第幾天 dayofxx 9. 當月最后一天 last_day ( ) 10. 日期差 datedif…

H. Beppa and SwerChat【雙指針】

思路分析&#xff1a;運用雙指針從后往前掃一遍&#xff0c;兩次分別記作數組a&#xff0c;b&#xff0c;分別使用雙指針i和j來掃&#xff0c;如果一樣就往前&#xff0c;如果不一樣&#xff0c;i–,ans #include<iostream> #include<cstring> #include<string…

SQL server 練習題2

課后作業 作業 1&#xff1a;自己查找方法&#xff0c;將 homework_1.xls 文件數據導入到 SQLServer 的 homework 數據庫中。數據導入完成后&#xff0c;把表名統一改為&#xff1a;外賣表 如下所示&#xff1a; 作業 2&#xff1a;找出所有在 2020 年 5 月 1 日至 5 月 31 …

Zookeeper之CAP理論及分布式一致性算法

CAP理論 CAP理論告訴我們&#xff0c;一個分布式系統不可能同時滿足以下三種 一致性&#xff08;C:consistency&#xff09;可用性&#xff08;A:Available&#xff09;分區容錯性&#xff08;P:Partition Tolerance&#xff09; 這三個基本要求&#xff0c;最多只能同時滿足…

python 語法學習 day2

python有七大數據類型, 數據類型轉換, 多變量賦值與print間隔, split函數, int用法總結python有七大數據類型&#xff1a; &#xff08;1&#xff09;數字&#xff08;Number&#xff09;&#xff1a;int(整型&#xff0c;長整型)&#xff0c;float(浮點型)&#xff0c;com…

部署k8s 1.28.9版本

繼上篇通過vagrant與virtualBox實現虛擬機的安裝。筆者已經將原有的vmware版本的虛擬機卸載掉了。這個場景下&#xff0c;需要重新安裝k8s 相關組件。由于之前寫的一篇文章本身也沒有截圖。只有命令。所以趁著現在。寫一篇&#xff0c;完整版帶截圖的步驟。現在行業這么卷。離…