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);
    }

图片处理效果和数据返回

SpringBoot前后端分离实现拼图滑动验证码(后端)_第1张图片

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

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

你可能感兴趣的:(java,java,spring,boot)