完整說明使用SpringBoot+js實現滑動圖片驗證


 

常見的網站驗證方式有手機短信驗證,圖片字符驗證,滑塊驗證,滑塊圖片驗證.本文主要講解的是滑塊圖片驗證的實現流程.包括后台和前端的實現.

 

實現效果

 

使用的API

java.awt.image.BufferedImage

BufferedImage是Java類庫中是一個帶緩沖區圖像類,主要作用是將一幅圖片加載到內存中(BufferedImage生成的圖片在內存里有一個圖像緩沖區,利用這個緩沖區我們可以很方便地操作這個圖片),提供獲得繪圖對象、圖像縮放、選擇圖像平滑度等功能,通常用來做圖片大小變換、圖片變灰、設置透明不透明等。

 

常見的api有

讀取一張圖片

String imgPath = "/demo.jpg";  
BufferedImage image = ImageIO.read(new FileInputStream(imgPath));

保存文件

ImageIO.write(image,"png",new File("xx.png"));

ImageIO提供read()和write()靜態方法,讀寫圖片,比以往的InputStream讀寫更方便。

像素處理

getRGB(int x, int y)
setRGB(intx ,inty,int rgb)

 

獲取Graphics2D對象

Graphics2D g2d = parentImage.createGraphics();


Graphics2D是一個畫圖工具,可以實現對圖片進行畫畫處理,比如花直線,圓,方形等操作.除此之外,還可以創建透明背景的圖片.本方案就是用它來對扣出來的圖透明化處理.


Thumbnails

該類用於對圖片進行壓縮以符合大小要求.

 
        
Thumbnails.of(image)
           .forceSize(width,height) //.width(width).height(height) .asBufferedImage();

  

使用forceSize強制大小時,對圖片會有一定的像素損耗.使用width(width).height(height)時圖片的大小不會和設定的一致.

這里用來對網上下載的圖片進行大小處理,當然也可以用其他圖像處理工具,比如PS

依賴

<dependency>
   <groupId>net.coobird</groupId>
   <artifactId>thumbnailator</artifactId>
   <version>0.4.11</version>
</dependency>

  

基礎知識

一張圖片的大小通常用像素數量來表示,比如1320*600(寬*高),對於彩色圖片,每一個像素點有RGB來表示.比如(250,180,0)表示黃色,或者使用十六進制表示#FFB400.

 

RGB查詢

https://tool.oschina.net/commons?type=3

 

因此要想把一張圖扣出來一部分,只要確定好摳圖區域,使用摳圖區域的像素值來創建另一張圖,原摳圖區域填充其他像素值.就可以得到兩張分體圖,組合在一起構成一副完整的圖.而對於被扣出來的圖,還要對其進行透明化處理.

 

處理流程

圖片校驗最需要考慮的是安全性,因此需要將數據發送給后端進行校驗,而不是在前端進行校驗.

 

1.前端刷新圖片請求給后台

2.后台收到請后后開始進行處理

3.后台隨機從圖片文件夾中取一張圖片

4.對圖片進行大小檢測,圖片偏小則拋出異常,偏大則進行截圖處理.實際生產環境應該使用處理好的圖片,就可以省略這一步

5.確定摳圖區的原點坐標,為了保證效果.將圖片寬度分成四段,最終的原點橫坐標位於2/4-3/4范圍內.坐標示意如下,圖片左上腳是原點(x0,y0)

  (x0,y0)         (xMax,y0)
        ****************
        *              *
        *              *
        *              *
        ****************
       (x0,yMax)  (xMax,yMax)

  

6.根據摳圖區的原點對摳圖區進行標定.標定區如上圖所示,由正方形和半圓組成,其中一邊的半圓突出,另一邊凹陷.

7.對原圖的被摳區域進行灰度處理,填充其他顏色

8.對摳出來的圖進行背景透明化處理,並截圖(截取圖片范圍內的圖)

9.返回扣出來圖的大小,偏移量,兩張圖的base64數據返回給前端

10.前端渲染圖片

11.移動滑塊,滑塊移動時摳出來的圖也會跟着移動,兩者移動的偏移量是一樣的.鼠標釋放時將偏移量發送給后端進行校驗.

12.后端校驗后將結果返回給前端,前端根據該結果做不同的處理

13.一般是登錄才進行此類的圖片驗證,因此在點擊登錄時,偏移量仍然和帳號密碼再次發送,后端再一次進行校驗

14.完成

后台實現

屬性說明

 

//圖片的路徑
private  String basePathClasspath = "img/";
private  String basePathFile = "src/main/resources/img/";

private  String basePath = basePathFile;
private  String basePathOutput = "src/main/resources/img/out/";
//圖片的最大大小
private  static int IMAGE_MAX_WIDTH = 300;
private  static int IMAGE_MAX_HEIGHT = 260;
//摳圖上面的半徑
private   static int RADIUS = IMAGE_MAX_WIDTH/20;
//摳圖區域的高度
private   static int CUT_HEIGHT = IMAGE_MAX_WIDTH/5;
//摳圖區域的寬度
private   static int CUT_WIDTH = IMAGE_MAX_WIDTH/5;
//被扣地方填充的顏色
private   static int FLAG = 0x778899;
//輸出圖片后綴
private   static String IMAGE_SUFFIX =  "png";

//
private  int imageOffset = 0;

//摳圖部分凸起的方向
private  Location location;

ImageResult imageResult = new ImageResult();

//
private  String ORI_IMAGE_KEY = "ORI_IMAGE_KEY";
private  String CUT_IMAGE_KEY = "CUT_IMAGE_KEY";

//摳圖區的原點坐標(x0,y0)

/*
  (x0,y0)         (xMax,y0)
    ****************
    *              *
    *              *
    *              *
    ****************
   (x0,yMax)   (xMax,yMax)
*/
private  int XPOS;
private  int YPOS;

 

對外提供的接口 

也是任務的流程處理

public  ImageResult imageResult(File file) throws IOException {


        
        log.info("file = {}",file.getName());

        BufferedImage oriBufferedImage = getBufferedImage(file);

        //檢測圖片大小
        oriBufferedImage = checkImage(oriBufferedImage);

        //初始化方形的原點坐標
        createXYPos(oriBufferedImage);
        //獲取被扣圖像的標志圖
        int[][] blockData = getBlockData(oriBufferedImage);
        //printBlockData(blockData);

        //計算摳圖區域的信息
        createImageMessage();

        //獲取扣了圖的原圖和被扣部分的圖
        Map<String,BufferedImage> imageMap =  cutByTemplate(oriBufferedImage,blockData);

     //處理完成
     //設置返回的數據 imageResult.setOriImage(ImageBase64(imageMap.get(ORI_IMAGE_KEY))); imageResult.setCutImage(ImageBase64(imageMap.get(CUT_IMAGE_KEY))); imageResult.setXpos(imageMessage.getXpos()); imageResult.setYpos(imageMessage.getYpos()); imageResult.setCutImageWidth(imageMessage.getCutImageWidth()); imageResult.setCutImageHeight(imageMessage.getCutImageHeight());
return imageResult; }

 

確定原點的坐標

這里需要注意兩點

一是摳圖區域不能超過原圖范圍,因此隨機生成范圍需要減去摳圖區的長度和半圓的半徑

二是為了保證用戶體驗,限定橫坐標在圖像的2/4-3/4處,才能保證滑塊有一定的滑程,同時保證摳圖不會超出原圖范圍.

/**
     *功能描述 獲取摳圖區的坐標原點
     * @author lgj
     * @Description
     * @date 3/29/20
     * @param:
     * @return:  void
     *
    */
    public  void createXYPos(BufferedImage oriImage){

        int height = oriImage.getHeight();
        int width = oriImage.getWidth();

        
        XPOS = new Random().nextInt(width-CUT_WIDTH-RADIUS);
        YPOS = new Random().nextInt(height-CUT_HEIGHT-RADIUS);

        //確保橫坐標位於2/4--3/4
        int div = (IMAGE_MAX_WIDTH/4);

        if(XPOS/div ==  0 ){
            XPOS = XPOS + div*2;
        }
        else if(XPOS/div ==  1 ){
            XPOS = XPOS + div;
        }
        else if(XPOS/div ==  3 ){
            XPOS = XPOS - div;
        }

    }

 

標記摳圖區域

這里使用一個二維數組locations[width][height]來保存摳圖標記數據,每一個數據表示位置和是否為摳圖區,使用常量FLAG進行標記

這里的摳圖區為一個巨型加上突出的半圓和凹陷的半圓.

 

對於半圓的處理參考該公式: (x-a)2+(y-b)2=R2, 

其中(a,b)為圓中心的坐標,R為圓半徑.(x,y)為任一坐標

(x,y)在圓上: (x-a)2+(y-b)2 == R

 (x,y)在圓內: (x-a)2+(y-b)2   < R2

 (x,y) 在圓外: (x-a)2+(y-b)2  > R2

 

 

public  int[][] getBlockData(BufferedImage oriImage){

        int height = oriImage.getHeight();
        int width = oriImage.getWidth();
        int[][] blockData =new int[width][height];

        Location locations[] = {Location.UP,Location.LEFT,Location.DOWN,Location.RIGHT};

        //矩形
        for(int x = 0; x< width; x++){
            for(int y = 0; y < height; y++){

                blockData[x][y] = 0;
                if ( (x > XPOS) && (x < (XPOS+CUT_WIDTH))
                    && (y > YPOS) && (y < (YPOS+CUT_HEIGHT))){
                    blockData[x][y] = FLAG;
                }
            }
        }

        //圓形突出區域
        //突出圓形的原點坐標(x,y)
        int xBulgeCenter=0,yBulgeCenter=0;
        //
        int xConcaveCenter=0,yConcaveCenter=0;

        //位於矩形的哪一邊,0123--上下左右
        location = locations[new Random().nextInt(3)];
        if(location == Location.UP){
            //上 凸起
            xBulgeCenter = XPOS +  CUT_WIDTH/2;
            yBulgeCenter = YPOS;
            //左 凹陷
            xConcaveCenter = XPOS ;
            yConcaveCenter = YPOS + CUT_HEIGHT/2;

        }
        else if(location == Location.DOWN){
            //下 凸起
            xBulgeCenter = XPOS +  CUT_WIDTH/2;
            yBulgeCenter = YPOS + CUT_HEIGHT;

            //右 凹陷
            xConcaveCenter = XPOS +  CUT_WIDTH;
            yConcaveCenter = YPOS + CUT_HEIGHT/2;
        }
        else if(location == Location.LEFT){
            //左 凸起
            xBulgeCenter = XPOS ;
            yBulgeCenter = YPOS + CUT_HEIGHT/2;

            //下 凹陷
            xConcaveCenter = XPOS +  CUT_WIDTH/2;
            yConcaveCenter = YPOS + CUT_HEIGHT;

        }
        else {
            //Location.RIGHT
            //右 凸起
            xBulgeCenter = XPOS +  CUT_WIDTH;
            yBulgeCenter = YPOS + CUT_HEIGHT/2;
            //上 凹陷
            xConcaveCenter = XPOS +  CUT_WIDTH/2;
            yConcaveCenter = YPOS;


        }



        //for test
        log.info("突出圓形位置:"+location);

        log.info("XPOS={}  YPOS={}",XPOS,YPOS);
        log.info("xBulgeCenter={}  yBulgeCenter={}",xBulgeCenter,yBulgeCenter);
        log.info("xConcaveCenter={}  yConcaveCenter={}",xConcaveCenter,yConcaveCenter);

        //半徑的平方
        int RADIUS_POW2 = RADIUS * RADIUS;

        //凸起部分
        for(int x = xBulgeCenter-RADIUS; x< xBulgeCenter+RADIUS; x++){
            for(int y = yBulgeCenter-RADIUS; y < yBulgeCenter+RADIUS; y++){
                //(x-a)2+(y-b)2 = r2

                if(Math.pow((x-xBulgeCenter),2) + Math.pow((y-yBulgeCenter),2) <= RADIUS_POW2){
                    blockData[x][y] = FLAG;
                }
            }
        }

        //凹陷部分
        for(int x = xConcaveCenter-RADIUS; x< xConcaveCenter+RADIUS; x++){
            for(int y = yConcaveCenter-RADIUS; y < yConcaveCenter+RADIUS; y++){
                //(x-a)2+(y-b)2 = r2

                if(Math.pow((x-xConcaveCenter),2) + Math.pow((y-yConcaveCenter),2) < RADIUS_POW2){
                    blockData[x][y] = 0;
                }
            }
        }



        return blockData;
    }

 

 

獲取摳完圖的原圖和被摳出來的圖

通過遍歷摳圖數據blockData來進行摳圖.原圖被標記的位置使用FLAG進行填充,而摳出來的部分重新構成一張同樣大小的圖

這里的操作是:

1.創建一個與摳圖區域大小(w*h)的圖,並將背景設為透明

2.遍歷摳圖區域,原圖被摳的地方填充其他顏色

3.摳出來的像素點復制到上面創建的透明圖

public  Map<String,BufferedImage> cutByTemplate(BufferedImage oriImage,  int[][] blockData){


        Map<String,BufferedImage> imgMap = new HashMap<>();
     //創建一個與摳圖區域大小的圖
        BufferedImage cutImage = new BufferedImage(imageMessage.cutImageWidth,imageMessage.cutImageHeight,oriImage.getType());

        // 獲取Graphics2D
        Graphics2D g2d = cutImage.createGraphics();

        //透明化整張圖
        cutImage = g2d.getDeviceConfiguration()
                .createCompatibleImage(imageMessage.cutImageWidth,imageMessage.cutImageHeight, Transparency.BITMASK);
        g2d.dispose();
        g2d = cutImage.createGraphics();
        // 背景透明代碼結束


        log.info("imageMessage = {}",imageMessage);
        int xmax = imageMessage.xpos + imageMessage.cutImageWidth;
        int ymax = imageMessage.ypos + imageMessage.cutImageHeight;
     //只對摳圖區域進行遍歷
        for(int x = imageMessage.xpos; x< xmax; x++){
            for(int y = imageMessage.ypos; y < ymax; y++){

                int oriRgb = oriImage.getRGB(x,y);

                if(blockData[x][y] == FLAG){
            //原圖 oriImage.setRGB(x,y,FLAG);             //摳的圖 g2d.setColor(color(oriRgb)); g2d.setStroke(
new BasicStroke(1f)); g2d.fillRect(x-imageMessage.xpos, y-imageMessage.ypos, 1, 1); } } } // 釋放對象 g2d.dispose(); imgMap.put(ORI_IMAGE_KEY,oriImage); imgMap.put(CUT_IMAGE_KEY,cutImage); return imgMap; }

 

圖片原始數據轉換成base64格式數據

由於圖片原始數據很多是不可打印字符,因此需要將其轉換成base64格式,再進行發送

 private  String ImageBase64(BufferedImage bufferedImage) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ImageIO.write(bufferedImage, "png", out);
        //轉成byte數組
        byte[] bytes = out.toByteArray();
        BASE64Encoder encoder = new BASE64Encoder();
        //生成BASE64編碼
        return encoder.encode(bytes);
    }

 

 

控制器

 

在進行校驗時,需要允許一定的誤差.

package slide.picture.verification.demo.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import slide.picture.verification.demo.image.ImageResult;
import slide.picture.verification.demo.image.ImgUtil;
import slide.picture.verification.demo.ret.RetCode;
import slide.picture.verification.demo.ret.WebReturn;
import slide.picture.verification.demo.time.TimeUtil;

import javax.jws.WebResult;
import java.util.concurrent.TimeUnit;


@Slf4j
@RestController
@RequestMapping("/slider")
public class SliderController {

    private  int xPosCache = 0;

    @RequestMapping("/image")
    public WebReturn image(){

        log.info("/slider/image");
        ImageResult imageResult = null;

        try{

            TimeUtil.start(1);
            imageResult = new ImgUtil().imageResult();
            TimeUtil.end(1);

            xPosCache = imageResult.getXpos();
            return new WebReturn(RetCode.IMAGE_REQ_SUCCESS,imageResult);
        }
        catch(Exception ex){
            log.error(ex.getMessage());
            ex.printStackTrace();
            return new WebReturn(RetCode.IMAGE_REQ_FAIL,null);
        }
    }


    @RequestMapping("/verification")
    public WebReturn verification(@RequestParam("moveX") int moveX){

        log.info("/slider/verification/{}",moveX);

        int MOVE_CHECK_ERROR = 2;
        if(( moveX < ( xPosCache + MOVE_CHECK_ERROR))
                && ( moveX >  (xPosCache - MOVE_CHECK_ERROR))){
            log.info("驗證正確");
            return new WebReturn(RetCode.VERIFI_REQ_SUCCESS,true);
        }
        return new WebReturn(RetCode.VERIFI_REQ_FAIL,false);
    }




}

 

 

后台的關鍵代碼就這些,整個處理流程大概耗時40ms(圖片大小320*260).上面的getBlockdata()還可以繼續優化,並不需要全局遍歷.只要摳圖區域遍歷就可以了.

由於對BufferedImage對象的操作是操作其內存中的數據,因此在大並發的情況下需要考慮內存占用狀況.

前端實現

這里需要注意的地方是摳圖和原圖的左邊緣需要對齊,以及縱坐標位置.

鼠標按下滑塊時才會開始計算偏移的距離,滑塊滑動的距離會反映到摳圖的偏移量.當松開鼠標時會將偏移量發送到后端進行校驗.

還有,后端發送過來的數據是base64數據.由於圖片原始數據很多是不可打印字符,因此需要將其轉換成

 

圖片顯示使用base64時的格式.這里的xxxx是圖片的base64數據.需要注意base64后面的逗號.

<img src="data:image/png;base64,xxxxxxxx">

html代碼

   <div id="captchaContainer">
        <!-- 標題欄 -->
        <div class="header">
            <span class="headerText">圖片滑動驗證</span>
            <span class="refreshIcon"/>
        </div> 

        <!-- 圖片顯示區域 -->
        <div id="captchaImg">
            <img id="oriImg" alt="原圖"/>
            <img id="cutImg" alt="摳圖"/>
        </div>
        <!--滑塊顯示區域-->
         <div class="sliderContainer">
            <div class="sliderMask">
                <div class="slider">
                    <span class="sliderIcon"></span>
                </div>
            </div>
            <span class="sliderText">向右滑動填充拼圖</span>
            
        </div> 
  </div>

 

 

JS代碼

<script>

    //圖片顯示使用base64時的前綴,src=base64PrefixPath + imgBase64Value
    var base64PrefixPath="data:image/png;base64,";

    var IMAGE_WIDTH = 300;
    //初始化
    //滑塊初始偏移量
    var sliderInitOffset = 0;
    //滑塊移動的最值
    var MIN_MOVE = 0;
    var MAX_MOVE = 0;
    //鼠標按下標志
    var mousedownFlag=false;
    //滑塊移動的距離
    var moveX;
    //滑塊位置檢測允許的誤差,正負2
    var MOVE_CHECK_ERROR = 2;
    //滑塊滑動使能
    var moveEnable = true;

    var ImageMsg = {
            //摳圖的坐標
            xpos: 0,
            ypos: 0,
            //摳圖的大小
            cutImageWidth: 0,
            cutImageHeight: 0,
            //原圖的base64
            oriImageSrc: 0,
            //摳圖的base64
            cutImageSrc: 0,
    }



   //加載頁面時進行初始化
    function init(){
        console.log("init")

        moveEnable = true;
        mousedownFlag=false;

        $(".slider").css("left",0+"px");

        initClass();

        MAX_MOVE = IMAGE_WIDTH - ImageMsg.cutImageWidth;

        console.log("ImageMsg = " + ImageMsg)
        $("#cutImg").css("left",0+"px");
        $("#oriImg").attr("src",ImageMsg.oriImageSrc)
        $("#cutImg").attr("src",ImageMsg.cutImageSrc)
        $("#cutImg").css("width",ImageMsg.cutImageWidth)
        $("#cutImg").css("height",ImageMsg.cutImageHeight)
        $("#cutImg").css("top",ImageMsg.ypos)




    }
    //加載頁面時
    $(function(){

        httpRequest.requestImage.request();
    })

    var httpRequest={
      //請求獲取圖片
      requestImage:{
        path: "slider/image",
        request:function(){
            $.get(httpRequest.requestImage.path,function(data,status){

                console.log(data)
                console.log(data.message);

                if(data.data != null){

                    ImageMsg.oriImageSrc = base64PrefixPath + data.data.oriImage;
                    ImageMsg.cutImageSrc = base64PrefixPath + data.data.cutImage;
                    ImageMsg.xpos = data.data.xpos;
                    ImageMsg.ypos = data.data.ypos;
                    ImageMsg.cutImageWidth = data.data.cutImageWidth;
                    ImageMsg.cutImageHeight = data.data.cutImageHeight;

                    init();
                }

          });
        },
      },
      //請求驗證
      requestVerification:{
        path: "slider/verification",
        request:function(){
            $.get(httpRequest.requestVerification.path,{moveX:(moveX)},function(data,status){
                console.log(data)
                console.log(data.code);
                console.log(data.message);

                if(data.data == true){
                    checkSuccessHandle();
                }
                else{
                    checkFailHandle();
                }
          });
        },
      },
    }

    //刷新圖片操作
    $(".refreshIcon").on("click",function(){
        httpRequest.requestImage.request();
    })

    //滑塊鼠標按下
    $(".slider").mousedown(function(event){
        console.log("鼠標按下mousedown:"+event.clientX + " " + event.clientY);
        sliderInitOffset = event.clientX;  
        mousedownFlag = true;

        

        //滑塊綁定鼠標滑動事件
        $(".slider").on("mousemove",function(event){
            if(mousedownFlag  == false){
              return;
            }
            if(moveEnable == false){
              return
            }

            moveX = event.clientX - sliderInitOffset;

            moveX<MIN_MOVE?moveX=MIN_MOVE:moveX=moveX;
            moveX>MAX_MOVE?moveX=MAX_MOVE:moveX=moveX;


            $(this).css("left",moveX+"px");
            $("#cutImg").css("left",moveX+"px");
        })

    })
    //滑塊鼠標彈起操作
    $(".slider").mouseup(function(event){
        console.log("mouseup:"+event.clientX + " " + event.clientY);
        sliderInitOffset = 0;
        $(this).off("mousemove");
        mousedownFlag=false;
        console.log("moveX = " + moveX)
        checkLocation();
        
    })
    //檢測滑塊 位置是否正確
    function checkLocation(){

        moveEnable = false;

        //后端請求檢測滑塊位置
        httpRequest.requestVerification.request();
    }

    function checkSuccessHandle(){
      $(".sliderContainer").addClass("sliderContainer_success");
      $(".slider").addClass("slider_success");
    }
    function checkFailHandle(){
      $(".sliderContainer").addClass("sliderContainer_fail");
      $(".slider").addClass("slider_success");
    }

    function initClass(){
      $(".sliderContainer").removeClass("sliderContainer_success");
      $(".slider").removeClass("slider_success");
  
      $(".sliderContainer").removeClass("sliderContainer_fail");
      $(".slider").removeClass("slider_fail");
    }
    


</script>

 

 

完整代碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM