仿12306的图片验证码
由于要做一个新项目,所以打算做一个简单的图片验证码。
先说说思路吧:在服务端,从一个文件夹里面找出8张图片,再把8张图片合并成一张大图,在8个小图里面随机生成一个要用户验证的图片分类,如小狗、啤酒等。在前端,访问这个页面时,把图片加载上去,用户在图片上选择提示所需要的图片,当用户点登陆时,根据用户选择的所有坐标判断所选的图片是不是实际上的验证图片。
先放两张效果图:
为了让文件查找比较简单,在图片文件结构上可以这样:
这样方便生成用户要选择的Key图片,和取出8张小图合并成大图。
上代码:这是选择8张图片,并且在每张图片选取时用递归保证选取的图片不会重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //选取8个图片 public static List<Object> getEightImages() { //保存取到的每一个图片的path,保证图片不会重复 List<String> paths = new ArrayList<String>(); File[] finalImages = new File[ 8 ]; List<Object> object = new ArrayList<Object>(); //保存tips String[] tips = new String[ 8 ]; for ( int i = 0 ; i < 8 ; i++) { //获取随机的二级目录 int dirIndex = getRandom(secondaryDirNumbers); File secondaryDir = getFiles()[dirIndex]; //随机到的文件夹名称保存到tips中 tips[i] = secondaryDir.getName(); //获取二级图片目录下的文件 File[] images = secondaryDir.listFiles(); int imageIndex = getRandom(imageRandomIndex); File image = images[imageIndex]; //图片去重 image = dropSameImage(image, paths, tips, i); paths.add(image.getPath()); finalImages[i] = image; } object.add(finalImages); object.add(tips); return object; } |
在生成这8张图片中,用一个数组保存所有的文件分类。在这个分类里面可以用随机数选取一个分类做为Key分类,就是用户要选择的所有图片。由于数组是有序的,可以遍历数组中的元素,获取每个key分类图片的位置,方便在用户验证时,进行匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //获取位置,返回的是第几个图片,而不是下标,从1开始,集合第一个元素为tip public static List<Object> getLocation(String[] tips) { List<Object> locations = new ArrayList<Object>(); //获取Key分类 String tip = getTip(tips); locations.add(tip); int length = tips.length; for ( int i = 0 ; i < length; i++) { if (tip.equals(tips[i])) { locations.add(i+ 1 ); } } return locations; } |
选取了8张图片后,接下来就是合并图片。合并图片可以用到BufferedImage这个类的方法:setRGB()这个方法如果不明白可以看下api文档,上面有详细的说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public static void mergeImage(File[] finalImages, HttpServletResponse response) throws IOException { //读取图片 BufferedImage mergeImage = new BufferedImage( 800 , 400 , BufferedImage.TYPE_INT_BGR); for ( int i = 0 ; i < 8 ; i++) { File image = finalImages[i]; BufferedImage bufferedImage = ImageIO.read(image); int width = bufferedImage.getWidth(); int height = bufferedImage.getHeight(); //从图片中读取RGB int [] imageBytes = new int [width*height]; imageBytes = bufferedImage.getRGB( 0 , 0 , width, height, imageBytes, 0 , width); if ( i < 4 ) { mergeImage.setRGB(i* 200 , 0 , width, height, imageBytes, 0 , width); } else { mergeImage.setRGB((i - 4 )* 200 , 200 , width, height, imageBytes, 0 , width); } } ImageIO.write(mergeImage, "jpg" , response.getOutputStream()); //ImageIO.write(mergeImage, "jpg", destImage); } |
在controller层中,先把key分类保存到session中,为用户选择图片分类做提示和图片验证做判断。然后把图片流输出到response中,就可以生成验证图片了。
response.setContentType("image/jpeg"); response.setHeader("Pragma", "No-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); List<Object> object = ImageSelectedHelper.getEightImages(); File[] finalImages = (File[]) object.get(0); String[] tips = (String[]) object.get(1); //所有key的图片位置,即用户必须要选的图片 List<Object> locations = ImageSelectedHelper.getLocation(tips); String tip = locations.get(0).toString(); System.out.println(tip); session.setAttribute("tip", tip); locations.remove(0); int length = locations.size(); for (int i = 0; i < length; i++) { System.out.println("实际Key图片位置:" + locations.get(i)); }
session.setAttribute("locations", locations); ImageMerge.mergeImage(finalImages, response);
在jsp中,为用户的点击生成小图片标记。当用户点图片击时,在父div上添加一个子div标签,并且把他定位为relative, 并且设置zIndex,然后再这个div上添加一个img标签,定位为absolute。在用户的点击时,可以获取点击事件,根据点击事件获取点击坐标,然后减去父div的坐标,就可以获取相对坐标。可以根据自己的喜好定坐标原点,这里的坐标原点是第8个图片的右下角。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <div><br> <div id= "base" ><br> <img src= "<%=request.getContextPath()%>/identify" style= "width: 300px; height: 150px;" onclick= "clickImg(event)" id= "bigPicture" ><br> </div><br> <br> </div><br><br> function clickImg(e) { var baseDiv = document.getElementById( "base" ); var topValue = 0; var leftValue = 0; var obj = baseDiv; while (obj) { leftValue += obj.offsetLeft; topValue +=obj.offsetTop; obj = obj.offsetParent; } //解决firefox获取不到点击事件的问题 var clickEvent = e ? e : (window.event ? window.event : null ); var clickLeft = clickEvent.clientX + document.body.scrollLeft - document.body.clientLeft - 10; var clickTop = clickEvent.clientY + document.body.scrollTop - document.body.clientTop - 10; var divId = "img_" + index++; var divEle = document.createElement( "div" ); divEle.setAttribute( "id" , divId); divEle.style.position = "relative" ; divEle.style.zIndex = index; divEle.style.width = "20px" ; divEle.style.height = "20px" ; divEle.style.display = "inline" ; divEle.style.top = clickTop - topValue - 150 + 10 + "px" ; divEle.style.left = clickLeft - leftValue - 300 + "px" ; divEle.setAttribute( "onclick" , "remove('" + divId + "')" ); baseDiv.appendChild(divEle); var imgEle = document.createElement( "img" ); imgEle.src = "<%=request.getContextPath()%>/resources/timo.png" ; imgEle.style.width = "20px" ; imgEle.style.height = "20px" ; imgEle.style.top = "0px" ; imgEle.style.left = "0px" ; imgEle.style.position = "absolute" ; imgEle.style.zIndex = index; divEle.appendChild(imgEle); } |
用户选择登录后,服务器端根据用户的选择坐标进行判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | public List<Integer> isPass(String result) { String[] xyLocations = result.split( "," ); //保存用户选择的坐标落在哪些图片上 List<Integer> list = new ArrayList<Integer>(); //每一组坐标 System.out.println( "用户选择图片数:" +xyLocations.length); for (String xyLocation : xyLocations) { String[] xy = xyLocation.split( "\\|\\|" ); int x = Integer.parseInt(xy[ 0 ]); int y = Integer.parseInt(xy[ 1 ]); //8,4图片区间 if ( x > - 75 && x <= 0 ) { if ( y > - 75 && y <= 0 ) { //8号 list.add( 8 ); } else if ( y >= - 150 && y <= - 75 ) { //4号 list.add( 4 ); } } else if ( x > - 150 && x <= - 75 ) { //7,3图片区间 if ( y > - 75 && y <= 0 ) { //7号 list.add( 7 ); } else if ( y >= - 150 && y <= - 75 ) { //3号 list.add( 3 ); } } else if ( x > - 225 && x <= - 150 ) { //6,2图片区间 if ( y > - 75 && y <= 0 ) { //6号 list.add( 6 ); } else if ( y >= - 150 && y <= - 75 ) { //2号 list.add( 2 ); } } else if ( x >= - 300 && x <= - 225 ) { //5,1图片区间 if ( y > - 75 && y <= 0 ) { //5号 list.add( 5 ); } else if ( y >= - 150 && y <= - 75 ) { //1号 list.add( 1 ); } } else { return null ; } } return list; } |
刷新生成新的图片,由于ajax不支持二进制流,可以自己用原生的xmlHttpRequest对象加html5的blob来完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function refresh() { var url = "<%=request.getContextPath()%>/identify" ; var xhr = new XMLHttpRequest(); xhr.open( 'GET' , url, true ); xhr.responseType = "blob" ; xhr.onload = function () { if ( this .status == 200) { var blob = this .response; //加载成功后释放blob bigPicture.onload = function (e) { window.URL.revokeObjectURL(bigPicture.src); }; bigPicture.src = window.URL.createObjectURL(blob); } } xhr.send(); |
验证码整体代码完成了,还有有一些细节要处理。由于图片容易被百度识图,要对生成的图片做模糊处理,暂时还没想到什么好的办法~
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验