Spring框架實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼功能的代碼示例
1. 整體描述
之前項(xiàng)目需要在驗(yàn)證碼模塊,增加滑動(dòng)驗(yàn)證碼,用來給手機(jī)端使用的,大概看了下,主要方法就是將圖片切割,然后記住偏移量,進(jìn)行滑動(dòng),前端驗(yàn)證的時(shí)候,需要用前端傳入的偏移量和生成的偏移量進(jìn)行對(duì)比,如果在閾值之內(nèi),就驗(yàn)證通過,否則就不通過。具體實(shí)現(xiàn)方式見下文。之前沒時(shí)間寫,最近記錄一下。
2. 具體實(shí)現(xiàn)
本工程主要依賴springboot框架,并且需要redis存驗(yàn)證碼的信息,還需要幾個(gè)圖片,用來生成驗(yàn)證碼。
2.1 滑動(dòng)驗(yàn)證碼實(shí)體類
package com.thcb.captchademo.captcha.domain;
import lombok.Data;
/**
* 滑動(dòng)驗(yàn)證碼
*
* @author thcb
* @date 2023-05-25
*/
@Data
public class Captcha {
/**
* 隨機(jī)字符串
**/
private String nonceStr;
/**
* 驗(yàn)證值
**/
private String value;
/**
* 生成的畫布的base64
**/
private String canvasSrc;
/**
* 畫布寬度
**/
private Integer canvasWidth;
/**
* 畫布高度
**/
private Integer canvasHeight;
/**
* 生成的阻塞塊的base64
**/
private String blockSrc;
/**
* 阻塞塊寬度
**/
private Integer blockWidth;
/**
* 阻塞塊高度
**/
private Integer blockHeight;
/**
* 阻塞塊凸凹半徑
**/
private Integer blockRadius;
/**
* 阻塞塊的橫軸坐標(biāo)
**/
private Integer blockX;
/**
* 阻塞塊的縱軸坐標(biāo)
**/
private Integer blockY;
/**
* 圖片獲取位置
**/
private Integer place;
}
2.2 滑動(dòng)驗(yàn)證碼登錄VO
package com.thcb.captchademo.captcha.domain;
import lombok.Data;
/**
* 滑動(dòng)驗(yàn)證碼登錄Vo
*
* @author thcb
* @date 2023-05-25
*/
@Data
public class LoginVo {
/**
* 隨機(jī)字符串
**/
private String nonceStr;
/**
* 驗(yàn)證值
**/
private String value;
}
2.3 滑動(dòng)驗(yàn)證碼接口返回類
package com.thcb.captchademo.captcha.utils;
import java.util.HashMap;
/**
* 操作消息提醒
*
* @author thcb
*/
public class AjaxResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/**
* 狀態(tài)碼
*/
public static final String CODE_TAG = "code";
/**
* 返回內(nèi)容
*/
public static final String MSG_TAG = "msg";
/**
* 數(shù)據(jù)對(duì)象
*/
public static final String DATA_TAG = "data";
/**
* 初始化一個(gè)新創(chuàng)建的 AjaxResult 對(duì)象,使其表示一個(gè)空消息。
*/
public AjaxResult() {
}
/**
* 初始化一個(gè)新創(chuàng)建的 AjaxResult 對(duì)象
*
* @param code 狀態(tài)碼
* @param msg 返回內(nèi)容
*/
public AjaxResult(int code, String msg) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
/**
* 初始化一個(gè)新創(chuàng)建的 AjaxResult 對(duì)象
*
* @param code 狀態(tài)碼
* @param msg 返回內(nèi)容
* @param data 數(shù)據(jù)對(duì)象
*/
public AjaxResult(int code, String msg, Object data) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (data != null && !data.equals("")) {
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success() {
return AjaxResult.success("操作成功");
}
/**
* 返回成功數(shù)據(jù)
*
* @return 成功消息
*/
public static AjaxResult success(Object data) {
return AjaxResult.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回內(nèi)容
* @return 成功消息
*/
public static AjaxResult success(String msg) {
return AjaxResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回內(nèi)容
* @param data 數(shù)據(jù)對(duì)象
* @return 成功消息
*/
public static AjaxResult success(String msg, Object data) {
return new AjaxResult(200, msg, data);
}
/**
* 返回錯(cuò)誤消息
*
* @return
*/
public static AjaxResult error() {
return AjaxResult.error("操作失敗");
}
/**
* 返回錯(cuò)誤消息
*
* @param msg 返回內(nèi)容
* @return 警告消息
*/
public static AjaxResult error(String msg) {
return AjaxResult.error(msg, null);
}
/**
* 返回錯(cuò)誤消息
*
* @param msg 返回內(nèi)容
* @param data 數(shù)據(jù)對(duì)象
* @return 警告消息
*/
public static AjaxResult error(String msg, Object data) {
return new AjaxResult(500, msg, data);
}
/**
* 返回錯(cuò)誤消息
*
* @param code 狀態(tài)碼
* @param msg 返回內(nèi)容
* @return 警告消息
*/
public static AjaxResult error(int code, String msg) {
return new AjaxResult(code, msg, null);
}
}
2.4 滑動(dòng)驗(yàn)證碼工具類
此類是核心類,主要實(shí)現(xiàn)了圖片的切割功能。
package com.thcb.captchademo.captcha.utils;
import com.thcb.captchademo.captcha.domain.Captcha;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Base64;
import java.util.Objects;
import java.util.Random;
/**
* 滑動(dòng)驗(yàn)證碼工具類
*
* @author thcb
* @date 2023-05-25
*/
public class CaptchaUtils {
/**
* 網(wǎng)絡(luò)圖片地址
**/
private final static String IMG_URL = "https://loyer.wang/view/ftp/wallpaper/%s.jpg";
/**
* 本地圖片地址
**/
private final static String IMG_PATH = "E:\\caphcha\\%s.jpg";
/**
* 入?yún)⑿r?yàn)設(shè)置默認(rèn)值
**/
public static void checkCaptcha(Captcha captcha) {
//設(shè)置畫布寬度默認(rèn)值
if (captcha.getCanvasWidth() == null) {
captcha.setCanvasWidth(320);
}
//設(shè)置畫布高度默認(rèn)值
if (captcha.getCanvasHeight() == null) {
captcha.setCanvasHeight(155);
}
//設(shè)置阻塞塊寬度默認(rèn)值
if (captcha.getBlockWidth() == null) {
captcha.setBlockWidth(65);
}
//設(shè)置阻塞塊高度默認(rèn)值
if (captcha.getBlockHeight() == null) {
captcha.setBlockHeight(55);
}
//設(shè)置阻塞塊凹凸半徑默認(rèn)值
if (captcha.getBlockRadius() == null) {
captcha.setBlockRadius(9);
}
//設(shè)置圖片來源默認(rèn)值
if (captcha.getPlace() == null) {
captcha.setPlace(1);
}
}
/**
* 獲取指定范圍內(nèi)的隨機(jī)數(shù)
**/
public static int getNonceByRange(int start, int end) {
Random random = new Random();
return random.nextInt(end - start + 1) + start;
}
/**
* 獲取驗(yàn)證碼資源圖
**/
public static BufferedImage getBufferedImage(Integer place) {
try {
//隨機(jī)圖片
//獲取網(wǎng)絡(luò)資源圖片
if (0 == place) {
int nonce = getNonceByRange(0, 1000);
String imgUrl = String.format(IMG_URL, nonce);
URL url = new URL(imgUrl);
return ImageIO.read(url.openStream());
}
//獲取本地圖片
else {
int nonce = getNonceByRange(0, 20);
String imgPath = String.format(IMG_PATH, nonce);
File file = new File(imgPath);
return ImageIO.read(file);
}
} catch (Exception e) {
System.out.println("獲取拼圖資源失敗");
//異常處理
return null;
}
}
/**
* 調(diào)整圖片大小
**/
public static BufferedImage imageResize(BufferedImage bufferedImage, int width, int height) {
Image image = bufferedImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage resultImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = resultImage.createGraphics();
graphics2D.drawImage(image, 0, 0, null);
graphics2D.dispose();
return resultImage;
}
/**
* 摳圖,并生成阻塞塊
**/
public static void cutByTemplate(BufferedImage canvasImage, BufferedImage blockImage, int blockWidth, int blockHeight, int blockRadius, int blockX, int blockY) {
BufferedImage waterImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
//阻塞塊的輪廓圖
int[][] blockData = getBlockData(blockWidth, blockHeight, blockRadius);
//創(chuàng)建阻塞塊具體形狀
for (int i = 0; i < blockWidth; i++) {
for (int j = 0; j < blockHeight; j++) {
try {
//原圖中對(duì)應(yīng)位置變色處理
if (blockData[i][j] == 1) {
//背景設(shè)置為黑色
waterImage.setRGB(i, j, Color.BLACK.getRGB());
blockImage.setRGB(i, j, canvasImage.getRGB(blockX + i, blockY + j));
//輪廓設(shè)置為白色,取帶像素和無像素的界點(diǎn),判斷該點(diǎn)是不是臨界輪廓點(diǎn)
if (blockData[i + 1][j] == 0 || blockData[i][j + 1] == 0 || blockData[i - 1][j] == 0 || blockData[i][j - 1] == 0) {
blockImage.setRGB(i, j, Color.WHITE.getRGB());
waterImage.setRGB(i, j, Color.WHITE.getRGB());
}
}
//這里把背景設(shè)為透明
else {
blockImage.setRGB(i, j, Color.TRANSLUCENT);
waterImage.setRGB(i, j, Color.TRANSLUCENT);
}
} catch (ArrayIndexOutOfBoundsException e) {
//防止數(shù)組下標(biāo)越界異常
}
}
}
//在畫布上添加阻塞塊水印
addBlockWatermark(canvasImage, waterImage, blockX, blockY);
}
/**
* 構(gòu)建拼圖輪廓軌跡
**/
private static int[][] getBlockData(int blockWidth, int blockHeight, int blockRadius) {
int[][] data = new int[blockWidth][blockHeight];
double po = Math.pow(blockRadius, 2);
//隨機(jī)生成兩個(gè)圓的坐標(biāo),在4個(gè)方向上 隨機(jī)找到2個(gè)方向添加凸/凹
//凸/凹1
Random random1 = new Random();
int face1 = random1.nextInt(4);
//凸/凹2
int face2;
//保證兩個(gè)凸/凹不在同一位置
do {
Random random2 = new Random();
face2 = random2.nextInt(4);
} while (face1 == face2);
//獲取凸/凹起位置坐標(biāo)
int[] circle1 = getCircleCoords(face1, blockWidth, blockHeight, blockRadius);
int[] circle2 = getCircleCoords(face2, blockWidth, blockHeight, blockRadius);
//隨機(jī)凸/凹類型
int shape = getNonceByRange(0, 1);
//圓的標(biāo)準(zhǔn)方程 (x-a)2+(y-b)2=r2,標(biāo)識(shí)圓心(a,b),半徑為r的圓
//計(jì)算需要的小圖輪廓,用二維數(shù)組來表示,二維數(shù)組有兩張值,0和1,其中0表示沒有顏色,1有顏色
for (int i = 0; i < blockWidth; i++) {
for (int j = 0; j < blockHeight; j++) {
data[i][j] = 0;
//創(chuàng)建中間的方形區(qū)域
if ((i >= blockRadius && i <= blockWidth - blockRadius && j >= blockRadius && j <= blockHeight - blockRadius)) {
data[i][j] = 1;
}
double d1 = Math.pow(i - Objects.requireNonNull(circle1)[0], 2) + Math.pow(j - circle1[1], 2);
double d2 = Math.pow(i - Objects.requireNonNull(circle2)[0], 2) + Math.pow(j - circle2[1], 2);
//創(chuàng)建兩個(gè)凸/凹
if (d1 <= po || d2 <= po) {
data[i][j] = shape;
}
}
}
return data;
}
/**
* 根據(jù)朝向獲取圓心坐標(biāo)
*/
private static int[] getCircleCoords(int face, int blockWidth, int blockHeight, int blockRadius) {
//上
if (0 == face) {
return new int[]{blockWidth / 2 - 1, blockRadius};
}
//左
else if (1 == face) {
return new int[]{blockRadius, blockHeight / 2 - 1};
}
//下
else if (2 == face) {
return new int[]{blockWidth / 2 - 1, blockHeight - blockRadius - 1};
}
//右
else if (3 == face) {
return new int[]{blockWidth - blockRadius - 1, blockHeight / 2 - 1};
}
return null;
}
/**
* 在畫布上添加阻塞塊水印
*/
private static void addBlockWatermark(BufferedImage canvasImage, BufferedImage blockImage, int x, int y) {
Graphics2D graphics2D = canvasImage.createGraphics();
graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f));
graphics2D.drawImage(blockImage, x, y, null);
graphics2D.dispose();
}
/**
* BufferedImage轉(zhuǎn)BASE64
*/
public static String toBase64(BufferedImage bufferedImage, String type) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, type, byteArrayOutputStream);
String base64 = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return String.format("data:image/%s;base64,%s", type, base64);
} catch (IOException e) {
System.out.println("圖片資源轉(zhuǎn)換BASE64失敗");
//異常處理
return null;
}
}
}
2.5 滑動(dòng)驗(yàn)證碼Service
service層,封裝了工具類的一些方法,這里為了簡(jiǎn)單沒寫接口,正常應(yīng)該是service和impl兩個(gè)類。
package com.thcb.captchademo.captcha.service;
import com.thcb.captchademo.captcha.domain.Captcha;
import com.thcb.captchademo.captcha.utils.CaptchaUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.awt.image.BufferedImage;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 滑動(dòng)驗(yàn)證碼Service
*
* @author thcb
* @date 2023-05-25
*/
@Service
public class CaptchaService {
/**
* 拼圖驗(yàn)證碼允許偏差
**/
private static Integer ALLOW_DEVIATION = 3;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 校驗(yàn)驗(yàn)證碼
*
* @param imageKey
* @param imageCode
* @return boolean
**/
public String checkImageCode(String imageKey, String imageCode) {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String key = "imageCode:" + imageKey;
String text = ops.get(key);
if (text == null || text.equals("")) {
return "驗(yàn)證碼已失效";
}
// 根據(jù)移動(dòng)距離判斷驗(yàn)證是否成功
if (Math.abs(Integer.parseInt(text) - Integer.parseInt(imageCode)) > ALLOW_DEVIATION) {
return "驗(yàn)證失敗,請(qǐng)控制拼圖對(duì)齊缺口";
}
// 驗(yàn)證成功,刪除redis緩存
stringRedisTemplate.delete(key);
return null;
}
/**
* 緩存驗(yàn)證碼,有效期1分鐘
*
* @param key
* @param code
**/
public void saveImageCode(String key, String code) {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("imageCode:" + key, code, 60, TimeUnit.SECONDS);
}
/**
* 獲取驗(yàn)證碼拼圖(生成的摳圖和帶摳圖陰影的大圖及摳圖坐標(biāo))
**/
public Object getCaptcha(Captcha captcha) {
//參數(shù)校驗(yàn)
CaptchaUtils.checkCaptcha(captcha);
//獲取畫布的寬高
int canvasWidth = captcha.getCanvasWidth();
int canvasHeight = captcha.getCanvasHeight();
//獲取阻塞塊的寬高/半徑
int blockWidth = captcha.getBlockWidth();
int blockHeight = captcha.getBlockHeight();
int blockRadius = captcha.getBlockRadius();
//獲取資源圖
BufferedImage canvasImage = CaptchaUtils.getBufferedImage(captcha.getPlace());
//調(diào)整原圖到指定大小
canvasImage = CaptchaUtils.imageResize(canvasImage, canvasWidth, canvasHeight);
//隨機(jī)生成阻塞塊坐標(biāo)
int blockX = CaptchaUtils.getNonceByRange(blockWidth, canvasWidth - blockWidth - 10);
int blockY = CaptchaUtils.getNonceByRange(10, canvasHeight - blockHeight + 1);
//阻塞塊
BufferedImage blockImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
//新建的圖像根據(jù)輪廓圖顏色賦值,源圖生成遮罩
CaptchaUtils.cutByTemplate(canvasImage, blockImage, blockWidth, blockHeight, blockRadius, blockX, blockY);
// 移動(dòng)橫坐標(biāo)
String nonceStr = UUID.randomUUID().toString().replaceAll("-", "");
// 緩存
saveImageCode(nonceStr, String.valueOf(blockX));
//設(shè)置返回參數(shù)
captcha.setNonceStr(nonceStr);
captcha.setBlockY(blockY);
captcha.setBlockSrc(CaptchaUtils.toBase64(blockImage, "png"));
captcha.setCanvasSrc(CaptchaUtils.toBase64(canvasImage, "png"));
return captcha;
}
}
2.6 滑動(dòng)驗(yàn)證碼Controller
controller層有兩個(gè)方法,一個(gè)是獲取驗(yàn)證碼的方法,一個(gè)是驗(yàn)證碼校驗(yàn)的方法。
package com.thcb.captchademo.captcha.controller;
import com.thcb.captchademo.captcha.domain.Captcha;
import com.thcb.captchademo.captcha.domain.LoginVo;
import com.thcb.captchademo.captcha.service.CaptchaService;
import com.thcb.captchademo.captcha.utils.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 滑動(dòng)驗(yàn)證碼Controller
*
* @author thcb
* @date 2023-05-25
*/
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private CaptchaService captchaService;
@PostMapping("/slideCaptchaImage")
public AjaxResult getCaptcha() {
return AjaxResult.success(captchaService.getCaptcha(new Captcha()));
}
@PostMapping(value = "/login")
public AjaxResult login(@RequestBody LoginVo loginVo) {
// 驗(yàn)證碼校驗(yàn)
String msg = captchaService.checkImageCode(loginVo.getNonceStr(), loginVo.getValue());
if (msg != null && !msg.equals("")) {
return AjaxResult.error(msg);
}
return AjaxResult.success();
}
}
3 總結(jié)
滑動(dòng)驗(yàn)證碼功能不算復(fù)雜,可以和項(xiàng)目當(dāng)前已有的驗(yàn)證碼共存,調(diào)用不同的接口,返回不同類型的驗(yàn)證碼,當(dāng)然這個(gè)就根據(jù)項(xiàng)目具體情況確定了。
以上就是Spring框架實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼功能的代碼示例的詳細(xì)內(nèi)容,更多關(guān)于Spring滑動(dòng)驗(yàn)證碼功能的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
LeetCode -- Path Sum III分析及實(shí)現(xiàn)方法
這篇文章主要介紹了LeetCode -- Path Sum III分析及實(shí)現(xiàn)方法的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-10-10
如何避免在Java項(xiàng)目里大批量使用if-else?
想起剛開始接觸JAVA時(shí),若遇到大量流程判斷語句,幾乎滿屏都是if-else語句,多得讓自己都忘了哪里是頭,哪里是尾,但是,縱然滿屏是if-else,但彼時(shí)也沒有覺得多別扭.等到編程能力漸漸提升之后,再回過頭去看曾經(jīng)寫過的滿屏if-else時(shí),感覺全都是翔.....,需要的朋友可以參考下2021-06-06
Java集合和IO流實(shí)現(xiàn)水果攤項(xiàng)目
最近閑來無事,使用java基礎(chǔ)知識(shí)集合和IO流做了一個(gè)簡(jiǎn)單的小項(xiàng)目,水果攤項(xiàng)目,用到GUI和Mysql數(shù)據(jù)庫搭建,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-06-06
Springboot3+Redis實(shí)現(xiàn)消息隊(duì)列的多種方法小結(jié)
本文主要介紹了Springboot3+Redis實(shí)現(xiàn)消息隊(duì)列的多種方法小結(jié),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03
SpringBoot整合Dubbo+Zookeeper實(shí)現(xiàn)RPC調(diào)用
這篇文章主要給大家介紹了Spring Boot整合Dubbo+Zookeeper實(shí)現(xiàn)RPC調(diào)用的步驟詳解,文中有詳細(xì)的代碼示例,對(duì)我們的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-07-07
SpringBoot?實(shí)現(xiàn)CAS?Server統(tǒng)一登錄認(rèn)證的詳細(xì)步驟
??CAS(Central?Authentication?Service)中心授權(quán)服務(wù),是一個(gè)開源項(xiàng)目,目的在于為Web應(yīng)用系統(tǒng)提供一種可靠的單點(diǎn)登錄,這篇文章主要介紹了SpringBoot?實(shí)現(xiàn)CAS?Server統(tǒng)一登錄認(rèn)證,需要的朋友可以參考下2024-02-02

