本文将讲解滑动验证码由来、原理及功能实现。文章,只贴出主要的逻辑代码,相关的实现代码和资源文件可以在项目中获取。 项目地址:[https://gitee.com/gester/captcha.git](https://gitee.com/gester/captcha.git) 同时,推一下**字符运算码和运算验证码**文章。文章地址:[https://www.jianshu.com/p/fdafd4126c2e](https://www.jianshu.com/p/fdafd4126c2e) 原创不易!如果有帮到您,可以给作者一个小星星鼓励下 ^ _ ^ ### 滑动验证码产生 传统的字符验证码、运算验证码已经存在很长一段时间,可以称得上老古董了,相信每个人都见多。 易用性:在新生滑动验证码、点选验证码等面前简直弱爆了。用户还需要动手、动脑去操作,想想都烦,并且大家都懒嘛,还要照顾近视的同时,和老年用户,那岂不是有点弱。 安全性:现在已经过度到大数据时代,特别是机器学的冲击。机器通过模板训练,两天的时间都可以攻破你的传统验证码。当然滑动验证码,点选验证码也是可以破解的,相对传统验证码而言,肯定要费力些。 ### 滑动验证码原理 1. 服务器存有原始图片、抠图模板、抠图边框等图片 2. 请求获取验证码,服务器随机获取一张图片,根据抠图模板图片在原图中随机生成x, y轴的矩形感兴趣区域 3. 再通过抠图模板在感兴趣的区域图片中抠图,这里会产生一张小块的验证滑块图 4. 验证滑块图再通过抠图边框进行颜色处理,生成带有描边的新的验证滑块图 5. 原图再根据抠图模板做颜色处理,这里会产生一张遮罩图(缺少小块的目标图) 6. 到这里可以得到三张图,一张原图,一张遮罩图。将这三张图和抠图的y轴坐标通过base64加密,返回给前端,并将验证的抠图位置的x轴、y轴存放在session、db、nosql中 7. 前端在移动方块验证时,将移动后的x轴和y轴坐标传递到后台与原来的x坐标和y轴坐标作比较,如果在阈值内则验证通过,验证通过后可以是给提示或者显示原图 8. 后端可以通过token、session、redis等方式取出存放的x轴和y轴坐标数据,与用户滑动的x轴和y轴进行对比验证 ### 滑动验证码实现 ##### 功能 - 滑动验证码 - 字符验证码(扩展,参见上篇文章) - 运算验证码(扩展,参见上篇文章) ##### 依赖 - 无 ##### 实现代码 **获取验证码方法:** ``` /** * 获取滑动验证码 * @param imageVerificationDto 验证码参数 * @return 滑动验证码 * @throws ServiceException 获取滑动验证码异常 */ public ImageVerificationVo selectSlideVerificationCode(ImageVerificationDto imageVerificationDto) throws ServiceException { ImageVerificationVo imageVerificationVo = null; try { // // 原图路径,这种方式不推荐。当运行jar文件的时候,路径是找不到的,我的路径是写到配置文件中的。 // String verifyImagePath = URLDecoder.decode(this.getClass().getResource("/").getPath() + "static/targets", "UTF-8"); // 获取模板文件,。推荐文件通过流读取, 因为文件在开发中的路径和打成jar中的路径是不一致的 // InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("static/template/1.png"); File verifyImageImport = new File(verificationImagePathPrefix); File[] verifyImages = verifyImageImport.listFiles(); Random random = new Random(System.currentTimeMillis()); // 随机取得原图文件夹中一张图片 File originImageFile = verifyImages[random.nextInt(verifyImages.length)]; // 获取模板图片文件 File templateImageFile = new File(templateImagePathPrefix + "/template.png"); // 获取描边图片文件 File borderImageFile = new File(templateImagePathPrefix + "/border.png"); // 获取描边图片类型 String borderImageFileType = borderImageFile.getName().substring(borderImageFile.getName().lastIndexOf(".") + 1); // 获取原图文件类型 String originImageFileType = originImageFile.getName().substring(originImageFile.getName().lastIndexOf(".") + 1); // 获取模板图文件类型 String templateImageFileType = templateImageFile.getName().substring(templateImageFile.getName().lastIndexOf(".") + 1); // 读取原图 BufferedImage verificationImage = ImageIO.read(originImageFile); // 读取模板图 BufferedImage readTemplateImage = ImageIO.read(templateImageFile); // 读取描边图片 BufferedImage borderImage = ImageIO.read(borderImageFile); // 获取原图感兴趣区域坐标 imageVerificationVo = ImageVerificationUtil.generateCutoutCoordinates(verificationImage, readTemplateImage); int Y = imageVerificationVo.getY(); // 在分布式应用中,可将session改为redis存储 getRequest().getSession().setAttribute("imageVerificationVo", imageVerificationVo); // 根据原图生成遮罩图和切块图 imageVerificationVo = ImageVerificationUtil.pictureTemplateCutout(originImageFile, originImageFileType, templateImageFile, templateImageFileType, imageVerificationVo.getX(), imageVerificationVo.getY()); // 剪切图描边 imageVerificationVo = ImageVerificationUtil.cutoutImageEdge(imageVerificationVo, borderImage, borderImageFileType); imageVerificationVo.setY(Y); imageVerificationVo.setType(imageVerificationDto.getType()); // ============================================= // 输出图片 // HttpServletResponse response = getResponse(); // response.setContentType("image/jpeg"); // ServletOutputStream outputStream = response.getOutputStream(); // outputStream.write(oriCopyImages); // BufferedImage bufferedImage = ImageIO.read(originImageFile); // ImageIO.write(bufferedImage, originImageType, outputStream); // outputStream.flush(); // ================================================= } catch (UnsupportedEncodingException e) { log.error(e.getMessage(), e); throw new ServiceException(ServiceExceptionCode.URL_DECODER_ERROR); } catch (IOException e) { log.error(e.getMessage(), e); throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON); } return imageVerificationVo; } ``` **生成滑动验证码调用工具类:** ``` package com.selfimpr.captcha.utils; import com.selfimpr.captcha.exception.ServiceException; import com.selfimpr.captcha.exception.code.ServiceExceptionCode; import com.selfimpr.captcha.model.vo.ImageVerificationVo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Base64Utils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Random; /** * @Description: 图片验证工具 * ------------------- * @Author: YangXingfu * @Date: 2019/07/24 18:40 */ public class ImageVerificationUtil { private static final Logger log = LoggerFactory.getLogger(ImageVerificationUtil.class); // 默认图片宽度 private static final int DEFAULT_IMAGE_WIDTH = 280; // 默认图片高度 private static final int DEFAULT_IMAGE_HEIGHT = 171; // 获取request对象 protected static HttpServletRequest getRequest() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } // 获取response对象 protected static HttpServletResponse getResponse() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); } /** * 生成感兴趣区域坐标 * @param verificationImage 源图 * @param templateImage 模板图 * @return 裁剪坐标 */ public static ImageVerificationVo generateCutoutCoordinates(BufferedImage verificationImage, BufferedImage templateImage) { int X, Y; ImageVerificationVo imageVerificationVo = null; // int VERIFICATION_IMAGE_WIDTH = verificationImage.getWidth(); // 原图宽度 // int VERIFICATION_IMAGE_HEIGHT = verificationImage.getHeight(); // 原图高度 int TEMPLATE_IMAGE_WIDTH = templateImage.getWidth(); // 抠图模板宽度 int TEMPLATE_IMAGE_HEIGHT = templateImage.getHeight(); // 抠图模板高度 Random random = new Random(System.currentTimeMillis()); // 取范围内坐标数据,坐标抠图一定要落在原图中,否则会导致程序错误 X = random.nextInt(DEFAULT_IMAGE_WIDTH - TEMPLATE_IMAGE_WIDTH) % (DEFAULT_IMAGE_WIDTH - TEMPLATE_IMAGE_WIDTH - TEMPLATE_IMAGE_WIDTH + 1) + TEMPLATE_IMAGE_WIDTH; Y = random.nextInt(DEFAULT_IMAGE_HEIGHT - TEMPLATE_IMAGE_WIDTH) % (DEFAULT_IMAGE_HEIGHT - TEMPLATE_IMAGE_WIDTH - TEMPLATE_IMAGE_WIDTH + 1) + TEMPLATE_IMAGE_WIDTH; if (TEMPLATE_IMAGE_HEIGHT - DEFAULT_IMAGE_HEIGHT >= 0) { Y = random.nextInt(10); } imageVerificationVo = new ImageVerificationVo(); imageVerificationVo.setX(X); imageVerificationVo.setY(Y); return imageVerificationVo; } /** * 根据模板图裁剪图片,生成源图遮罩图和裁剪图 * @param originImageFile 源图文件 * @param originImageFileType 源图文件扩展名 * @param templateImageFile 模板图文件 * @param templateImageFileType 模板图文件扩展名 * @param X 感兴趣区域X轴 * @param Y 感兴趣区域Y轴 * @return * @throws ServiceException */ public static ImageVerificationVo pictureTemplateCutout(File originImageFile, String originImageFileType, File templateImageFile, String templateImageFileType, int X, int Y) throws ServiceException { ImageVerificationVo imageVerificationVo = null; try { // 读取模板图 BufferedImage templateImage = ImageIO.read(templateImageFile); // 读取原图 BufferedImage originImage = ImageIO.read(originImageFile); int TEMPLATE_IMAGE_WIDTH = templateImage.getWidth(); int TEMPLATE_IMAGE_HEIGHT = templateImage.getHeight(); // 切块图 根据模板图尺寸创建一张透明图片 BufferedImage cutoutImage = new BufferedImage(TEMPLATE_IMAGE_WIDTH, TEMPLATE_IMAGE_HEIGHT, templateImage.getType()); // 根据坐标获取感兴趣区域 BufferedImage interestArea = getInterestArea(X, Y, TEMPLATE_IMAGE_WIDTH, TEMPLATE_IMAGE_HEIGHT, originImageFile, originImageFileType); // 根据模板图片切图 cutoutImage = cutoutImageByTemplateImage(interestArea, templateImage, cutoutImage); // 图片绘图 int bold = 5; Graphics2D graphics2D = cutoutImage.createGraphics(); graphics2D.setBackground(Color.white); // 设置抗锯齿属性 graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics2D.setStroke(new BasicStroke(bold, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); graphics2D.drawImage(cutoutImage, 0, 0, null); graphics2D.dispose(); // 原图生成遮罩 BufferedImage shadeImage = generateShadeByTemplateImage(originImage, templateImage, X, Y); imageVerificationVo = new ImageVerificationVo(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 图片转为二进制字符串 ImageIO.write(originImage, originImageFileType, byteArrayOutputStream); byte[] originImageBytes = byteArrayOutputStream.toByteArray(); byteArrayOutputStream.flush(); byteArrayOutputStream.reset(); // 图片加密成base64字符串 String originImageString = Base64Utils.encodeToString(originImageBytes); imageVerificationVo.setOriginImage(originImageString); ImageIO.write(shadeImage, templateImageFileType, byteArrayOutputStream); // 图片转为二进制字符串 byte[] shadeImageBytes = byteArrayOutputStream.toByteArray(); byteArrayOutputStream.flush(); byteArrayOutputStream.reset(); // 图片加密成base64字符串 String shadeImageString = Base64Utils.encodeToString(shadeImageBytes); imageVerificationVo.setShadeImage(shadeImageString); ImageIO.write(cutoutImage, templateImageFileType, byteArrayOutputStream); // 图片转为二进制字符串 byte[] cutoutImageBytes = byteArrayOutputStream.toByteArray(); byteArrayOutputStream.reset(); // 图片加密成base64字符串 String cutoutImageString = Base64Utils.encodeToString(cutoutImageBytes); imageVerificationVo.setCutoutImage(cutoutImageString); } catch (IOException e) { log.error(e.getMessage(), e); throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON); } return imageVerificationVo; } /** * 根据模板图生成遮罩图 * @param originImage 源图 * @param templateImage 模板图 * @param x 感兴趣区域X轴 * @param y 感兴趣区域Y轴 * @return 遮罩图 * @throws IOException 数据转换异常 */ private static BufferedImage generateShadeByTemplateImage(BufferedImage originImage, BufferedImage templateImage, int x, int y) throws IOException { // 根据原图,创建支持alpha通道的rgb图片 // BufferedImage shadeImage = new BufferedImage(originImage.getWidth(), originImage.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); BufferedImage shadeImage = new BufferedImage(originImage.getWidth(), originImage.getHeight(), BufferedImage.TYPE_INT_ARGB); // 原图片矩阵 int[][] originImageMatrix = getMatrix(originImage); // 模板图片矩阵 int[][] templateImageMatrix = getMatrix(templateImage); // 将原图的像素拷贝到遮罩图 for (int i = 0; i < originImageMatrix.length; i++) { for (int j = 0; j < originImageMatrix[0].length; j++) { int rgb = originImage.getRGB(i, j); // 获取rgb色度 int r = (0xff & rgb); int g = (0xff & (rgb >> 8)); int b = (0xff & (rgb >> 16)); // 无透明处理 rgb = r + (g << 8) + (b << 16) + (255 << 24); shadeImage.setRGB(i, j, rgb); } } // 对遮罩图根据模板像素进行处理 for (int i = 0; i < templateImageMatrix.length; i++) { for (int j = 0; j < templateImageMatrix[0].length; j++) { int rgb = templateImage.getRGB(i, j); //对源文件备份图像(x+i,y+j)坐标点进行透明处理 if (rgb != 16777215 && rgb < 0) { int rgb_ori = shadeImage.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) + (140 << 24); // 对遮罩透明处理 shadeImage.setRGB(x + i, y + j, rgb_ori); // 设置遮罩颜色 // shadeImage.setRGB(x + i, y + j, rgb_ori); } } } return shadeImage; } /** * 根据模板图抠图 * @param interestArea 感兴趣区域图 * @param templateImage 模板图 * @param cutoutImage 裁剪图 * @return 裁剪图 */ private static BufferedImage cutoutImageByTemplateImage(BufferedImage interestArea, BufferedImage templateImage, BufferedImage cutoutImage) { // 获取兴趣区域图片矩阵 int[][] interestAreaMatrix = getMatrix(interestArea); // 获取模板图片矩阵 int[][] templateImageMatrix = getMatrix(templateImage); // 将模板图非透明像素设置到剪切图中 for (int i = 0; i < templateImageMatrix.length; i++) { for (int j = 0; j < templateImageMatrix[0].length; j++) { int rgb = templateImageMatrix[i][j]; if (rgb != 16777215 && rgb < 0) { cutoutImage.setRGB(i, j, interestArea.getRGB(i, j)); } } } return cutoutImage; } /** * 图片生成图像矩阵 * @param bufferedImage 图片源 * @return 图片矩阵 */ private static int[][] getMatrix(BufferedImage bufferedImage) { int[][] matrix = new int[bufferedImage.getWidth()][bufferedImage.getHeight()]; for (int i = 0; i < bufferedImage.getWidth(); i++) { for (int j = 0; j < bufferedImage.getHeight(); j++) { matrix[i][j] = bufferedImage.getRGB(i, j); } } return matrix; } /** * 获取感兴趣区域 * @param x 感兴趣区域X轴 * @param y 感兴趣区域Y轴 * @param TEMPLATE_IMAGE_WIDTH 模板图宽度 * @param TEMPLATE_IMAGE_HEIGHT 模板图高度 * @param originImage 源图 * @param originImageType 源图扩展名 * @return * @throws ServiceException */ private static BufferedImage getInterestArea(int x, int y, int TEMPLATE_IMAGE_WIDTH, int TEMPLATE_IMAGE_HEIGHT, File originImage, String originImageType) throws ServiceException { try { Iterator imageReaderIterator = ImageIO.getImageReadersByFormatName(originImageType); ImageReader imageReader = imageReaderIterator.next(); // 获取图片流 ImageInputStream imageInputStream = ImageIO.createImageInputStream(originImage); // 图片输入流顺序读写 imageReader.setInput(imageInputStream, true); ImageReadParam imageReadParam = imageReader.getDefaultReadParam(); // 根据坐标生成矩形 Rectangle rectangle = new Rectangle(x, y, TEMPLATE_IMAGE_WIDTH, TEMPLATE_IMAGE_HEIGHT); imageReadParam.setSourceRegion(rectangle); BufferedImage interestImage = imageReader.read(0, imageReadParam); return interestImage; } catch (IOException e) { log.error(e.getMessage(), e); throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON); } } /** * 切块图描边 * @param imageVerificationVo 图片容器 * @param borderImage 描边图 * @param borderImageFileType 描边图类型 * @return 图片容器 * @throws ServiceException 图片描边异常 */ public static ImageVerificationVo cutoutImageEdge(ImageVerificationVo imageVerificationVo, BufferedImage borderImage, String borderImageFileType) throws ServiceException{ try { String cutoutImageString = imageVerificationVo.getCutoutImage(); // 图片解密成二进制字符创 byte[] bytes = Base64Utils.decodeFromString(cutoutImageString); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); // 读取图片 BufferedImage cutoutImage = ImageIO.read(byteArrayInputStream); // 获取模板边框矩阵, 并进行颜色处理 int[][] borderImageMatrix = getMatrix(borderImage); for (int i = 0; i < borderImageMatrix.length; i++) { for (int j = 0; j < borderImageMatrix[0].length; j++) { int rgb = borderImage.getRGB(i, j); if (rgb < 0) { cutoutImage.setRGB(i, j , -7237488); } } } ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ImageIO.write(cutoutImage, borderImageFileType, byteArrayOutputStream); // 新模板图描边处理后转成二进制字符串 byte[] cutoutImageBytes = byteArrayOutputStream.toByteArray(); // 二进制字符串加密成base64字符串 String cutoutImageStr = Base64Utils.encodeToString(cutoutImageBytes); imageVerificationVo.setCutoutImage(cutoutImageStr); } catch (IOException e) { log.error(e.getMessage(), e); throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON); } return imageVerificationVo; } } ``` **滑动验证码验证方法:** ``` /** * 滑动验证码验证方法 * @param X x轴坐标 * @param Y y轴坐标 * @return 滑动验证码验证状态 * @throws ServiceException 验证滑动验证码异常 */ @Override public boolean checkVerificationResult(String X, String Y) throws ServiceException { try { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); ImageVerificationVo imageVerificationVo = (ImageVerificationVo) request.getSession().getAttribute("imageVerificationVo"); if (imageVerificationVo != null) { if ((Math.abs(Integer.parseInt(X) - imageVerificationVo.getX()) <= 5) && Y.equals(String.valueOf(imageVerificationVo.getY()))) { System.out.println("验证成功"); return true; } else { System.out.println("验证失败"); return false; } } else { return false; } } catch (Exception e) { log.error(e.getMessage(), e); throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON); } } ``` ##### 预览图 ![在这里插入图片描述](https://upload-images.jianshu.io/upload_images/15544502-82b129ffaf1b3c30?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ##### 后话 以上部分为主要业务逻辑代码,你需要创建一个类和简单的调试一下就能正常运行使用。相关的图片资源文件和模板文件参见项目地址:[https://gitee.com/gester/captcha.git](https://gitee.com/gester/captcha.git) 同时,推荐一波字符验证码和运算验证码文章。文章地址:[https://www.jianshu.com/p/fdafd4126c2e](https://www.jianshu.com/p/fdafd4126c2e) 如果这篇文章有帮助到您,请给一个star,谢谢大大。