基于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, HEIGHT, 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, 0, 0, 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, imageTemplate, 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, "png", os);
        //从流中获取数据数组
        return os.toByteArray();
    }


    /**
     * 根据模板图片抠图
     * @param oriImage 原始图片
     * @param templateImage 模板图片
     * @return 扣了图片之后的原始图片
     */
    private static BufferedImage dealCutPictureByTemplate(BufferedImage oriImage, BufferedImage templateImage,
                                                          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, j, 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>&nbsp;换一组</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:image/png;base64,"+data.data.newImage);
            }else{
                layer.open({
                    icon:2,
                    title: "温馨提示",
                    content: data.info
                });
            }
        },
        error : function() {}
    });
}

function exchange(){
    initImageValidate();
}

// 校验
function checkImageValidate(){
    $.ajax({
        async : false,
        type : "POST",
        url : "/common/checkImgValidate",
        dataType: "json",
        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;
}

资源包下载

链接:https://pan.baidu.com/s/1Qh-mLs_HjvZL2ly5SBIeVA  密码:r3h7

最后,大家有什么不懂的或者其他需要交流的内容,也可以进入我的QQ讨论群一起讨论:654331206

 

  • 14
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
以下是一个简单的 Java 图片滑动验证码的示例代码,使用 Java Swing 库实现: ```java import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.Random; public class ImageSliderVerificationCode extends JFrame { private static final long serialVersionUID = 1L; private final int WIDTH = 400; private final int HEIGHT = 300; private final int BLOCK_SIZE = 50; private Image bgImage; private Image blockImage; private int blockX; private int blockY; private int mouseX; private int mouseY; private boolean isDragging; public ImageSliderVerificationCode() { setTitle("Image Slider Verification Code"); setSize(WIDTH, HEIGHT); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); setLocationRelativeTo(null); bgImage = new ImageIcon("background.jpg").getImage(); blockImage = new ImageIcon("block.png").getImage(); addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { if (e.getX() >= blockX && e.getX() <= blockX + BLOCK_SIZE && e.getY() >= blockY && e.getY() <= blockY + BLOCK_SIZE) { isDragging = true; mouseX = e.getX(); mouseY = e.getY(); } } public void mouseReleased(MouseEvent e) { isDragging = false; if (blockX >= WIDTH - BLOCK_SIZE) { JOptionPane.showMessageDialog(null, "Verification passed!"); System.exit(0); } else { blockX = 0; blockY = new Random().nextInt(HEIGHT - BLOCK_SIZE); repaint(); } } }); addMouseMotionListener(new MouseMotionAdapter() { public void mouseDragged(MouseEvent e) { if (isDragging) { blockX += e.getX() - mouseX; blockY += e.getY() - mouseY; if (blockX < 0) { blockX = 0; } if (blockX > WIDTH - BLOCK_SIZE) { blockX = WIDTH - BLOCK_SIZE; } if (blockY < 0) { blockY = 0; } if (blockY > HEIGHT - BLOCK_SIZE) { blockY = HEIGHT - BLOCK_SIZE; } mouseX = e.getX(); mouseY = e.getY(); repaint(); } } }); } public void paint(Graphics g) { g.drawImage(bgImage, 0, 0, null); g.drawImage(blockImage, blockX, blockY, null); } public static void main(String[] args) { ImageSliderVerificationCode verCode = new ImageSliderVerificationCode(); verCode.setVisible(true); } } ``` 此代码将生成一个包含背景图片和滑块图片的窗口。当用户按住滑块图片并拖动它时,窗口将显示移动后的滑块图片。如果用户将滑块图片拖动到窗口的右边缘,则显示验证通过的消息并退出程序。否则,滑块图片将被重置到窗口的左侧,并随机在窗口的垂直方向上移动。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值