1. 程式人生 > 程式設計 >java實現圖片滑動驗證(包含前端程式碼)

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驗證碼》

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。