JAVA 完整实现滑块拼图验证码

  1. 后端随机生成抠图和带有抠图阴影的背景图片,后台保存随机抠图位置坐标
  2. 前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值,比如以下示例
  1. 前端将用户滑动距离值传入后端,后端校验误差是否在容许范围内。

        这里单纯校验用户滑动距离是最基本的校验,出于更高的安全考虑,可能还会考虑用户滑动的整个轨迹,用户在当前页面的访问行为等。这些可以很复杂,甚至借助到用户行为数据分析模型,最终的目标都是增加非法的模拟和绕过的难度。这些有机会可以再归纳总结常用到的方法,本文重点集中在如何基于Java来一步步实现滑动验证码的生成。

    可以看到,滑动图形验证码,重要有两个图片组成,抠块和带有抠块阴影的原图,这里面有两个重要特性保证被暴力破解的难度:抠块的形状随机和抠块所在原图的位置随机。这样就可以在有限的图集中制造出随机的、无规律可寻的抠图和原图的配对。

    用代码如何从一张大图中抠出一个有特定随机形状的小图呢?

    第一步,先确定一个抠出图的轮廓,方便后续真正开始执行图片处理操作

    图片是有像素组成,每个像素点对应一种颜色,颜色可以用RGB形式表示,外加一个透明度,把一张图理解成一个平面图形,左上角为原点,向右x轴,向下y轴,一个坐标值对应该位置像素点的颜色,这样就可以把一张图转换成一个二维数组。基于这个考虑,轮廓也用二维数组来表示,轮廓内元素值为1,轮廓外元素值对应0。

    这时候就要想这个轮廓形状怎么生成了。有坐标系、有矩形、有圆形,没错,用到数学的图形函数。典型用到一个圆的函数方程和矩形的边线的函数,类似:

(x-a)²+(y-b)²=r²中,有三个参数a、b、r,即圆心坐标为(a,b),半径r。这些将抠图放在上文描述的坐标系上很容易就图算出来具体的值。

示例代码如下:

    static int targetWidth = 55;//小图长static int targetHeight = 45;//小图宽static int circleR = 8;//半径static int r1 = 4;//距离点/*** @Createdate: 2019年1月24日上午10:52:42* @Title: getBlockData* @Description: 生成小图轮廓* @author zhoujin* @return int[][]* @throws*/private static int[][] getBlockData() {int[][] data = new int[targetWidth][targetHeight];double x2 = targetWidth -circleR; //47//随机生成圆的位置double h1 = circleR + Math.random() * (targetWidth-3*circleR-r1);double po = Math.pow(circleR,2); //64double xbegin = targetWidth - circleR - r1;double ybegin = targetHeight- circleR - r1;//圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆//计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色for (int i = 0; i < targetWidth; i++) {for (int j = 0; j < targetHeight; j++) {double d2 = Math.pow(j - 2,2) + Math.pow(i - h1,2);double d3 = Math.pow(i - x2,2) + Math.pow(j - h1,2);if ((j <= ybegin && d2 < po)||(i >= xbegin && d3 > po)) {data[i][j] = 0;}  else {data[i][j] = 1;}}}return data;}

 第二步,有这个轮廓后就可以依据这个二维数组的值来判定抠图并在原图上抠图位置处加阴影。

    操作如下:

/**** @Createdate: 2019年1月24日上午10:51:30* @Title: cutByTemplate* @Description: 有这个轮廓后就可以依据这个二维数组的值来判定抠图并在原图上抠图位置处加阴影,* @author zhoujin* @param oriImage  原图* @param targetImage  抠图拼图* @param templateImage 颜色* @param x* @param y void* @throws*/private static void cutByTemplate(BufferedImage oriImage, BufferedImage targetImage, int[][] templateImage, int x, int y){int[][] martrix = new int[3][3];int[] values = new int[9];//创建shape区域for (int i = 0; i < targetWidth; i++) {for (int j = 0; j < targetHeight; j++) {int rgb = templateImage[i][j];// 原图中对应位置变色处理int rgb_ori = oriImage.getRGB(x + i, y + j);if (rgb == 1) {targetImage.setRGB(i, j, rgb_ori);//抠图区域高斯模糊readPixel(oriImage, x + i, y + j, values);fillMatrix(martrix, values);oriImage.setRGB(x + i, y + j, avgMatrix(martrix));}else{//这里把背景设为透明targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);}}}}private static void readPixel(BufferedImage img, int x, int y, int[] pixels) {int xStart = x - 1;int yStart = y - 1;int current = 0;for (int i = xStart; i < 3 + xStart; i++)for (int j = yStart; j < 3 + yStart; j++) {int tx = i;if (tx < 0) {tx = -tx;} else if (tx >= img.getWidth()) {tx = x;}int ty = j;if (ty < 0) {ty = -ty;} else if (ty >= img.getHeight()) {ty = y;}pixels[current++] = img.getRGB(tx, ty);}}private static void fillMatrix(int[][] matrix, int[] values) {int filled = 0;for (int i = 0; i < matrix.length; i++) {int[] x = matrix[i];for (int j = 0; j < x.length; j++) {x[j] = values[filled++];}}}private static int avgMatrix(int[][] matrix) {int r = 0;int g = 0;int b = 0;for (int i = 0; i < matrix.length; i++) {int[] x = matrix[i];for (int j = 0; j < x.length; j++) {if (j == 1) {continue;}Color c = new Color(x[j]);r += c.getRed();g += c.getGreen();b += c.getBlue();}}return new Color(r / 8, g / 8, b / 8).getRGB();}

经过前面两步后,就得到了抠图和带高斯模糊抠图阴影的原图。返回生成的抠图和带阴影的大图base64码及抠图坐标。

/*** @Description: 读取本地图片,生成拼图验证码* @author zhoujin* @return Map<String,Object>  返回生成的抠图和带抠图阴影的大图 base64码及抠图坐标*/public static Map<String,Object> createImage(File file, Map<String,Object> resultMap){try {BufferedImage oriImage = ImageIO.read(file);Random random = new Random();//X轴距离右端targetWidth  Y轴距离底部targetHeight以上int widthRandom = random.nextInt(oriImage.getWidth()-  2*targetWidth) + targetWidth;int heightRandom = random.nextInt(oriImage.getHeight()- targetHeight);logger.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})",oriImage.getWidth(),oriImage.getHeight(),widthRandom,heightRandom);BufferedImage targetImage= new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_4BYTE_ABGR);cutByTemplate(oriImage,targetImage,getBlockData(),widthRandom,heightRandom);resultMap.put("bigImage", getImageBASE64(oriImage));//大图resultMap.put("smallImage", getImageBASE64(targetImage));//小图resultMap.put("xWidth",widthRandom);resultMap.put("yHeight",heightRandom);} catch (Exception e) {logger.info("创建图形验证码异常",e);} finally{return resultMap;}}/*** @Description: 读取网络图片,生成拼图验证码* @author zhoujin* @return Map<String,Object>  返回生成的抠图和带抠图阴影的大图 base64码及抠图坐标*/public static Map<String,Object> createImage(String imgUrl, Map<String,Object> resultMap){try {//通过URL 读取图片URL url = new URL(imgUrl);BufferedImage bufferedImage = ImageIO.read(url.openStream());Random rand = new Random();int widthRandom = rand.nextInt(bufferedImage.getWidth()-  targetWidth - 100 + 1 ) + 100;int heightRandom = rand.nextInt(bufferedImage.getHeight()- targetHeight + 1 );logger.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})",bufferedImage.getWidth(),bufferedImage.getHeight(),widthRandom,heightRandom);BufferedImage target= new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_4BYTE_ABGR);cutByTemplate(bufferedImage,target,getBlockData(),widthRandom,heightRandom);resultMap.put("bigImage", getImageBASE64(bufferedImage));//大图resultMap.put("smallImage", getImageBASE64(target));//小图resultMap.put("xWidth",widthRandom);resultMap.put("yHeight",heightRandom);} catch (Exception e) {logger.info("创建图形验证码异常",e);} finally{return resultMap;}}/*** @Title: getImageBASE64* @Description: 图片转BASE64* @author zhoujin* @param image* @return* @throws IOException String*/public static String getImageBASE64(BufferedImage image) throws IOException {byte[] imagedata = null;ByteArrayOutputStream bao=new ByteArrayOutputStream();ImageIO.write(image,"png",bao);imagedata=bao.toByteArray();BASE64Encoder encoder = new BASE64Encoder();String BASE64IMAGE=encoder.encodeBuffer(imagedata).trim();BASE64IMAGE = BASE64IMAGE.replaceAll("\r|\n", "");  //删除 \r\nreturn BASE64IMAGE;}

控制层代码实现及校验验证码:

  /*** @param @return 参数说明* @return BaseRestResult 返回类型* @Description: 生成滑块拼图验证码*/@RequestMapping(value = "/getImageVerifyCode.do", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})public BaseRestResult getImageVerifyCode() {Map<String, Object> resultMap = new HashMap<>();//读取本地路径下的图片,随机选一条File file = new File(this.getClass().getResource("/image").getPath());File[] files = file.listFiles();int n = new Random().nextInt(files.length);File imageUrl = files[n];ImageUtil.createImage(imageUrl, resultMap);//读取网络图片//ImageUtil.createImage("http://hbimg.b0.upaiyun.com/7986d66f29bfeb6015aaaec33d33fcd1d875ca16316f-2bMHNG_fw658",resultMap);session.setAttribute("xWidth", resultMap.get("xWidth"));resultMap.remove("xWidth");resultMap.put("errcode", 0);resultMap.put("errmsg", "success");return new BaseRestResult(resultMap);}/*** 校验滑块拼图验证码** @param moveLength 移动距离* @return BaseRestResult 返回类型* @Description: 生成图形验证码*/@RequestMapping(value = "/verifyImageCode.do", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})public BaseRestResult verifyImageCode(@RequestParam(value = "moveLength") String moveLength) {Double dMoveLength = Double.valueOf(moveLength);Map<String, Object> resultMap = new HashMap<>();try {Integer xWidth = (Integer) session.getAttribute("xWidth");if (xWidth == null) {resultMap.put("errcode", 1);resultMap.put("errmsg", "验证过期,请重试");return new BaseRestResult(resultMap);}if (Math.abs(xWidth - dMoveLength) > 10) {resultMap.put("errcode", 1);resultMap.put("errmsg", "验证不通过");} else {resultMap.put("errcode", 0);resultMap.put("errmsg", "验证通过");}} catch (Exception e) {throw new EsServiceException(e.getMessage());} finally {session.removeAttribute("xWidth");}return new BaseRestResult(resultMap);}

前端显示图片代码:

<img src="https://img-blog.csdnimg.cn/2022010700031595841.png" alt="抠图">
<img src="https://img-blog.csdnimg.cn/2022010700031595841.png" alt="带抠图阴影的原图">

至此后台java实现滑块验证码关键代码完成! 

本文链接:https://my.lmcjl.com/post/2377.html

展开阅读全文

4 评论

留下您的评论.