java实现图片滑动验证(包含前端代码)
作者:MrSpirit 发布时间:2022-03-21 12:13:52
前言
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> 换一组</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;
}
资源包下载:Java图片滑动验证
来源:https://blog.csdn.net/MrSpirit/article/details/100653864


猜你喜欢
- 一般数据库的编码是utf8,utf8是不支持存储表情符的,当存入的微信昵称带有表情符时就会出现乱码情况,有两种解决方法:1.mysql数据库
- 前言今天看代码看到有牵扯到弱引用的东西,就先稍微补一补Java的四种引用类型吧。Java为引用类型专门定义了一个类Reference,它是引
- Android 原生的按钮点击状态是有变化的,但是如果是自己加了一个.png格式的图片为背景色,按钮点击就不会有任何效果,为了达到点击按钮有
- feignclient https接口调用报证书错误问题最近在使用 feignclient 过程中,和第三方通过https 协议交互的时候,
- 单独一个变量直接使用 @a 的形式,无需加分号,一般是直接使用已有变量,注意在使用 html 标签时
- 最近,在使用spring cloud框架时,发现feign也能实现三方请求,而且实现很简单,请求接口的结构很清晰,便果断学习一波。记录一下。
- 反编译jar包并修改class文件重新打包这两天碰到一个需求:需要修改一个jar包中的逻辑代码,并且重新打包本来是很简单的问题,但是因为这个
- 破解流程破解Android程序流程:反编译—>分析–>修改–>回编译–>签名,这些都是在命令行中操作,当然也有集成了
- 在这里就分享两条开发中曾经忽略的问题:1、Union(联合体)的字节对齐先看代码:#pragma pack(4)struct com{&nb
- 什么是指纹解锁技术根据人的指纹来验证是否能够解锁的技术,类似于通过输入密码来解锁,都是通过一定的数字特征来解锁。指纹解锁技术原理理解首先说明
- main方法调用spring的service将业务层类配置到Spring中:<bean id="customerServic
- 动态方法就是一个Action对应多个请求,减少Action的数量1、指定method属性<action name="addA
- 使用IDEA开发微服务项目,需要启动多个微服务,可以开启IDEA的Run DashBoard窗口,需要对IDEA中指定工程的父工程进行配置进
- C#事件使用+= -=使用起来是很方便的,但是却不能整体清空所有事件。比如一个常见的操作,打开界面注册监听事件,关闭界面需要把所有的事件清空
- 概述本文的编写初衷,是想了解一下Spring Boot2中,具体是怎么序列化和反序列化JSR 310日期时间体系的,Spring MVC应用
- 目录使用格式化编辑手机号格式化编辑身份证号设置监听移除格式化的文本实现原理项目地址格式化编辑的需求一般是从编辑手机号开始的,UI 给出的效果
- 在Servlet2.5中,我们要实现文件上传功能时,一般都需要借助第三方开源组件,例如Apache的commons-fileupload组件
- Double转化为String时的保留位数及格式有时需要将程序中的数据写入到文件中进行保存,这时候就涉及到数据的字符串格式问题。下面介绍Do
- 一、读线圈状态/// <summary> /// 读线圈状态测试 &nbs
- Java 利用poi把数据库中数据导入Excel效果:使用时先把poi包导入工程的path,注意只需要导入poi包即可,下载后有三个jar包