SpringBoot實現(xiàn)滑塊驗證碼驗證登陸校驗功能詳解
前言
驗證碼一直是各類網(wǎng)站登錄和注冊的一種校驗方式,是用來防止有人惡意使用腳本批量進行操作從而設(shè)置的一種安全保護方式。隨著近幾年技術(shù)的發(fā)展,人們對于系統(tǒng)安全性和用戶體驗的要求越來越高,大多數(shù)網(wǎng)站系統(tǒng)都逐漸采用行為驗證碼來代替?zhèn)鹘y(tǒng)的圖片驗證碼。
今天這篇文章就來記錄一下,我是如何實現(xiàn)從前端、到后端校驗的整個流程的。
一、實現(xiàn)效果
無圖無真相,實現(xiàn)的效果如下圖所示,點擊登錄后彈出一個彈出層,拼圖是由后端生成的,拖動滑塊位置,后端校驗是否已拖動到指定的位置。
二、實現(xiàn)思路
整體的實現(xiàn)思路如下:
1、從服務(wù)器隨機取一張底透明有形狀的模板圖,再隨機取一張背景圖、
2、隨機生成摳圖塊坐標
3、根據(jù)步驟2的坐標點,對背景大圖的摳圖區(qū)域的顏色進行處理,新建的圖像根據(jù)輪廓圖顏色賦值,背景圖生成遮罩層。
4、完成以上步驟之后得到兩張圖(扣下來的方塊圖,帶有摳圖區(qū)域陰影的原圖),將這兩張圖和摳圖區(qū)域的y坐標傳到前臺,x坐標存入redis。
5、前端在移動拼圖時將滑動距離x坐標參數(shù)請求后臺驗證,服務(wù)器根據(jù)redis取出x坐標與參數(shù)的x進行比較,如果在伐值內(nèi)則驗證通過。如果滑動不成功,自動刷新圖片,重置拼圖,滑動成功,且賬號密碼正確就直接跳轉(zhuǎn)到首頁。
三、實現(xiàn)步驟
1. 后端 java 代碼
1.1 新建一個拼圖驗證碼類
代碼如下(示例):
@Data public class Captcha { /** * 隨機字符串 **/ private String nonceStr; /** * 驗證值 **/ 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; /** * 阻塞塊的橫軸坐標 **/ private Integer blockX; /** * 阻塞塊的縱軸坐標 **/ private Integer blockY; /** * 圖片獲取位置 **/ private Integer place; }
1.2 新建一個拼圖驗證碼工具類
代碼如下(示例):
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:/Temp/wallpaper/%s.jpg"; /** * 入?yún)⑿r炘O(shè)置默認值 **/ public static void checkCaptcha(Captcha captcha) { //設(shè)置畫布寬度默認值 if (captcha.getCanvasWidth() == null) { captcha.setCanvasWidth(320); } //設(shè)置畫布高度默認值 if (captcha.getCanvasHeight() == null) { captcha.setCanvasHeight(155); } //設(shè)置阻塞塊寬度默認值 if (captcha.getBlockWidth() == null) { captcha.setBlockWidth(65); } //設(shè)置阻塞塊高度默認值 if (captcha.getBlockHeight() == null) { captcha.setBlockHeight(55); } //設(shè)置阻塞塊凹凸半徑默認值 if (captcha.getBlockRadius() == null) { captcha.setBlockRadius(9); } //設(shè)置圖片來源默認值 if (captcha.getPlace() == null) { captcha.setPlace(0); } } /** * 獲取指定范圍內(nèi)的隨機數(shù) **/ public static int getNonceByRange(int start, int end) { Random random = new Random(); return random.nextInt(end - start + 1) + start; } /** * 獲取驗證碼資源圖 **/ public static BufferedImage getBufferedImage(Integer place) { try { //隨機圖片 int nonce = getNonceByRange(0, 1000); //獲取網(wǎng)絡(luò)資源圖片 if (0 == place) { String imgUrl = String.format(IMG_URL, nonce); URL url = new URL(imgUrl); return ImageIO.read(url.openStream()); } //獲取本地圖片 else { 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 { //原圖中對應(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è)置為白色,取帶像素和無像素的界點,判斷該點是不是臨界輪廓點 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ù)組下標越界異常 } } } //在畫布上添加阻塞塊水印 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); //隨機生成兩個圓的坐標,在4個方向上 隨機找到2個方向添加凸/凹 //凸/凹1 int face1 = RandomUtils.nextInt(0,4); //凸/凹2 int face2; //保證兩個凸/凹不在同一位置 do { face2 = RandomUtils.nextInt(0,4); } while (face1 == face2); //獲取凸/凹起位置坐標 int[] circle1 = getCircleCoords(face1, blockWidth, blockHeight, blockRadius); int[] circle2 = getCircleCoords(face2, blockWidth, blockHeight, blockRadius); //隨機凸/凹類型 int shape = getNonceByRange(0, 1); //圓的標準方程 (x-a)2+(y-b)2=r2,標識圓心(a,b),半徑為r的圓 //計算需要的小圖輪廓,用二維數(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)建兩個凸/凹 if (d1 <= po || d2 <= po) { data[i][j] = shape; } } } return data; } /** * 根據(jù)朝向獲取圓心坐標 */ 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; } } }
1.3 新建一個 service 類
代碼如下(示例):
@Service public class CaptchaService { /** * 拼圖驗證碼允許偏差 **/ private static Integer ALLOW_DEVIATION = 3; @Autowired private StringRedisTemplate stringRedisTemplate; /** * 校驗驗證碼 * @param imageKey * @param imageCode * @return boolean **/ public String checkImageCode(String imageKey, String imageCode) { ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); String text = ops.get("imageCode:" + imageKey); if(StrUtil.isBlank(text)){ return "驗證碼已失效"; } // 根據(jù)移動距離判斷驗證是否成功 if (Math.abs(Integer.parseInt(text) - Integer.parseInt(imageCode)) > ALLOW_DEVIATION) { return "驗證失敗,請控制拼圖對齊缺口"; } return null; } /** * 緩存驗證碼,有效期15分鐘 * @param key * @param code **/ public void saveImageCode(String key, String code) { ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); ops.set("imageCode:" + key, code, 15, TimeUnit.MINUTES); } /** * 獲取驗證碼拼圖(生成的摳圖和帶摳圖陰影的大圖及摳圖坐標) **/ public Object getCaptcha(Captcha captcha) { //參數(shù)校驗 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); //隨機生成阻塞塊坐標 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); // 移動橫坐標 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; } }
1.4 新建一個 controller 類
代碼如下(示例):
@RestController @RequestMapping("/captcha") public class CaptchaController { @Autowired private CaptchaService captchaService; @ApiOperation(value = "生成驗證碼拼圖") @PostMapping("get-captcha") public R getCaptcha(@RequestBody Captcha captcha) { return R.ok(captchaService.getCaptcha(captcha)); } }
1.5 登錄接口
代碼如下(示例):
@ApiOperation(value = "登錄") @PostMapping(value = "login") public R login(@RequestBody LoginVo loginVo) { // 只有開啟了驗證碼功能才需要驗證 if (needAuthCode) { String msg = captchaService.checkImageCode(loginVo.getNonceStr(),loginVo.getValue()); if (StringUtils.isNotBlank(msg)) { return R.error(msg); } } String token = loginService.login(loginVo.getUserName(), loginVo.getPassWord()); if (StringUtils.isBlank(token)) { return R.error("用戶名或密碼錯誤"); } Map<String, String> tokenMap = new HashMap<>(); tokenMap.put("token", token); tokenMap.put("tokenHead", tokenHead); return R.ok(tokenMap); }
2. 前端 vue 代碼
2.1 新建一個 sliderVerify 組件
代碼如下(示例):
<template> <div class="slide-verify" :style="{width: canvasWidth + 'px'}" onselectstart="return false;"> <!-- 圖片加載遮蔽罩 --> <div :class="{'img-loading': isLoading}" :style="{height: canvasHeight + 'px'}" v-if="isLoading"/> <!-- 認證成功后的文字提示 --> <div class="success-hint" :style="{height: canvasHeight + 'px'}" v-if="verifySuccess">{{ successHint }}</div> <!--刷新按鈕--> <div class="refresh-icon" @click="refresh"/> <!--前端生成--> <template v-if="isFrontCheck"> <!--驗證圖片--> <canvas ref="canvas" class="slide-canvas" :width="canvasWidth" :height="canvasHeight"/> <!--阻塞塊--> <canvas ref="block" class="slide-block" :width="canvasWidth" :height="canvasHeight"/> </template> <!--后端生成--> <template v-else> <!--驗證圖片--> <img ref="canvas" class="slide-canvas" :width="canvasWidth" :height="canvasHeight"/> <!--阻塞塊--> <img ref="block" :class="['slide-block', {'verify-fail': verifyFail}]"/> </template> <!-- 滑動條 --> <div class="slider" :class="{'verify-active': verifyActive, 'verify-success': verifySuccess, 'verify-fail': verifyFail}"> <!--滑塊--> <div class="slider-box" :style="{width: sliderBoxWidth}"> <!-- 按鈕 --> <div class="slider-button" id="slider-button" :style="{left: sliderButtonLeft}"> <!-- 按鈕圖標 --> <div class="slider-button-icon"/> </div> </div> <!--滑動條提示文字--> <span class="slider-hint">{{ sliderHint }}</span> </div> </div> </template> <script> function sum(x, y) { return x + y; } function square(x) { return x * x; } import { getCodeImg } from "@/api/login"; export default { name: 'sliderVerify', props: { // 阻塞塊長度 blockLength: { type: Number, default: 42, }, // 阻塞塊弧度 blockRadius: { type: Number, default: 10, }, // 畫布寬度 canvasWidth: { type: Number, default: 320, }, // 畫布高度 canvasHeight: { type: Number, default: 155, }, // 滑塊操作提示 sliderHint: { type: String, default: '向右滑動', }, // 可允許的誤差范圍?。粸?時,則表示滑塊要與凹槽完全重疊,才能驗證成功。默認值為5,若為 -1 則不進行機器判斷 accuracy: { type: Number, default: 3, }, // 圖片資源數(shù)組 imageList: { type: Array, default: () => [], }, }, data() { return { // 前端校驗 isFrontCheck: false, // 校驗進行狀態(tài) verifyActive: false, // 校驗成功狀態(tài) verifySuccess: false, // 校驗失敗狀態(tài) verifyFail: false, // 阻塞塊對象 blockObj: null, // 圖片畫布對象 canvasCtx: null, // 阻塞塊畫布對象 blockCtx: null, // 阻塞塊寬度 blockWidth: this.blockLength * 2, // 阻塞塊的橫軸坐標 blockX: undefined, // 阻塞塊的縱軸坐標 blockY: undefined, // 圖片對象 image: undefined, // 移動的X軸坐標 originX: undefined, // 移動的Y軸做坐標 originY: undefined, // 拖動距離數(shù)組 dragDistanceList: [], // 滑塊箱拖動寬度 sliderBoxWidth: 0, // 滑塊按鈕距離左側(cè)起點位置 sliderButtonLeft: 0, // 鼠標按下狀態(tài) isMouseDown: false, // 圖片加載提示,防止圖片沒加載完就開始驗證 isLoading: true, // 時間戳,計算滑動時長 timestamp: null, // 成功提示 successHint: '', // 隨機字符串 nonceStr: undefined, }; }, mounted() { this.init(); }, methods: { /* 初始化*/ init() { this.initDom(); this.bindEvents(); }, /* 初始化DOM對象*/ initDom() { this.blockObj = this.$refs.block; if (this.isFrontCheck) { this.canvasCtx = this.$refs.canvas.getContext('2d'); this.blockCtx = this.blockObj.getContext('2d'); this.initImage(); } else { this.getCaptcha(); } }, /* 后臺獲取驗證碼*/ getCaptcha() { let self = this; //取后端默認值 const data = {}; getCodeImg(data).then((response) => { const data = response.data; self.nonceStr = data.nonceStr; self.$refs.block.src = data.blockSrc; self.$refs.block.style.top = data.blockY + 'px'; self.$refs.canvas.src = data.canvasSrc; }).finally(() => { self.isLoading = false; }); }, /* 前端獲取驗證碼*/ initImage() { const image = this.createImage(() => { this.drawBlock(); let {canvasWidth, canvasHeight, blockX, blockY, blockRadius, blockWidth} = this; this.canvasCtx.drawImage(image, 0, 0, canvasWidth, canvasHeight); this.blockCtx.drawImage(image, 0, 0, canvasWidth, canvasHeight); // 將摳圖防止最左邊位置 let yAxle = blockY - blockRadius * 2; let ImageData = this.blockCtx.getImageData(blockX, yAxle, blockWidth, blockWidth); this.blockObj.width = blockWidth; this.blockCtx.putImageData(ImageData, 0, yAxle); // 圖片加載完關(guān)閉遮蔽罩 this.isLoading = false; // 前端校驗設(shè)置特殊值 this.nonceStr = 'loyer'; }); this.image = image; }, /* 創(chuàng)建image對象*/ createImage(onload) { const image = document.createElement('img'); image.crossOrigin = 'Anonymous'; image.onload = onload; image.onerror = () => { image.src = require('../../assets/images/bgImg.jpg'); }; image.src = this.getImageSrc(); return image; }, /* 獲取imgSrc*/ getImageSrc() { const len = this.imageList.length; return len > 0 ? this.imageList[this.getNonceByRange(0, len)] : `https://loyer.wang/view/ftp/wallpaper/${this.getNonceByRange(1, 1000)}.jpg`; }, /* 根據(jù)指定范圍獲取隨機數(shù)*/ getNonceByRange(start, end) { return Math.round(Math.random() * (end - start) + start); }, /* 繪制阻塞塊*/ drawBlock() { this.blockX = this.getNonceByRange(this.blockWidth + 10, this.canvasWidth - (this.blockWidth + 10)); this.blockY = this.getNonceByRange(10 + this.blockRadius * 2, this.canvasHeight - (this.blockWidth + 10)); this.draw(this.canvasCtx, 'fill'); this.draw(this.blockCtx, 'clip'); }, /* 繪制事件*/ draw(ctx, operation) { const PI = Math.PI; let {blockX: x, blockY: y, blockLength: l, blockRadius: r} = this; // 繪制 ctx.beginPath(); ctx.moveTo(x, y); ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI); ctx.lineTo(x + l, y); ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI); ctx.lineTo(x + l, y + l); ctx.lineTo(x, y + l); ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true); ctx.lineTo(x, y); // 修飾 ctx.lineWidth = 2; ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'; ctx.stroke(); ctx[operation](); ctx.globalCompositeOperation = 'destination-over'; }, /* 事件綁定*/ bindEvents() { // 監(jiān)聽鼠標按下事件 document.getElementById('slider-button').addEventListener('mousedown', (event) => { this.startEvent(event.clientX, event.clientY); }); // 監(jiān)聽鼠標移動事件 document.addEventListener('mousemove', (event) => { this.moveEvent(event.clientX, event.clientY); }); // 監(jiān)聽鼠標離開事件 document.addEventListener('mouseup', (event) => { this.endEvent(event.clientX); }); // 監(jiān)聽觸摸開始事件 document.getElementById('slider-button').addEventListener('touchstart', (event) => { this.startEvent(event.changedTouches[0].pageX, event.changedTouches[0].pageY); }); // 監(jiān)聽觸摸滑動事件 document.addEventListener('touchmove', (event) => { this.moveEvent(event.changedTouches[0].pageX, event.changedTouches[0].pageY); }); // 監(jiān)聽觸摸離開事件 document.addEventListener('touchend', (event) => { this.endEvent(event.changedTouches[0].pageX); }); }, /* 校驗圖片是否存在*/ checkImgSrc() { if (this.isFrontCheck) { return true; } return !!this.$refs.canvas.src; }, /* 滑動開始事件*/ startEvent(originX, originY) { if (!this.checkImgSrc() || this.isLoading || this.verifySuccess) { return; } this.originX = originX; this.originY = originY; this.isMouseDown = true; this.timestamp = +new Date(); }, /* 滑動事件*/ moveEvent(originX, originY) { if (!this.isMouseDown) { return false; } const moveX = originX - this.originX; const moveY = originY - this.originY; if (moveX < 0 || moveX + 40 >= this.canvasWidth) { return false; } this.sliderButtonLeft = moveX + 'px'; let blockLeft = (this.canvasWidth - 40 - 20) / (this.canvasWidth - 40) * moveX; this.blockObj.style.left = blockLeft + 'px'; this.verifyActive = true; this.sliderBoxWidth = moveX + 'px'; this.dragDistanceList.push(moveY); }, /* 滑動結(jié)束事件*/ endEvent(originX) { if (!this.isMouseDown) { return false; } this.isMouseDown = false; if (originX === this.originX) { return false; } // 開始校驗 this.isLoading = true; // 校驗結(jié)束 this.verifyActive = false; // 滑動時長 this.timestamp = +new Date() - this.timestamp; // 移動距離 const moveLength = parseInt(this.blockObj.style.left); // 限制操作時長10S,超出判斷失敗 if (this.timestamp > 10000) { this.verifyFailEvent(); } else // 人為操作判定 if (!this.turingTest()) { this.verifyFail = true; this.$emit('again'); } else // 是否前端校驗 if (this.isFrontCheck) { const accuracy = this.accuracy <= 1 ? 1 : this.accuracy > 10 ? 10 : this.accuracy; // 容錯精度值 const spliced = Math.abs(moveLength - this.blockX) <= accuracy; // 判斷是否重合 if (!spliced) { this.verifyFailEvent(); } else { // 設(shè)置特殊值,后臺特殊處理,直接驗證通過 this.$emit('success', {nonceStr: this.nonceStr, value: moveLength}); } } else { this.$emit('success', {nonceStr: this.nonceStr, value: moveLength}); } }, /* 圖靈測試*/ turingTest() { const arr = this.dragDistanceList; // 拖動距離數(shù)組 const average = arr.reduce(sum) / arr.length; // 平均值 const deviations = arr.map((x) => x - average); // 偏離值 const stdDev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length); // 標準偏差 return average !== stdDev; // 判斷是否人為操作 }, /* 校驗成功*/ verifySuccessEvent() { this.isLoading = false; this.verifySuccess = true; const elapsedTime = (this.timestamp / 1000).toFixed(1); if (elapsedTime < 1) { this.successHint = `僅僅${elapsedTime}S,你的速度快如閃電`; } else if (elapsedTime < 2) { this.successHint = `只用了${elapsedTime}S,這速度簡直完美`; } else { this.successHint = `耗時${elapsedTime}S,爭取下次再快一點`; } }, /* 校驗失敗*/ verifyFailEvent(msg) { this.verifyFail = true; this.$emit('fail', msg); this.refresh(); }, /* 刷新圖片驗證碼*/ refresh() { // 延遲class的刪除,等待動畫結(jié)束 setTimeout(() => { this.verifyFail = false; }, 500); this.isLoading = true; this.verifyActive = false; this.verifySuccess = false; this.blockObj.style.left = 0; this.sliderBoxWidth = 0; this.sliderButtonLeft = 0; if (this.isFrontCheck) { // 刷新畫布 let {canvasWidth, canvasHeight} = this; this.canvasCtx.clearRect(0, 0, canvasWidth, canvasHeight); this.blockCtx.clearRect(0, 0, canvasWidth, canvasHeight); this.blockObj.width = canvasWidth; // 刷新圖片 this.image.src = this.getImageSrc(); } else { this.getCaptcha(); } }, }, }; </script> <style scoped> .slide-verify { position: relative; } /*圖片加載樣式*/ .img-loading { position: absolute; top: 0; right: 0; left: 0; bottom: 0; z-index: 999; animation: loading 1.5s infinite; background-image: url(../../assets/images/loading.svg); background-repeat: no-repeat; background-position: center center; background-size: 100px; background-color: #737c8e; border-radius: 5px; } @keyframes loading { 0% { opacity: .7; } 100% { opacity: 9; } } /*認證成功后的文字提示*/ .success-hint { position: absolute; top: 0; right: 0; left: 0; z-index: 999; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.8); color: #2CD000; font-size: large; } /*刷新按鈕*/ .refresh-icon { position: absolute; right: 0; top: 0; width: 35px; height: 35px; cursor: pointer; background: url("../../assets/images/light.png") 0 -432px; background-size: 35px 470px; } /*驗證圖片*/ .slide-canvas { border-radius: 5px; } /*阻塞塊*/ .slide-block { position: absolute; left: 0; top: 0; } /*校驗失敗時的阻塞塊樣式*/ .slide-block.verify-fail { transition: left 0.5s linear; } /*滑動條*/ .slider { position: relative; text-align: center; width: 100%; height: 40px; line-height: 40px; margin-top: 15px; background: #f7f9fa; color: #45494c; border: 1px solid #e4e7eb; border-radius: 5px; } /*滑動盒子*/ .slider-box { position: absolute; left: 0; top: 0; height: 40px; border: 0 solid #1991FA; background: #D1E9FE; border-radius: 5px; } /*滑動按鈕*/ .slider-button { position: absolute; top: 0; left: 0; width: 40px; height: 40px; background: #fff; box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); cursor: pointer; transition: background .2s linear; border-radius: 5px; } /*鼠標懸浮時的按鈕樣式*/ .slider-button:hover { background: #1991FA } /*鼠標懸浮時的按鈕圖標樣式*/ .slider-button:hover .slider-button-icon { background-position: 0 -13px } /*滑動按鈕圖標*/ .slider-button-icon { position: absolute; top: 15px; left: 13px; width: 15px; height: 13px; background: url("../../assets/images/light.png") 0 -26px; background-size: 35px 470px } /*校驗時的按鈕樣式*/ .verify-active .slider-button { height: 38px; top: -1px; border: 1px solid #1991FA; } /*校驗時的滑動箱樣式*/ .verify-active .slider-box { height: 38px; border-width: 1px; } /*校驗成功時的滑動箱樣式*/ .verify-success .slider-box { height: 38px; border: 1px solid #52CCBA; background-color: #D2F4EF; } /*校驗成功時的按鈕樣式*/ .verify-success .slider-button { height: 38px; top: -1px; border: 1px solid #52CCBA; background-color: #52CCBA !important; } /*校驗成功時的按鈕圖標樣式*/ .verify-success .slider-button-icon { background-position: 0 0 !important; } /*校驗失敗時的滑動箱樣式*/ .verify-fail .slider-box { height: 38px; border: 1px solid #f57a7a; background-color: #fce1e1; transition: width 0.5s linear; } /*校驗失敗時的按鈕樣式*/ .verify-fail .slider-button { height: 38px; top: -1px; border: 1px solid #f57a7a; background-color: #f57a7a !important; transition: left 0.5s linear; } /*校驗失敗時的按鈕圖標樣式*/ .verify-fail .slider-button-icon { top: 14px; background-position: 0 -82px !important; } /*校驗狀態(tài)下的提示文字隱藏*/ .verify-active .slider-hint, .verify-success .slider-hint, .verify-fail .slider-hint { display: none; } </style>
2.2 在登錄頁使用滑塊組件
代碼如下(示例):
<template> <div class="login-container"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left"> ...省略無關(guān)代碼 <!--滑塊驗證--> <el-dialog title="請拖動滑塊完成拼圖" width="360px" :visible.sync="isShowSliderVerify" :close-on-click-modal="false" @closed="refresh" append-to-body> <slider-verify ref="sliderVerify" @success="onSuccess" @fail="onFail" @again="onAgain"/> </el-dialog> <el-button :loading="loading" round style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登錄</el-button> </el-form> </div> </template> <script> import sliderVerify from './sliderVerify'; export default { name: 'Login', components: { sliderVerify, }, data() { return { loginForm: { userName: '', passWord: '', // 隨機字符串 nonceStr: '', // 驗證值 value: '', }, loading: false, // 是否顯示滑塊驗證 isShowSliderVerify: false, } }, methods: { /* 提交*/ handleLogin() { let self = this; self.$refs.loginForm.validate((flag) => { self.isShowSliderVerify = flag; }); }, /* 登錄*/ login() { let self = this; self.loading = true; self.$store.dispatch('user/login', self.loginForm).then(() => { self.$refs.sliderVerify.verifySuccessEvent(); setTimeout(() => { self.isShowSliderVerify = false; self.message('success', '登錄成功'); }, 500); this.$router.push({ path: this.redirect || '/' }) }).catch(() => { self.$refs.sliderVerify.verifyFailEvent(); }); }, /* 滑動驗證成功*/ onSuccess(captcha) { Object.assign(this.loginForm, captcha); this.login(); }, /* 滑動驗證失敗*/ onFail(msg) { //this.message('error', msg || '驗證失敗,請控制拼圖對齊缺口'); }, /* 滑動驗證異常*/ onAgain() { this.message('error', '滑動操作異常,請重試'); }, /* 刷新驗證碼*/ refresh() { this.$refs.sliderVerify.refresh(); }, /* 提示彈框*/ message(type, message) { this.$message({ showClose: true, type: type, message: message, duration: 1500, }); }, }, } </script>
到此這篇關(guān)于SpringBoot實現(xiàn)滑塊驗證碼驗證登陸校驗功能詳解的文章就介紹到這了,更多相關(guān)SpringBoot滑塊驗證碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用apache 的FileUtils處理文件的復制等操作方式
這篇文章主要介紹了使用apache 的FileUtils處理文件的復制等操作方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07什么情況下會出現(xiàn)java.io.IOException?:?Broken?pipe這個錯誤以及解決辦法
這篇文章主要介紹了什么情況下會出現(xiàn)java.io.IOException?:?Broken?pipe這個錯誤以及解決辦法的相關(guān)資料,這個錯誤表示通信另一端已關(guān)閉連接,常發(fā)生在客戶端關(guān)閉連接、網(wǎng)絡(luò)超時或資源不足等情況,文中將解決辦法介紹的非常詳細,需要的朋友可以參考下2024-10-10Spring BeanPostProcessor(后置處理器)的用法
這篇文章主要介紹了Spring BeanPostProcessor(后置處理器)的用法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10使用SpringJPA?直接實現(xiàn)count(*)
這篇文章主要介紹了SpringJPA?直接實現(xiàn)count(*),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11Java圖片與二進制相互轉(zhuǎn)換實現(xiàn)示例講解
這篇文章主要介紹了Java圖片與二進制相互轉(zhuǎn)換實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-03-03