Java滑动验证码的原理与实现

本文将讲解滑动验证码由来、原理及功能实现。文章,只贴出主要的逻辑代码,相关的实现代码和资源文件可以在项目中获取。
项目地址:https://gitee.com/gester/captcha.git
同时,推一下字符运算码和运算验证码文章。文章地址:https://blog.csdn.net/YTenderness/article/details/99946144

原创不易!如果有帮到您,可以给作者一个小星星鼓励下 ^ _ ^

滑动验证码产生

传统的字符验证码、运算验证码已经存在很长一段时间,可以称得上老古董了,相信每个人都见多。

易用性:在新生滑动验证码、点选验证码等面前简直弱爆了。用户还需要动手、动脑去操作,想想都烦,并且大家都懒嘛,还要照顾近视的同时,和老年用户,那岂不是有点弱。

安全性:现在已经过度到大数据时代,特别是机器学的冲击。机器通过模板训练,两天的时间都可以攻破你的传统验证码。当然滑动验证码,点选验证码也是可以破解的,相对传统验证码而言,肯定要费力些。

滑动验证码原理

  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<ImageReader> 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);
        }
    }
预览图

Java滑动验证码的原理与实现

后话

以上部分为主要业务逻辑代码,你需要创建一个类和简单的调试一下就能正常运行使用。相关的图片资源文件和模板文件参见项目地址:https://gitee.com/gester/captcha.git

同时,推荐一波字符验证码和运算验证码文章。文章地址:https://blog.csdn.net/YTenderness/article/details/99946144

如果这篇文章有帮助到您,请给一个star,谢谢大大。

上一篇:图片像素的替换


下一篇:java – 如何用#000000格式更改BufferedImage的颜色?