zoukankan      html  css  js  c++  java
  • 完整说明使用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>

    完整代码

  • 相关阅读:
    js中定时器2
    js中定时器之一
    js中的Event对象
    hdu 1041(递推,大数)
    hdu 1130,hdu 1131(卡特兰数,大数)
    hdu 2044-2050 递推专题
    hdu 3078(LCA的在线算法)
    hdu 1806(线段树区间合并)
    hdu 3308(线段树区间合并)
    poj 2452(RMQ+二分查找)
  • 原文地址:https://www.cnblogs.com/lgjlife/p/12604121.html
Copyright © 2011-2022 走看看