SpringBoot前后端分离实现拼图滑动验证码(后端)

最近有需求要实现拼图滑动验证码,找了点资料做了个demo看看。

思路

简单来说就是后端随机获取图片,随即切割后把处理完的两张图片,拼图和背景存放到对应文件夹,之后将文件地址(名称)和拼图位置信息返回到前端,实现拖动重合效果即可。

传递数据格式

由bean类进行数据存放,包括拼图左上角的x,y坐标位置,拼图和背景文件名称。

public class VerificationCodePlace {
    private String backName;
    private String markName;
    private int xLocation;
private int yLocation;

    public VerificationCodePlace(){}
    public VerificationCodePlace(String backName, String markName, int xLocation, int yLocation){
        this.backName = backName;
        this.markName = markName;
        this.xLocation = xLocation;
        this.yLocation = yLocation;
    }

    public String getBackName() {
        return backName;
    }

    public void setBackName(String backName) {
        this.backName = backName;
    }

    public String getMarkName() {
        return markName;
    }

    public void setMarkName(String markName) {
        this.markName = markName;
    }

    public int getxLocation() {
        return xLocation;
    }

    public void setxLocation(int xLocation) {
        this.xLocation = xLocation;
    }

    public int getyLocation() {
        return yLocation;
    }

    public void setyLocation(int yLocation) {
        this.yLocation = yLocation;
    }
}

拼图切割工具类

该工具类的随机切割方法主要参考https://www.jianshu.com/p/eb190c26fb5b中提供的方法,利用一个图片宽×图片高的数组来存放像素透明,保持原状或者半透明的信息,来实现拼图造型的随机生成。

public class VerificationCodeAdapter {
    /**
     * 源文件宽度
     */
    private static int ORI_WIDTH = 300;
    /**
     * 源文件高度
     */
    private static int ORI_HEIGHT = 150;
    /**
     * 模板图宽度
     */
    private static int CUT_WIDTH = 50;
    /**
     * 模板图高度
     */
    private static int CUT_HEIGHT = 50;
    /**
     * 抠图凸起圆心
     */
    private static int circleR = 5;
    /**
     * 抠图内部矩形填充大小
     */
    private static int RECTANGLE_PADDING = 8;
    /**
     * 抠图的边框宽度
     */
    private static int SLIDER_IMG_OUT_PADDING = 1;

    // 生成拼图样式
    private static int[][] getBlockData(){
        int[][] data = new int[CUT_WIDTH][CUT_HEIGHT];
        Random random = new Random();
        //(x-a)²+(y-b)²=r²
        //x中心位置左右5像素随机
        double x1 = RECTANGLE_PADDING + (CUT_WIDTH - 2 * RECTANGLE_PADDING) / 2.0 - 5 + random.nextInt(10);
        //y 矩形上边界半径-1像素移动
        double y1_top = RECTANGLE_PADDING - random.nextInt(3);
        double y1_bottom = CUT_HEIGHT - RECTANGLE_PADDING + random.nextInt(3);
        double y1 = random.nextInt(2) == 1 ? y1_top : y1_bottom;


        double x2_right = CUT_WIDTH - RECTANGLE_PADDING - circleR + random.nextInt(2 * circleR - 4);
        double x2_left = RECTANGLE_PADDING + circleR - 2 - random.nextInt(2 * circleR - 4);
        double x2 = random.nextInt(2) == 1 ? x2_right : x2_left;
        double y2 = RECTANGLE_PADDING + (CUT_HEIGHT - 2 * RECTANGLE_PADDING) / 2.0 - 4 + random.nextInt(10);

        double po = Math.pow(circleR, 2);
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                //矩形区域
                boolean fill;
                if ((i >= RECTANGLE_PADDING && i < CUT_WIDTH - RECTANGLE_PADDING)
                        && (j >= RECTANGLE_PADDING && j < CUT_HEIGHT - RECTANGLE_PADDING)) {
                    data[i][j] = 1;
                    fill = true;
                } else {
                    data[i][j] = 0;
                    fill = false;
                }
                //凸出区域
                double d3 = Math.pow(i - x1, 2) + Math.pow(j - y1, 2);
                if (d3 < po) {
                    data[i][j] = 1;
                } else {
                    if (!fill) {
                        data[i][j] = 0;
                    }
                }
                //凹进区域
                double d4 = Math.pow(i - x2, 2) + Math.pow(j - y2, 2);
                if (d4 < po) {
                    data[i][j] = 0;
                }
            }
        }
        //边界阴影
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                //四个正方形边角处理
                for (int k = 1; k <= SLIDER_IMG_OUT_PADDING; k++) {
                    //左上、右上
                    if (i >= RECTANGLE_PADDING - k && i < RECTANGLE_PADDING
                            && ((j >= RECTANGLE_PADDING - k && j < RECTANGLE_PADDING)
                            || (j >= CUT_HEIGHT - RECTANGLE_PADDING - k && j < CUT_HEIGHT - RECTANGLE_PADDING +1))) {
                        data[i][j] = 2;
                    }

                    //左下、右下
                    if (i >= CUT_WIDTH - RECTANGLE_PADDING + k - 1 && i < CUT_WIDTH - RECTANGLE_PADDING + 1) {
                        for (int n = 1; n <= SLIDER_IMG_OUT_PADDING; n++) {
                            if (((j >= RECTANGLE_PADDING - n && j < RECTANGLE_PADDING)
                                    || (j >= CUT_HEIGHT - RECTANGLE_PADDING - n && j <= CUT_HEIGHT - RECTANGLE_PADDING ))) {
                                data[i][j] = 2;
                            }
                        }
                    }
                }

                if (data[i][j] == 1 && j - SLIDER_IMG_OUT_PADDING > 0 && data[i][j - SLIDER_IMG_OUT_PADDING] == 0) {
                    data[i][j - SLIDER_IMG_OUT_PADDING] = 2;
                }
                if (data[i][j] == 1 && j + SLIDER_IMG_OUT_PADDING > 0 && j + SLIDER_IMG_OUT_PADDING < CUT_HEIGHT && data[i][j + SLIDER_IMG_OUT_PADDING] == 0) {
                    data[i][j + SLIDER_IMG_OUT_PADDING] = 2;
                }
                if (data[i][j] == 1 && i - SLIDER_IMG_OUT_PADDING > 0 && data[i - SLIDER_IMG_OUT_PADDING][j] == 0) {
                    data[i - SLIDER_IMG_OUT_PADDING][j] = 2;
                }
                if (data[i][j] == 1 && i + SLIDER_IMG_OUT_PADDING > 0 && i + SLIDER_IMG_OUT_PADDING < CUT_WIDTH && data[i + SLIDER_IMG_OUT_PADDING][j] == 0) {
                    data[i + SLIDER_IMG_OUT_PADDING][j] = 2;
                }
            }
        }
        return data;
    }

    // 抠出拼图
    private static void cutImgByTemplate(BufferedImage oriImage, BufferedImage targetImage, int[][] blockImage, int x, int y) {
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                int _x = x + i;
                int _y = y + j;
                int rgbFlg = blockImage[i][j];
                int rgb_ori = oriImage.getRGB(_x, _y);
                // 原图中对应位置变色处理
                if (rgbFlg == 1) {
                    //抠图上复制对应颜色值
                    targetImage.setRGB(i,j, rgb_ori);
                    //原图对应位置颜色变化
                    oriImage.setRGB(_x, _y, Color.LIGHT_GRAY.getRGB());
                } else if (rgbFlg == 2) {
                    targetImage.setRGB(i, j, Color.WHITE.getRGB());
                    oriImage.setRGB(_x, _y, Color.GRAY.getRGB());
                }else if(rgbFlg == 0){
                    //int alpha = 0;
                    targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);
                }
            }

        }
    }

    // 获取图片
    private static BufferedImage getBufferedImage(String path) throws IOException{
        File file = new File(path);
        System.out.println(file.getAbsolutePath());
        if(file.isFile()){
            return ImageIO.read(file);
        }
        return null;
    }

    // 存放图片
    private static void writeImg(BufferedImage image, String file) throws Exception {
        byte[] imagedata = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ImageIO.write(image,"png",bos);
        imagedata = bos.toByteArray();
        File outFile = new File(file);
        System.out.println(outFile.getAbsolutePath());
        FileOutputStream out = new FileOutputStream(outFile);
        out.write(imagedata);
        out.close();
    }

该项目作为示意demo,处理完的图片赋随机文件名时仅仅简单加上随机四位后缀。随机取的图片为定高定宽,方便处理。Demo中将这些图片存放到resources文件夹image中,方便随机选取。图片定高定长处理脚本可以看我之前写的 Python实现 jpg图像修改大小并转换为png

	    // 处理存放
    private static VerificationCodePlace cutAndSave(String imgName, String path, int [][] data, String headPath) throws Exception {
        VerificationCodePlace vcPlace =
                new VerificationCodePlace("sample_after.png", "sample_after_mark.png", 112, 50);

        // 进行图片处理
        BufferedImage originImage = getBufferedImage(path);
        if(originImage!=null) {
            int locationX = CUT_WIDTH + new Random().nextInt(originImage.getWidth() - CUT_WIDTH * 3);
            int locationY = CUT_HEIGHT + new Random().nextInt(originImage.getHeight() - CUT_HEIGHT) / 2;
            BufferedImage markImage = new BufferedImage(CUT_WIDTH, CUT_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR);
            cutImgByTemplate(originImage, markImage, data, locationX, locationY);

            String name = imgName.substring(0, imgName.indexOf('.'));

            // 考虑图片覆盖,简单设置四位随机数
            int r = (int)Math.round(Math.random() * 8999) + 1000;
            String afterName = name + "_after" + r + ".png";
            String markName = name + "_after_mark" + r + ".png";
            writeImg(originImage, headPath + afterName);
            writeImg(markImage, headPath + markName);
            vcPlace = new VerificationCodePlace(afterName, markName, locationX, locationY);
        }

        return vcPlace;
    }

    // 获取文件夹下所有文件名
    private static ArrayList<String> getFileNamesFromDic(String dicPath){
        File dic = new File(dicPath);
        ArrayList<String> imageFileNames = new ArrayList<String>();
        File[] dicFileList = dic.listFiles();
        for(File f: dicFileList){
            imageFileNames.add(f.getName());
        }
        return imageFileNames;
    }

    // 总流程,随机获取图片并处理,将拼图和对应图片存放至after_img
    // 出错则返回sample
    // headPath为存放生成图片的文件夹地址
    public static VerificationCodePlace getRandomVerificationCodePlace(String headPath) {
        VerificationCodePlace vcPlace = new VerificationCodePlace("sample_after.png", "sample_mark_after.png", 112, 50);

        // 从文件夹中读取所有待选择文件
        String directoryPath = "src/main/resources/static/image";
        ArrayList<String> imageFileNames = getFileNamesFromDic(directoryPath);

        // 随机获取
        int r = (int)Math.round(Math.random() * (imageFileNames.size() - 1));
        String imgName = imageFileNames.get(r);
        String path = "src/main/resources/static/image/" + imgName;
        int[][] data = VerificationCodeAdapter.getBlockData();

        // 进行图片处理
        try {
            vcPlace = cutAndSave(imgName, path, data, headPath);
        } catch (Exception e) {
            e.printStackTrace();
            return vcPlace;
        }

        return vcPlace;
    }

参数headpath为处理后文件存放处。为了防止出错时前端没有收到返回,在文件夹中默认放置两张sample图片,在后端不能成功切割时进行数据的返回。

控制器类

前端计划使用ajax进行数据的获取,首先写一个简单get来处理图片返回信息,同时一个页面用来显示验证码。

	@RequestMapping("/getImgInfo")
    @ResponseBody
    // 随机获取背景和拼图,返回json
    public String imgInfo(){
        VerificationCodePlace vcPlace =VerificationCodeAdapter.getRandomVerificationCodePlace(imgLocation);
        ObjectMapper om = new ObjectMapper();
        String jsonResult = "";
        try {
            jsonResult = om.writeValueAsString(vcPlace);
            return jsonResult;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return jsonResult;
    }

    @RequestMapping("/vcode")
    // 验证码主界面
    public String vCode(){
        return "vc_sample.html";
    }

图片存放和url获取

由于Springboot是自带tomcat,服务器文件夹为临时文件夹,不能直接设置获取图片文件。故本demo将图片使用额外的文件夹存放。为了保证前端能通过url获取到对应的图片信息,增加config类。

@Configuration
public class ResourceConfig implements WebMvcConfigurer {

    // 为了前端能访问生成图片,进行本地映射
    @Value("${afterImage.resourceHandler}")
    private String resourceHandler;

    @Value("${afterImage.location}")
    private String location;

    public void addResourceHandlers(ResourceHandlerRegistry registry){
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        registry.addResourceHandler(resourceHandler).addResourceLocations("file:///" + location);
    }
}

config类中设置对应的url前缀映射的文件地址。application.yml中加入配置信息。

afterImage:
    resourceHandler: /after/**
    location: D:/vc_image/

图片删除

考虑到正式使用时图片是会存放在服务器中,需要定期进行删除来减轻服务器的负担,给出了删除图片的方法。
Adapter类中:

    // 删除after中的图片文件
    public static String deleteAfterImage(String headPath){
        boolean successDelete = true;
        int sum = 0;
        float fileSize = 0;
        String directoryPath = headPath;
        File dic = new File(directoryPath);
        File[] dicFileList = dic.listFiles();
        if(dicFileList != null) {
            for (File f : dicFileList) {
                if (!f.getName().equals("sample_after.png") && !f.getName().equals("sample_after_mark.png")) {
                    long fLength = f.length();
                    successDelete = f.delete();
                    if(!successDelete)
                        break;
                    sum ++;
                    fileSize += fLength;

                }
            }
        }
        float fileSizeInMB = fileSize / 1024 / 1024;
        if(!successDelete){
            String tip = "拼图文件删除中出现错误,请到" + directoryPath + "中进行查看";
            System.out.println(tip);
            return tip;
        }else{
            String tip = "拼图文件删除成功,删除文件数量为" + sum + ",文件总大小为" + fileSizeInMB + "MB";
            System.out.println(tip);
            return tip;
        }
    }

controller中:

	@RequestMapping("/deleteImg")
    @ResponseBody
    // 删除生成的验证码图片
    public String deleteImg(){
        return VerificationCodeAdapter.deleteAfterImage(imgLocation);
    }

图片处理效果和数据返回

在这里插入图片描述

请添加图片描述
返回数据:
{“backName”:“vc1_min_after9841.png”,“markName”:“vc1_min_after_mark9841.png”,“xLocation”:74,“yLocation”:89}
前端使用即可。

下篇文章讲下前端实现。
项目开源地址:
https://github.com/huiluczP/verification_code_demo

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
滑动拼图验证码在小红书中的应用类似于其他网站。滑动拼图验证码是一种人机验证机制,通过用户拖动滑块将缺口对齐,以证明用户是真实的人类,而不是自动化程序或恶意机器人。 具体到小红书的实现,根据引用中提供的代码,可以看到它使用了一个名为`slideverify`的自定义组件。这个组件接受一些参数,如滑块宽度、滑块高度、滑块位置等,并提供了一些回调函数,如`onSuccess`、`onRefresh`等。 其中,`getImageVerifyCode`函数用于获取验证码图片,并将图片的地址赋值给`imgurl`和`miniimgurl`。`imgurl`存储原始大小的验证码图片地址,`miniimgurl`存储缩略图的验证码图片地址。 `onRefresh`函数用于刷新验证码,它会清空`imgurl`和`miniimgurl`的值,并重新调用`getImageVerifyCode`函数获取新的验证码图片。 `onSuccess`函数在滑动结束后,将滑动的距离作为参数传入,并调用`verifyImageCode`函数进行后台验证。根据后台返回的验证结果,如果通过则显示成功信息,否则显示错误信息,并调用`onRefresh`函数刷新验证码。 总的来说,滑动拼图验证码在小红书中的实现是通过自定义组件和一些回调函数来完成的,它增加了用户与机器的交互,提高了系统的安全性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Vue实现滑动拼图验证码功能](https://download.csdn.net/download/weixin_38747917/14818686)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [3分钟使用Halcon识别网易滑块拼图验证码](https://blog.csdn.net/qq_29888333/article/details/84192678)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Java Vue uni-app 三端实现滑动拼图验证码](https://blog.csdn.net/qq_32698323/article/details/118876646)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值