前段时间上级要我弄一个登陆时候的图片拖动验证。相似于哔哩哔哩登陆的那种。在网上找了一波,发现有的要钱,有的是直接前台的拖动验证操做的。可是不清楚究竟是在前端验证仍是后端验证安全点。想来想去我以为得在后端,原谅我原本是后端的被赶鸭子上架写前端潜意识以为后端更安全。如今记录一下。javascript
平常不变的SSM框架。css
思路:后端图片裁剪转为base64(裁剪的X轴起点存在session,原本是想存redis)-->前端展现(获取前端拖动的距离ajax进后台与session中X轴对比)html
就在两步。前端
首先是图片裁剪类(在网上找的一位大佬的本身改了一下):java
package com.image.yanzhen; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Date; import java.util.Iterator; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import sun.misc.BASE64Encoder; public class ImageCut { /** * 源图片路径名称如:c:\1.jpg */ private String srcpath = "e:/poool.jpg"; /** * 剪切图片存放路径名称.如:c:\2.jpg */ private String subpath = "e:/pool_end"; /** * jpg图片格式 */ private static final String IMAGE_FORM_OF_JPG = "jpg"; /** * png图片格式 */ private static final String IMAGE_FORM_OF_PNG = "png"; /** * 剪切点x坐标 */ private int x; /** * 剪切点y坐标 */ private int y; /** * 剪切点宽度 */ private int width; /** * 剪切点高度 */ private int height; public ImageCut() { } public ImageCut(int x, int y, int width, int height) { this.x = x; this.y = y; this.width = width; this.height = height; } public static void main(String[] args) throws Exception { ImageCut imageCut = new ImageCut(134, 0, 366, 366); imageCut.cut(imageCut.getSrcpath(), imageCut.getSubpath()); } /** * 返回包含全部当前已注册 ImageReader 的 Iterator,这些 ImageReader 声称可以解码指定格式。 * 参数:formatName - 包含非正式格式名称 .(例如 "jpeg" 或 "tiff")等 。 * * @param postFix * 文件的后缀名 * @return */ public Iterator<ImageReader> getImageReadersByFormatName(String postFix) { switch (postFix) { case IMAGE_FORM_OF_JPG: return ImageIO.getImageReadersByFormatName(IMAGE_FORM_OF_JPG); case IMAGE_FORM_OF_PNG: return ImageIO.getImageReadersByFormatName(IMAGE_FORM_OF_PNG); default: return ImageIO.getImageReadersByFormatName(IMAGE_FORM_OF_JPG); } } /** * 对图片裁剪,并把裁剪完蛋新图片保存 。 * @param srcpath 源图片路径 * @param subpath 剪切图片存放路径 * @throws IOException */ public String cut(String srcpath, String subpath) throws IOException { FileInputStream is = null; ImageInputStream iis = null; try { // 读取图片文件 is = new FileInputStream(srcpath); // 获取文件的后缀名 String postFix = getPostfix(srcpath); System.out.println("图片格式为:" + postFix); /* * 返回包含全部当前已注册 ImageReader 的 Iterator,这些 ImageReader 声称可以解码指定格式。 * 参数:formatName - 包含非正式格式名称 .(例如 "jpeg" 或 "tiff")等 。 */ Iterator<ImageReader> it = getImageReadersByFormatName(postFix); ImageReader reader = it.next(); // 获取图片流 iis = ImageIO.createImageInputStream(is); /* * <p>iis:读取源.true:只向前搜索 </p>.将它标记为 ‘只向前搜索’。 * 此设置意味着包含在输入源中的图像将只按顺序读取,可能容许 reader 避免缓存包含与之前已经读取的图像关联的数据的那些输入部分。 */ reader.setInput(iis, true); /* * <p>描述如何对流进行解码的类<p>.用于指定如何在输入时从 Java Image I/O * 框架的上下文中的流转换一幅图像或一组图像。用于特定图像格式的插件 将从其 ImageReader 实现的 * getDefaultReadParam 方法中返回 ImageReadParam 的实例。 */ ImageReadParam param = reader.getDefaultReadParam(); /* * 图片裁剪区域。Rectangle 指定了坐标空间中的一个区域,经过 Rectangle 对象 * 的左上顶点的坐标(x,y)、宽度和高度能够定义这个区域。 */ Rectangle rect = new Rectangle(x, y, width, height); // 提供一个 BufferedImage,将其用做解码像素数据的目标。 param.setSourceRegion(rect); /* * 使用所提供的 ImageReadParam 读取经过索引 imageIndex 指定的对象,并将 它做为一个完整的 * BufferedImage 返回。 */ BufferedImage bi = reader.read(0, param); ByteArrayOutputStream CatBaos = new ByteArrayOutputStream();//io流 ImageIO.write(bi, "jpg", CatBaos);//写入流中 byte[] CutBytes = CatBaos.toByteArray();//转换成字节 BASE64Encoder encoder = new BASE64Encoder(); String CutPng_base64 = encoder.encodeBuffer(CutBytes).trim();//转换成base64串 CutPng_base64 = CutPng_base64.replaceAll("\n", "").replaceAll("\r", "");//删除 \r\n // String path = subpath + "_" + new Date().getTime() + "." + postFix; // // File file = new File(path); // File fileParent = file.getParentFile(); // if(!fileParent.exists()){ // System.out.println("自动建立文件"); // fileParent.mkdirs(); // } // file.createNewFile(); // // // 保存新图片 // ImageIO.write(bi, postFix, new File(path)); return CutPng_base64; } finally { if (is != null) is.close(); if (iis != null) iis.close(); } } /** * 获取inputFilePath的后缀名,如:"e:/test.pptx"的后缀名为:"pptx"<br> * * @param inputFilePath * @return */ public String getPostfix(String inputFilePath) { return inputFilePath.substring(inputFilePath.lastIndexOf(".") + 1); } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public String getSrcpath() { return srcpath; } public void setSrcpath(String srcpath) { this.srcpath = srcpath; } public String getSubpath() { return subpath; } public void setSubpath(String subpath) { this.subpath = subpath; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } }
再下来就是裁剪了(代码辣鸡,你们看看就好)jquery
package com.image.yanzhen; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Random; import javax.imageio.ImageIO; import com.attendance.utils.PathUtil; import sun.misc.BASE64Encoder; public class ImgCutTest { public Map<Object,Object> getBase() throws FileNotFoundException, IOException { Map<Object,Object> map = new HashMap<Object,Object>(); Random rand = new Random(); int ImgIndex = rand.nextInt(6); //生成0-6之内的随机数 if(ImgIndex==0){ ImgIndex = 1; } String path = PathUtil.getClasspath()+"baseImg/"+ImgIndex+".jpg"; System.out.println("图片路径-->"+path); File picture = new File(path); BufferedImage sourceImg = ImageIO.read(new FileInputStream(picture)); /*************************裁剪图片得到base64*************************/ int CJX = rand.nextInt(700); //生成0-700之内的随机数 int CJY = rand.nextInt(480); //生成0-480之内的随机数 System.out.println("随机x起点-->"+CJX); System.out.println("随机y起点-->"+CJY); if(CJX<200){ System.out.println("太少了"); CJX = CJX + 200; System.out.println("增长后-->"+CJX); } int CutX1 = CJX; //裁剪X轴起点 int CutY1 = CJY; //裁剪Y轴起点 int CutW1 = 200; //裁剪宽度 int CutH1 = 120; //裁剪高度 System.out.println(CutX1+"、"+CutY1+"、"+CutW1+"、"+CutH1); ImageCut imageCut1 = new ImageCut(CutX1, CutY1, CutW1, CutH1); String CutPng_base64 = imageCut1.cut(path, null); /********************************************************/ /*************************生成数组*************************/ int[][] data = new int[sourceImg.getWidth()][sourceImg.getHeight()]; for (int i=0;i<sourceImg.getWidth();i++){//1280 for(int j=0;j<sourceImg.getHeight();j++){//720 if(i<CJX+200&&i>=CJX&&j<CJY+120&&j>CJY){ data[i][j]=1; }else { data[i][j]=0; } } } /*********************************************************/ /************************图片局部变黑************************/ for (int i = 0; i < sourceImg.getWidth(); i++) { for (int j = 0; j < sourceImg.getHeight(); j++) { int rgb = data[i][j]; // 原图中对应位置变色处理 int rgb_ori = sourceImg.getRGB(i, j); if (rgb == 1) { //颜色处理 int r = (0XFF000000 & rgb_ori); int g = (0XFF000000 & (rgb_ori >> 8)); int b = (0XFF000000 & (rgb_ori >> 16)); int Gray = (r*2 + g*5 + b*1) >> 3; //原图对应位置颜色变化 sourceImg.setRGB( i, j, Gray); } } } /**********************************************************/ /************************阴影图片转base64************************/ BASE64Encoder encoder = new BASE64Encoder(); ByteArrayOutputStream YYbaos = new ByteArrayOutputStream();//io流 ImageIO.write(sourceImg, "jpg", YYbaos);//写入流中 byte[] bytes = YYbaos.toByteArray();//转换成字节 String YYPng_base64 = encoder.encodeBuffer(bytes).trim();//转换成base64串 YYPng_base64 = YYPng_base64.replaceAll("\n", "").replaceAll("\r", "");//删除 \r\n /**********************************************************/ map.put("CJX", CJX); //裁剪开始X坐标 map.put("CJY", CJY); //裁剪开始Y坐标 map.put("YYPng_base64", YYPng_base64); //阴影图片base map.put("CutPng_base64", CutPng_base64);//裁剪图片base return map; } }
最重要的就是上面ajax
这个两个了。redis
而后是controller调用图片裁剪并返回前台json
/** * 去图片验证页面 * @param session * @return * @throws IOException */ @RequestMapping(value="/GoUploadImg.do",method = RequestMethod.GET) @ResponseBody public Object IndexGoLogin(HttpSession session) throws IOException{ System.out.println("进入图片上传页面"); Map<Object,Object> map = new HashMap<Object,Object>(); map = new ImgCutTest().getBase(); String uuid = UuidUtil.get32UUID(); //前台图片展现为原图的一半 int CJX = (int) map.get("CJX")/2; session.setAttribute(uuid, CJX); map.put("uuid", uuid); return JSONArray.toJSONString(map); }
接下来就是前台的一些展现,和拖动的一些东西。后端
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <script type="text/javascript" charset="utf-8" src="statics/jquery-3.2.1.min.js"></script> <link rel="stylesheet" href="statics/layui/css/layui.css" type="text/css"/> <script type="text/javascript" src="statics/layui/layui.js"></script> <script> function getYZ(){ $.ajax({ type:"GET", url:"GoUploadImg.do", data:{}, dataType:"json", success:function(data){ console.log(data); $("#YZUUID").val(data.uuid); $("#JQX").html(data.CJX); $("#JQY").html(data.CJY); $("#JQIMG").html("<img src=\"data:image/jpeg;base64,"+data.CutPng_base64+"\"/>"); $("#YYIMG").html("<img src=\"data:image/jpeg;base64,"+data.YYPng_base64+"\"/>"); $("#YZDIVIMG").html("<img style=\"max-width:100%;max-height:100%;\" src=\"data:image/jpeg;base64,"+data.YYPng_base64+"\"/>"); $("#CJDIVIMG").html("<img style=\"max-width:100%;max-height:100%;\" src=\"data:image/jpeg;base64,"+data.CutPng_base64+"\"/>"); //移动上下位置 var MarTop = data.CJY/2+20+"px"; $("#CJDIVIMG").css('margin-top',MarTop); },error:function(data){//当访问是,404,500,等非200错误状态码 alert("亲的网络忽然出错了呢!请稍后刷新再操做!"); } }); } $(document).ready(function(){ var w = 450; var PL_Size = 100; //缺失拼图的大小 var padding = 0; //缺失拼图与边框的距离 // 滑块拖动 var moveStart = '';//定义一个鼠标按下的X轴值 //鼠标按下 $(".slider-btn").mousedown(function(e){ e = e || window.event; // 鼠标在滑块按下切换滑块背景 $(this).css({ "background-position":"0 -216px" }); moveStart = e.pageX;//记录鼠标按下时的坐标 X轴值 }); //鼠标拖动(这里使用全局监听鼠标移动的事件) onmousemove = function(e) { e = e || window.event; var moveX = e.pageX;//监听鼠标的位置 var d = moveX-moveStart; //鼠标按住后在X轴上移动的距离 if(moveStart == '') { // console.log('未拖动滑块'); } else { if(d<0 || d>(w-padding-PL_Size)) { // console.log('超过范围'); } else { var OtherD = d+20; $(".slider-btn").css({ "left":d + 'px', "transition":"inherit" }); $("#CJDIVIMG").css({ "left":OtherD + 'px', "transition":"inherit" }); } } }; //鼠标松开 (这里使用全局监听鼠标松开的事件) onmouseup = function (e) { e = e || window.event; var moveEnd_X = e.pageX - moveStart;//松开鼠标后滑块移动的距离 if(moveStart == '') { } else { var uuid = $("#YZUUID").val(); $.ajax({ type:"POST", url:"YanZhenX.do", data:{uuid:uuid,moveEnd_X:moveEnd_X}, dataType:"json", success:function(data){ console.log(data); if(data.YZ=="yes"){ $("#YZDExpress").html("<i class=\"layui-icon layui-icon-ok-circle\" style=\"font-size: 18px; color: green;\"><i style=\"font-size:15px;\">验证经过 </i></i><font style=\"font-size:15px;\">你的速度飞快,超过绝大多数人</font>"); }else{ $("#YZDExpress").html("<i class=\"layui-icon layui-icon-close-fill\" style=\"font-size: 18px; color: red;\"><i style=\"font-size:15px;\">验证失败 </i></i><font style=\"font-size:15px;\">拖动滑块将悬浮图像正确拼接</font>"); } },error:function(data){//当访问是,404,500,等非200错误状态码 alert("亲的网络忽然出错了呢!请稍后刷新再操做!"); } }); } setTimeout(function () { $(".slider-btn").css({ "left":'0', "transition":"left 0.5s" }); $("#CJDIVIMG").css({ "left":'20px', "transition":"left 0.5s" }); $("#YZDExpress").html(""); },1000); $(".slider-btn").css({ "background-position":"0 -84px" }); moveStart = '';// 清空上一次鼠标按下时的坐标X轴值; } }); </script> <style> .YZDIV{ width: 490px; height:360px; border: solid #E7E3DA thin; background-color: #F4ECE3; border-radius: 20px; text-align: center; } .YYIMG{ width: 450px; height:300px; border: none; background-color: #ECE4DD; margin:0 auto; margin-top:20px; } .CJIMG{ width: 100px; height:60px; border: #D3D664 solid thin; background-color: #ECE4DD; z-index: 200; position: absolute; left:20px; } .slider-btn { position:absolute; width:44px; height:44px; left:0; top:-7px; z-index:12; cursor:pointer; background-image:url("statics/image/sprite.3.2.0.png"); background-position:0 -84px; transition:inherit; } .layui-icon-refresh-3:HOVER { color: green; } </style> <body onload="getYZ()"> <!-- <button onclick="getYZ()">验证素材</button> --> <input type="hidden" id="YZUUID"/> <div class="YZDIV"> <div class="CJIMG" id="CJDIVIMG"></div> <div class="YYIMG" id="YZDIVIMG"></div> <i onclick="getYZ()" class="layui-icon layui-icon-refresh-3" style="font-size: 18px;float: left;line-height: 40px;margin-left: 12px;cursor: pointer;" title="刷新验证"></i> <i id="YZDExpress" style="line-height: 40px;"></i> </div> <br/> <div style="position:relative;width:490px;"> <div style="border:1px solid #c3c3c3;border-radius:24px;background:#ece4dd;box-shadow:0 1px 1px rgba(12,10,10,0.2) inset;"> <p style="-moz-user-select: none; -khtml-user-select: none; user-select: none;font-size:12px;color: #486c80;line-height:28px;margin:0;text-align:right;padding-right:22px;text-align: center;">按住左边滑块,拖动完成上方拼图</p> </div> <div class="slider-btn" id="ANNIU"></div> </div> <br/><br/><br/><br/> 截取X起点:<div id="JQX"></div><br/> 截取Y起点:<div id="JQY"></div><br/> 截取图片:<div id="JQIMG"></div><br/> 阴影图片:<div id="YYIMG"></div><br/> </body> </html>
这边拖动在鼠标松开的时候会回后台进行验证
/** * 滑动验证 * @param uuid 标识符 * @param moveEnd_X 滑动距离 * @param session * @return * @throws IOException */ @RequestMapping(value="/YanZhenX.do",method = RequestMethod.POST) @ResponseBody public Object YanZhenX(@RequestParam String uuid,@RequestParam int moveEnd_X,HttpSession session) throws IOException{ System.out.println("进行验证"); Map<Object,Object> map = new HashMap<Object,Object>(); int CJX = (int) session.getAttribute(uuid); System.out.println("uuid-->"+uuid); System.out.println("滑动x距离-->"+moveEnd_X); System.out.println("裁剪距离-->"+CJX); if(moveEnd_X>CJX-3&&moveEnd_X<CJX+3){ //误差在3之类 System.out.println("拼接成功"); map.put("YZ", "yes"); }else{ System.out.println("误差过大"); map.put("YZ", "no"); } return JSONArray.toJSONString(map); }
大体的就这么个样子。
放几张效果图
好了,就这些了。有问题请多多指点。对了那些图片我都是直接选的900*600的
源码也放一下下。你们想看的能够下载,虽然基本的代码都在上面了