java實現圖片滑動驗證(包含前端程式碼)
前言
1、下面是一個效果展示;
2、先抱怨一下,在部落格上面的抄襲真的非常嚴重,為了實現一個圖片滑動驗證,我搜索了挺久的資料,不過內容翻來覆去就是同樣的內容,千篇一律,作者還各不相同;內容相同我就不多說了,畢竟能解決問題就行,然而恰恰相反,這些東西都沒有為我實質性地解決問題。可能圖片驗證是一個需要前後臺同時互動的功能吧,從業的人員大部分都是偏向後臺或者偏向前臺的,所以寫出來的部落格都不能完整闡述整個流程,下面是我自己實踐完成的內容,記錄一下,供各位參閱斧正。
注:由於使用到的控制元件和工具較多,有許多地方做了省略,這裡只做核心流程的記錄。
一、後端圖片裁剪與生成
首先是一個圖片處理工具VerifyImageUtil.class,它主要的作用是生成兩張圖片:一張被扣除了一部分的原始圖片;一張摳出來圖片。兩兩結合,可以組成一張完整的圖片。原始圖片(target目錄)提供了20張,規格都是590*360的;摳圖需要的模板圖(template目錄)有4張,規格都是93*360的(圖片等各種資源會在文末給出)。將圖片資源匯入到我們專案的靜態資源路徑下(你也可以通過其他方式儲存它們),我這邊是Spring Boot的專案,所以就放在resource下的static目錄下了:
下面是VerifyImageUtil.class
package com.mine.risk.util; import org.apache.commons.lang.StringUtils; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.text.NumberFormat; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Random; /** * 滑塊驗證工具類 * @author : spirit * @date : Created in 10:57 2019/9/05 */ public class VerifyImageUtil { /** 原始檔寬度 */ private static final int ORI_WIDTH = 590; /** 原始檔高度 */ private static final int ORI_HEIGHT = 360; /** 摳圖座標x */ private static int X; /** 摳圖座標y */ private static int Y; /** 模板圖寬度 */ private static int WIDTH; /** 模板圖高度 */ private static int HEIGHT; public static int getX() { return X; } public static int getY() { return Y; } /** * 根據模板切圖 * @param templateFile 模板檔案 * @param targetFile 目標檔案 * @param templateType 模板檔案型別 * @param targetType 目標檔案型別 * @return 切圖map集合 * @throws Exception 異常 */ public static Map<String,byte[]> pictureTemplatesCut(File templateFile,File targetFile,String templateType,String targetType) throws Exception { Map<String,byte[]> pictureMap = new HashMap<>(2); if (StringUtils.isEmpty(templateType) || StringUtils.isEmpty(targetType)) { throw new RuntimeException("file type is empty"); } InputStream targetIs = new FileInputStream(targetFile); // 模板圖 BufferedImage imageTemplate = ImageIO.read(templateFile); WIDTH = imageTemplate.getWidth(); HEIGHT = imageTemplate.getHeight(); // 隨機生成摳圖座標 generateCutoutCoordinates(); // 最終影象 BufferedImage newImage = new BufferedImage(WIDTH,HEIGHT,imageTemplate.getType()); Graphics2D graphics = newImage.createGraphics(); graphics.setBackground(Color.white); int bold = 5; // 獲取感興趣的目標區域 BufferedImage targetImageNoDeal = getTargetArea(X,Y,WIDTH,targetIs,targetType); // 根據模板圖片摳圖 newImage = dealCutPictureByTemplate(targetImageNoDeal,imageTemplate,newImage); // 設定“抗鋸齒”的屬性 graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); graphics.setStroke(new BasicStroke(bold,BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL)); graphics.drawImage(newImage,null); graphics.dispose(); //新建流。 ByteArrayOutputStream os = new ByteArrayOutputStream(); //利用ImageIO類提供的write方法,將bi以png圖片的資料模式寫入流。 ImageIO.write(newImage,"png",os); byte[] newImages = os.toByteArray(); pictureMap.put("newImage",newImages); // 源圖生成遮罩 BufferedImage oriImage = ImageIO.read(targetFile); byte[] oriCopyImages = dealOriPictureByTemplate(oriImage,X,Y); pictureMap.put("oriCopyImage",oriCopyImages); System.out.println("X="+X+";y="+Y); return pictureMap; } /** * 摳圖後原圖生成 * @param oriImage 原始圖片 * @param templateImage 模板圖片 * @param x 座標X * @param y 座標Y * @return 新增遮罩層後的原始圖片 * @throws Exception 異常 */ private static byte[] dealOriPictureByTemplate(BufferedImage oriImage,BufferedImage templateImage,int x,int y) throws Exception { // 原始檔備份影象矩陣 支援alpha通道的rgb影象 BufferedImage oriCopyImage = new BufferedImage(oriImage.getWidth(),oriImage.getHeight(),BufferedImage.TYPE_4BYTE_ABGR); // 原始檔影象矩陣 int[][] oriImageData = getData(oriImage); // 模板影象矩陣 int[][] templateImageData = getData(templateImage); //copy 源圖做不透明處理 for (int i = 0; i < oriImageData.length; i++) { for (int j = 0; j < oriImageData[0].length; j++) { int rgb = oriImage.getRGB(i,j); int r = (0xff & rgb); int g = (0xff & (rgb >> 8)); int b = (0xff & (rgb >> 16)); //無透明處理 rgb = r + (g << 8) + (b << 16) + (255 << 24); oriCopyImage.setRGB(i,j,rgb); } } for (int i = 0; i < templateImageData.length; i++) { for (int j = 0; j < templateImageData[0].length - 5; j++) { int rgb = templateImage.getRGB(i,j); //對原始檔備份影象(x+i,y+j)座標點進行透明處理 if (rgb != 16777215 && rgb <= 0) { int rgb_ori = oriCopyImage.getRGB(x + i,y + j); int r = (0xff & rgb_ori); int g = (0xff & (rgb_ori >> 8)); int b = (0xff & (rgb_ori >> 16)); rgb_ori = r + (g << 8) + (b << 16) + (150 << 24); oriCopyImage.setRGB(x + i,y + j,rgb_ori); } else { //do nothing } } } //新建流 ByteArrayOutputStream os = new ByteArrayOutputStream(); //利用ImageIO類提供的write方法,將bi以png圖片的資料模式寫入流 ImageIO.write(oriCopyImage,os); //從流中獲取資料陣列 return os.toByteArray(); } /** * 根據模板圖片摳圖 * @param oriImage 原始圖片 * @param templateImage 模板圖片 * @return 扣了圖片之後的原始圖片 */ private static BufferedImage dealCutPictureByTemplate(BufferedImage oriImage,BufferedImage targetImage) throws Exception { // 原始檔影象矩陣 int[][] oriImageData = getData(oriImage); // 模板影象矩陣 int[][] templateImageData = getData(templateImage); // 模板影象寬度 for (int i = 0; i < templateImageData.length; i++) { // 模板圖片高度 for (int j = 0; j < templateImageData[0].length; j++) { // 如果模板影象當前畫素點不是白色 copy原始檔資訊到目標圖片中 int rgb = templateImageData[i][j]; if (rgb != 16777215 && rgb <= 0) { targetImage.setRGB(i,oriImageData[i][j]); } } } return targetImage; } /** * 獲取目標區域 * @param x 隨機切圖座標x軸位置 * @param y 隨機切圖座標y軸位置 * @param targetWidth 切圖後目標寬度 * @param targetHeight 切圖後目標高度 * @param ois 原始檔輸入流 * @return 返回目標區域 * @throws Exception 異常 */ private static BufferedImage getTargetArea(int x,int y,int targetWidth,int targetHeight,InputStream ois,String fileType) throws Exception { Iterator<ImageReader> imageReaderList = ImageIO.getImageReadersByFormatName(fileType); ImageReader imageReader = imageReaderList.next(); // 獲取圖片流 ImageInputStream iis = ImageIO.createImageInputStream(ois); // 輸入源中的影象將只按順序讀取 imageReader.setInput(iis,true); ImageReadParam param = imageReader.getDefaultReadParam(); Rectangle rec = new Rectangle(x,y,targetWidth,targetHeight); param.setSourceRegion(rec); return imageReader.read(0,param); } /** * 生成影象矩陣 * @param bufferedImage 圖片流 * @return 影象矩陣 */ private static int[][] getData(BufferedImage bufferedImage){ int[][] data = new int[bufferedImage.getWidth()][bufferedImage.getHeight()]; for (int i = 0; i < bufferedImage.getWidth(); i++) { for (int j = 0; j < bufferedImage.getHeight(); j++) { data[i][j] = bufferedImage.getRGB(i,j); } } return data; } /** * 隨機生成摳圖座標 */ private static void generateCutoutCoordinates() { Random random = new Random(); // ORI_WIDTH:590 ORI_HEIGHT:360 // WIDTH:93 HEIGHT:360 int widthDifference = ORI_WIDTH - WIDTH; int heightDifference = ORI_HEIGHT - HEIGHT; if (widthDifference <= 0) { X = 5; } else { X = random.nextInt(ORI_WIDTH - 3*WIDTH) + 2*WIDTH + 5; } if (heightDifference <= 0) { Y = 5; } else { Y = random.nextInt(ORI_HEIGHT - HEIGHT) + 5; } NumberFormat numberFormat = NumberFormat.getInstance(); numberFormat.setMaximumFractionDigits(2); } }
有了工具類,就可以開始生成圖片內容了,我這邊直接在Spring的控制器生成內容並返回
@RequestMapping("createImgValidate") @ResponseBody public Message createImgValidate(SmsVerificationCodeVo vo){ try { Integer templateNum = new Random().nextInt(4) + 1; Integer targetNum = new Random().nextInt(20) + 1; File templateFile = ResourceUtils.getFile("classpath:static/images/validate/template/"+templateNum+".png"); File targetFile = ResourceUtils.getFile("classpath:static/images/validate/target/"+targetNum+".jpg"); Map<String,byte[]> pictureMap = VerifyImageUtil.pictureTemplatesCut(templateFile,targetFile,ConstString.IMAGE_TYPE_PNG,ConstString.IMAGE_TYPE_JPG); // 將生成的偏移位置資訊設定到redis中 String key = ConstString.WEB_VALID_IMAGE_PREFIX + vo.getTelephone(); boolean verified = redisUtil.exists(key); if(verified){ redisUtil.del(key); } redisUtil.set(key,(VerifyImageUtil.getX()+67)+"",SmsUtil.VALID_IMAGE_TIMEOUT); return ResponseUtil.success(pictureMap); } catch (Exception e) { e.printStackTrace(); return ResponseUtil.info(ResponseEnum.BUSINESS_ERROR); } }
基本的邏輯是從靜態資源中隨機載入一張target圖片和一張template圖片,放到圖片處理工具中,處理並返回我們需要的兩張圖片,生成圖片以後,就可以直接返回這個Map了,它會以base64的方式返回到瀏覽器端。在這裡,偏移的位置資訊屬於敏感內容,它會參與前臺傳入偏移量的對比校驗,所以我這裡存到了redis中,返回的內容也就是Map,只不過我用了一個自定義的返回輔助方法(有興趣的人也可以找我要這些輔助工具)。
二、前端展示圖片
首先還是需要在Spring Boot對應的控制器中,加入生成檢視的程式碼(我做圖片滑動驗證主要為了在傳送手機驗證碼之前做校驗,所以有一個手機號的引數)。
/** * 跳轉到圖片驗證介面 * @return 圖片驗證介面 */ @RequestMapping("imgValidate") public String toImgValidate(ModelMap map,String telephone){ map.addAttribute("telephone",telephone); return "component/imageValidate"; }
之後便是我們的HTML頁碼程式碼:imageValidate.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>圖片驗證</title> <link rel="stylesheet" type="text/css" th:href="@{/static/css/bootstrap.min.css}" /> <link rel="stylesheet" type="text/css" th:href="@{/static/fontawesome/css/all.css}" /> <link rel="stylesheet" type="text/css" th:href="@{/static/css/public.css}" /> <link rel="stylesheet" type="text/css" th:href="@{/static/css/component/imageValidate.css}" /> </head> <body> <div id="container"> <div class="imageDiv"> <img id="validateImage" src=""/> <img id="slideImage" src=""/> </div> <div class="resultDiv"> <button class="btn btn-success" onclick="exchange();"><i class="fas fa-redo"></i>換一組</button> <span id="operateResult"></span> </div> <div> <div id="sliderOuter"> <div id="dragDiv">拖動滑塊完成拼圖</div> <div id="sliderInner"> <i class="fas fa-angle-double-right"></i> <div class="coverIcon"></div> </div> </div> </div> </div> </body> <script th:inline="javascript"> var telephone = [[${telephone}]]; </script> <script type="text/javascript" th:src="@{/static/js/jquery-3.4.0.min.js}" ></script> <script type="text/javascript" th:src="@{/static/js/component/imageValidate.js}" ></script> </html>
然後是對應的JS邏輯程式碼:imageValidate.js。之前說過後臺返回的圖片是轉成base64了的,所以我們在生成圖片的時候,直接在img標籤的src內容前加入data:image/png;base64,即可,注意又一個英文逗號。
var left = 0; $(function(){ // 初始化圖片驗證碼 initImageValidate(); /* 初始化按鈕拖動事件 */ // 滑鼠點選事件 $("#sliderInner").mousedown(function(){ // 滑鼠移動事件 document.onmousemove = function(ev) { left = ev.clientX; if(left >= 67 && left <= 563){ $("#sliderInner").css("left",(left-67)+"px"); $("#slideImage").css("left",(left-67)+"px"); } }; // 滑鼠鬆開事件 document.onmouseup=function(){ document.onmousemove=null; checkImageValidate(); }; }); }); function initImageValidate(){ $.ajax({ async : false,type : "POST",url : "/common/createImgValidate",dataType: "json",data:{ telephone:telephone },success : function(data) { if(data.status < 400){ // 設定圖片的src屬性 $("#validateImage").attr("src","data:image/png;base64,"+data.data.oriCopyImage); $("#slideImage").attr("src","+data.data.newImage); }else{ layer.open({ icon:2,title: "溫馨提示",content: data.info }); } },error : function() {} }); } function exchange(){ initImageValidate(); } // 校驗 function checkImageValidate(){ $.ajax({ async : false,url : "/common/checkImgValidate",data:{ telephone:telephone,offsetHorizontal:left },success : function(data) { if(data.status < 400){ $("#operateResult").html(data.info).css("color","#28a745"); // 校驗通過,呼叫傳送簡訊的函式 parent.getValidateCode(left); }else{ $("#operateResult").html(data.info).css("color","#dc3545"); // 驗證未通過,將按鈕和拼圖恢復至原位置 $("#sliderInner").animate({"left":"0px"},200); $("#slideImage").animate({"left":"0px"},200); } },error : function() {} }); }
最後是css樣式程式碼:imageValidate.css
body{ overflow: hidden; } #container{ width: 100%; } .fontDiv{ margin: 16px 0; } .dragFont{ font-size: 16px; color: dodgerblue; } .imageDiv{ width: 590px; height: 360px; margin: 20px auto 0 auto; position: relative; } .resultDiv{ margin: 10px 20px; } #validateImage{ border-radius: 4px; } #slideImage{ position: absolute; top: 5px; left: 0; } #sliderOuter{ width: 590px; height: 40px; margin: 12px auto; border-radius: 20px; box-shadow: 0 0 10px 5px darkgrey; display: flex; align-items: center; justify-content: center; position: relative; } #dragDiv{ width: 100%; height: 40px; position: absolute; font-size: 16px; color: dodgerblue; text-align: center; line-height: 40px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #sliderInner{ width: 94px; height: 40px; border-radius: 20px; font-size: 2rem; background-color: #28a745; cursor: pointer; position: absolute; left: 0; } #sliderInner i{ position: relative; top: -2px; left: 36px; color: white; } .coverIcon{ width: 100%; height: 100%; position: absolute; top: 0; }
資源包下載:Java圖片滑動驗證
更多關於驗證碼的文章請點選檢視:《java驗證碼》
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。