前言
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图片滑动验证
更多关于验证码的文章请点击查看:《java验证码》
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持呐喊教程。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#nhooo.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。