目錄
一、效果演示
二、后端滑塊驗證碼生成思路
三、原理解析
四、核心代碼拿走
滑塊驗證碼react前端實現,見我的這篇博客:前端 React 彈窗式 滑動驗證碼實現_react中使用阿里云滑塊驗證碼2.0前端接入及相關視覺-CSDN博客
一、效果演示
生成的案例圖片:
視頻演示:
滑塊驗證碼演示
二、后端滑塊驗證碼生成思路
1、后端需要生成對應的兩個圖片(拼圖圖片和拼圖背景圖片,圖片內存盡量小一點)和對應位置(x和y, 等高拼圖只需要記錄x即可);
2、驗證碼生成服務,生成唯一的標識uuid(可考慮雪花算法生成),將生成圖片生成后得到的位置信息即x(非登高拼圖x和y)記錄到緩存中,建議使用redis存儲,即使分布式也能使用;
3、將驗證碼數據返回給前端,格式參考如下:
三、原理解析
要想生成拼圖形狀的拼圖,我們需要運用到一些數學知識,核心代碼如下:
通過?圓的標準方程 (x-a)2+(y-b)2=r2,標識圓心(a,b),半徑為r的圓,>=的在外側,<的內側。
簡單來看就是這樣的一個模型:
/*** 隨機生成拼圖圖輪廓數據** @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)2+(y-b)2=r2,標識圓心(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);// 創建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));// 摳虛假圖readPixel(bgImg, fakeX + i, y + j, values);fillMatrix(matrix, values);bgImg.setRGB(fakeX + i, y + j, avgMatrix(matrix));} else {// 這里把背景設為透明puzzleImg.setRGB(i, j, rgb_ori & 0x00ffffff);}}}}
四、核心代碼拿走
SliderCaptchaUtil:
package com.xloda.common.tool.captcha.util;import com.xloda.common.tool.captcha.constant.SliderCaptchaConfig;
import com.xloda.common.tool.captcha.pojo.SliderCaptcha;import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;/*** @author Dragon Wu* @since 2025/04/23 10:52* 滑塊驗證碼生成器*/public class SliderCaptchaUtil implements SliderCaptchaConfig {/*** 生成滑塊驗證碼** @param bgImg 1. 傳入隨機背景圖* @param accordant 是否生成登高拼圖* @return SliderCaptcha 驗證碼結果* @throws IOException IO異常*/public static SliderCaptcha generateCaptcha(BufferedImage bgImg, boolean accordant) throws IOException {// 2. 隨機生成離左上角的(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;// 3. 創建拼圖圖像BufferedImage puzzleImg = new BufferedImage(puzzleWidth, puzzleHeight, BufferedImage.TYPE_4BYTE_ABGR);// 4. 隨機獲取位置數據int randomR1 = getRandomR1();// 5. 隨機生成拼圖輪廓數據int[][] slideTemplateData = createTemplateData(randomR1);// 6. 從大圖中裁剪拼圖。摳原圖,裁剪拼圖cutByTemplate(bgImg, puzzleImg, slideTemplateData, x, y);// 7. 給拼圖加邊框puzzleImg = ImageUtil.addBorderWithOutline(puzzleImg, borderSize, Color.white);// 8. 判斷是否為登高拼圖if (accordant) {puzzleImg = reshapeAccordant(puzzleImg, y);return new SliderCaptcha(ImageUtil.toBase64(bgImg),ImageUtil.toBase64(puzzleImg), x);}// 非登高拼圖,記錄x和yreturn new SliderCaptcha(ImageUtil.toBase64(bgImg),ImageUtil.toBase64(puzzleImg), x, y);}// 隨機獲取小圓距離點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)2+(y-b)2=r2,標識圓心(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);// 創建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));// 摳虛假圖readPixel(bgImg, fakeX + i, y + j, values);fillMatrix(matrix, values);bgImg.setRGB(fakeX + i, y + j, avgMatrix(matrix));} 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;Random random = new Random();int fakeX = random.nextInt(width - 2 * puzzleRealWidth) + puzzleRealWidth;if (Math.abs(fakeX - x) <= puzzleRealWidth) {fakeX = width - x;}return fakeX;}/*** 通過拼圖圖片生成登高拼圖圖片** @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) {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 / 8, g / 8, b / 8).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++];}}}
}
RandomUtil:
package com.xloda.common.tool.captcha.util;import com.xloda.common.tool.captcha.constant.CaptchaConstants;import java.util.Random;/*** @author Dragon Wu* @since 2025/04/23 18:07* 隨機生成器工具*/public class RandomUtil {// 隨機獲取背景圖路徑public static String randomBgImgPath() {int index = new Random().nextInt(CaptchaConstants.BG_IMAGES.length);return CaptchaConstants.BG_IMAGES[index];}
}
ImageUtil:
package com.xloda.common.tool.captcha.util;import com.xloda.common.tool.captcha.constant.CaptchaConstants;import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;/*** @author Dragon Wu* @since 2025/04/25 10:18* 圖片處理工具*/public class ImageUtil {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;}// 圖片轉Base64public static String toBase64(BufferedImage image) throws IOException {// 創建一個字節數組輸出流ByteArrayOutputStream os = new ByteArrayOutputStream();// 將BufferedImage寫入到輸出流中,這里指定圖片格式為"png"或"jpg"等ImageIO.write(image, CaptchaConstants.IMG_FORMAT, os);// 將輸出流的字節數組轉換為Base64編碼的字符串String imageBase64 = Base64.getEncoder().encodeToString(os.toByteArray());// 關閉輸出流os.close();return CaptchaConstants.BASE64_PREFIX + imageBase64;}
}
SliderCaptcha:
package com.xloda.common.tool.captcha.pojo;import lombok.*;/*** @author Dragon Wu* @since 2025/04/23 10:49* 滑塊驗證碼*/
@AllArgsConstructor
@Getter
@ToString
public class SliderCaptcha {// 驗證碼背景圖private String bgImg;// 驗證碼滑塊private String puzzleImg;// 驗證碼正確的x位置(此值需自行存入緩存,用于驗證碼判斷)private int x;// 等高拼圖時,返回0(非登高拼圖,此值需自行存入緩存,用于驗證碼判斷)private int y;public SliderCaptcha(String bgImg, String puzzleImg, int x) {this.bgImg = bgImg;this.puzzleImg = puzzleImg;this.x = x;}
}
CaptchaConstants:
package com.xloda.common.tool.captcha.constant;/*** @author Dragon Wu* @since 2025/04/23 10:53*/
public interface CaptchaConstants {// 圖片格式String IMG_FORMAT = "png";// base64前綴String BASE64_PREFIX = "data:image/" + IMG_FORMAT + ";base64,";// 圖片存儲的目錄String FOLDER = "/static/img/captcha/";// 背景圖列表(引入依賴后,記得在項目資源目錄的該路徑下添加對應圖片)String[] BG_IMAGES = new String[]{FOLDER + "bg01.png",FOLDER + "bg01.png"};
}
SliderCaptchaConfig
package com.xloda.common.tool.captcha.constant;/*** @author Dragon Wu* @since 2025/04/23 11:09* 滑塊驗證碼的配置*/public interface SliderCaptchaConfig {// 大圖寬度(原圖裁剪拼圖后的背景圖)int width = 280;// 大圖高度int height = 173;// 小圖寬度(滑塊拼圖),前端拼圖的實際寬度:puzzleWidth + 2 * borderSize + 2int puzzleWidth = 66;// 小圖高度,前端拼圖的實際高度:puzzleHeight + 2 * borderSize + 2int puzzleHeight = 66;// 邊框厚度int borderSize = 1;// 小圓半徑,即拼圖上的凹凸輪廓半徑int radius = 8;// 圖片一周預留的距離,randomR1最大值不能超過radius * 3 / 2int distance = radius * 3 / 2;
}
接下來繼續手搓旋轉驗證碼前后端。
本節,總結到此,學點數學挺有用的!