Java滑動驗證碼的原理與實現(xiàn)

本文將講解滑動驗證碼由來、原理及功能實現(xiàn)。文章,只貼出主要的邏輯代碼,相關的實現(xiàn)代碼和資源文件可以在項目中獲取。
項目地址:https://gitee.com/gester/captcha.git
同時,推一下字符運算碼和運算驗證碼文章。文章地址:http://www.lxweimin.com/p/fdafd4126c2e

原創(chuàng)不易!如果有幫到您,可以給作者一個小星星鼓勵下 ^ _ ^

滑動驗證碼產生

傳統(tǒng)的字符驗證碼、運算驗證碼已經存在很長一段時間,可以稱得上老古董了,相信每個人都見多。

易用性:在新生滑動驗證碼、點選驗證碼等面前簡直弱爆了。用戶還需要動手、動腦去操作,想想都煩,并且大家都懶嘛,還要照顧近視的同時,和老年用戶,那豈不是有點弱。

安全性:現(xiàn)在已經過度到大數(shù)據(jù)時代,特別是機器學的沖擊。機器通過模板訓練,兩天的時間都可以攻破你的傳統(tǒng)驗證碼。當然滑動驗證碼,點選驗證碼也是可以破解的,相對傳統(tǒng)驗證碼而言,肯定要費力些。

滑動驗證碼原理

  1. 服務器存有原始圖片、摳圖模板、摳圖邊框等圖片
  2. 請求獲取驗證碼,服務器隨機獲取一張圖片,根據(jù)摳圖模板圖片在原圖中隨機生成x, y軸的矩形感興趣區(qū)域
  3. 再通過摳圖模板在感興趣的區(qū)域圖片中摳圖,這里會產生一張小塊的驗證滑塊圖
  4. 驗證滑塊圖再通過摳圖邊框進行顏色處理,生成帶有描邊的新的驗證滑塊圖
  5. 原圖再根據(jù)摳圖模板做顏色處理,這里會產生一張遮罩圖(缺少小塊的目標圖)
  6. 到這里可以得到三張圖,一張原圖,一張遮罩圖。將這三張圖和摳圖的y軸坐標通過base64加密,返回給前端,并將驗證的摳圖位置的x軸、y軸存放在session、db、nosql中
  7. 前端在移動方塊驗證時,將移動后的x軸和y軸坐標傳遞到后臺與原來的x坐標和y軸坐標作比較,如果在閾值內則驗證通過,驗證通過后可以是給提示或者顯示原圖
  8. 后端可以通過token、session、redis等方式取出存放的x軸和y軸坐標數(shù)據(jù),與用戶滑動的x軸和y軸進行對比驗證

滑動驗證碼實現(xiàn)

功能
  • 滑動驗證碼
  • 字符驗證碼(擴展,參見上篇文章)
  • 運算驗證碼(擴展,參見上篇文章)
依賴
實現(xiàn)代碼

獲取驗證碼方法:

 /**
     * 獲取滑動驗證碼
     * @param imageVerificationDto 驗證碼參數(shù)
     * @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");

//            獲取模板文件,。推薦文件通過流讀取, 因為文件在開發(fā)中的路徑和打成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);


            //  獲取原圖感興趣區(qū)域坐標
            imageVerificationVo = ImageVerificationUtil.generateCutoutCoordinates(verificationImage, readTemplateImage);

            int Y  = imageVerificationVo.getY();
                    //  在分布式應用中,可將session改為redis存儲
            getRequest().getSession().setAttribute("imageVerificationVo", imageVerificationVo);

            //  根據(jù)原圖生成遮罩圖和切塊圖
            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();
    }


    /**
     * 生成感興趣區(qū)域坐標
     * @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());

        //  取范圍內坐標數(shù)據(jù),坐標摳圖一定要落在原圖中,否則會導致程序錯誤
        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;
    }

    /**
     * 根據(jù)模板圖裁剪圖片,生成源圖遮罩圖和裁剪圖
     * @param originImageFile 源圖文件
     * @param originImageFileType 源圖文件擴展名
     * @param templateImageFile 模板圖文件
     * @param templateImageFileType 模板圖文件擴展名
     * @param X 感興趣區(qū)域X軸
     * @param Y 感興趣區(qū)域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();

            //  切塊圖   根據(jù)模板圖尺寸創(chuàng)建一張透明圖片
            BufferedImage cutoutImage = new BufferedImage(TEMPLATE_IMAGE_WIDTH, TEMPLATE_IMAGE_HEIGHT, templateImage.getType());

            //  根據(jù)坐標獲取感興趣區(qū)域
            BufferedImage interestArea = getInterestArea(X, Y, TEMPLATE_IMAGE_WIDTH, TEMPLATE_IMAGE_HEIGHT, originImageFile, originImageFileType);

            //  根據(jù)模板圖片切圖
            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;
    }

    /**
     * 根據(jù)模板圖生成遮罩圖
     * @param originImage 源圖
     * @param templateImage 模板圖
     * @param x 感興趣區(qū)域X軸
     * @param y 感興趣區(qū)域Y軸
     * @return 遮罩圖
     * @throws IOException 數(shù)據(jù)轉換異常
     */
    private static BufferedImage generateShadeByTemplateImage(BufferedImage originImage, BufferedImage templateImage, int x, int y) throws IOException {
        //  根據(jù)原圖,創(chuàng)建支持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);
            }
        }

        //  對遮罩圖根據(jù)模板像素進行處理
        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;
    }

    /**
     * 根據(jù)模板圖摳圖
     * @param interestArea  感興趣區(qū)域圖
     * @param templateImage  模板圖
     * @param cutoutImage 裁剪圖
     * @return 裁剪圖
     */
    private static BufferedImage cutoutImageByTemplateImage(BufferedImage interestArea, BufferedImage templateImage, BufferedImage cutoutImage) {
        //  獲取興趣區(qū)域圖片矩陣
        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;
    }

    /**
     * 獲取感興趣區(qū)域
     * @param x 感興趣區(qū)域X軸
     * @param y 感興趣區(qū)域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();

            //  根據(jù)坐標生成矩形
            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();
            //  圖片解密成二進制字符創(chuàng)
            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 滑動驗證碼驗證狀態(tài)
     * @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);
        }
    }
預覽圖
在這里插入圖片描述
后話

以上部分為主要業(yè)務邏輯代碼,你需要創(chuàng)建一個類和簡單的調試一下就能正常運行使用。相關的圖片資源文件和模板文件參見項目地址:https://gitee.com/gester/captcha.git

同時,推薦一波字符驗證碼和運算驗證碼文章。文章地址:http://www.lxweimin.com/p/fdafd4126c2e

如果這篇文章有幫助到您,請給一個star,謝謝大大。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容