登录校验之滑块验证码完整实现(vue + springboot)

文章目录

  • 前言
  • 一、实现效果
  • 二、实现思路
  • 三、实现步骤
    • 1. 后端 java 代码
      • 1.1 新建一个拼图验证码类
      • 1.2 新建一个拼图验证码工具类
      • 1.3 新建一个 service 类
      • 1.4 新建一个 controller 类
      • 1.5 登录接口
      • 2. 前端 vue 代码
        • 2.1 新建一个 sliderVerify 组件
        • 2.2 在登录页使用滑块组件
        • 总结

          前言

          嗨,大家好,我是希留。

          验证码一直是各类网站登录和注册的一种校验方式,是用来防止有人恶意使用脚本批量进行操作从而设置的一种安全保护方式。随着近几年技术的发展,人们对于系统安全性和用户体验的要求越来越高,大多数网站系统都逐渐采用行为验证码来代替传统的图片验证码。

          今天这篇文章就来记录一下,我是如何实现从前端、到后端校验的整个流程的。


          一、实现效果

          无图无真相,实现的效果如下图所示,点击登录后弹出一个弹出层,拼图是由后端生成的,拖动滑块位置,后端校验是否已拖动到指定的位置。

          二、实现思路

          整体的实现思路如下:

          • 1、从服务器随机取一张底透明有形状的模板图,再随机取一张背景图、
          • 2、随机生成抠图块坐标
          • 3、根据步骤2的坐标点,对背景大图的抠图区域的颜色进行处理,新建的图像根据轮廓图颜色赋值,背景图生成遮罩层。
          • 4、完成以上步骤之后得到两张图(扣下来的方块图,带有抠图区域阴影的原图),将这两张图和抠图区域的y坐标传到前台,x坐标存入redis。
          • 5、前端在移动拼图时将滑动距离x坐标参数请求后台验证,服务器根据redis取出x坐标与参数的x进行比较,如果在伐值内则验证通过。如果滑动不成功,自动刷新图片,重置拼图,滑动成功,且账号密码正确就直接跳转到首页。

            三、实现步骤

            1. 后端 java 代码

            1.1 新建一个拼图验证码类

            代码如下(示例):

            @Data
            public class Captcha { /**
                 * 随机字符串
                 **/
                private String nonceStr;
                /**
                 * 验证值
                 **/
                private String value;
                /**
                 * 生成的画布的base64
                 **/
                private String canvasSrc;
                /**
                 * 画布宽度
                 **/
                private Integer canvasWidth;
                /**
                 * 画布高度
                 **/
                private Integer canvasHeight;
                /**
                 * 生成的阻塞块的base64
                 **/
                private String blockSrc;
                /**
                 * 阻塞块宽度
                 **/
                private Integer blockWidth;
                /**
                 * 阻塞块高度
                 **/
                private Integer blockHeight;
                /**
                 * 阻塞块凸凹半径
                 **/
                private Integer blockRadius;
                /**
                 * 阻塞块的横轴坐标
                 **/
                private Integer blockX;
                /**
                 * 阻塞块的纵轴坐标
                 **/
                private Integer blockY;
                /**
                 * 图片获取位置
                 **/
                private Integer place;
            }
            

            1.2 新建一个拼图验证码工具类

            代码如下(示例):

            public class CaptchaUtils { /**
                 * 网络图片地址
                 **/
                private final static String IMG_URL = "https://loyer.wang/view/ftp/wallpaper/%s.jpg";
                /**
                 * 本地图片地址
                 **/
                private final static String IMG_PATH = "E:/Temp/wallpaper/%s.jpg";
                /**
                 * 入参校验设置默认值
                 **/
                public static void checkCaptcha(Captcha captcha) { //设置画布宽度默认值
                    if (captcha.getCanvasWidth() == null) { captcha.setCanvasWidth(320);
                    }
                    //设置画布高度默认值
                    if (captcha.getCanvasHeight() == null) { captcha.setCanvasHeight(155);
                    }
                    //设置阻塞块宽度默认值
                    if (captcha.getBlockWidth() == null) { captcha.setBlockWidth(65);
                    }
                    //设置阻塞块高度默认值
                    if (captcha.getBlockHeight() == null) { captcha.setBlockHeight(55);
                    }
                    //设置阻塞块凹凸半径默认值
                    if (captcha.getBlockRadius() == null) { captcha.setBlockRadius(9);
                    }
                    //设置图片来源默认值
                    if (captcha.getPlace() == null) { captcha.setPlace(0);
                    }
                }
                /**
                 * 获取指定范围内的随机数
                 **/
                public static int getNonceByRange(int start, int end) { Random random = new Random();
                    return random.nextInt(end - start + 1) + start;
                }
                /**
                 * 获取验证码资源图
                 **/
                public static BufferedImage getBufferedImage(Integer place) { try { //随机图片
                        int nonce = getNonceByRange(0, 1000);
                        //获取网络资源图片
                        if (0 == place) { String imgUrl = String.format(IMG_URL, nonce);
                            URL url = new URL(imgUrl);
                            return ImageIO.read(url.openStream());
                        }
                        //获取本地图片
                        else { String imgPath = String.format(IMG_PATH, nonce);
                            File file = new File(imgPath);
                            return ImageIO.read(file);
                        }
                    } catch (Exception e) { System.out.println("获取拼图资源失败");
                        //异常处理
                        return null;
                    }
                }
                /**
                 * 调整图片大小
                 **/
                public static BufferedImage imageResize(BufferedImage bufferedImage, int width, int height) { Image image = bufferedImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
                    BufferedImage resultImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
                    Graphics2D graphics2D = resultImage.createGraphics();
                    graphics2D.drawImage(image, 0, 0, null);
                    graphics2D.dispose();
                    return resultImage;
                }
                /**
                 * 抠图,并生成阻塞块
                 **/
                public static void cutByTemplate(BufferedImage canvasImage, BufferedImage blockImage, int blockWidth, int blockHeight, int blockRadius, int blockX, int blockY) { BufferedImage waterImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
                    //阻塞块的轮廓图
                    int[][] blockData = getBlockData(blockWidth, blockHeight, blockRadius);
                    //创建阻塞块具体形状
                    for (int i = 0; i < blockWidth; i++) { for (int j = 0; j < blockHeight; j++) { try { //原图中对应位置变色处理
                                if (blockData[i][j] == 1) { //背景设置为黑色
                                    waterImage.setRGB(i, j, Color.BLACK.getRGB());
                                    blockImage.setRGB(i, j, canvasImage.getRGB(blockX + i, blockY + j));
                                    //轮廓设置为白色,取带像素和无像素的界点,判断该点是不是临界轮廓点
                                    if (blockData[i + 1][j] == 0 || blockData[i][j + 1] == 0 || blockData[i - 1][j] == 0 || blockData[i][j - 1] == 0) { blockImage.setRGB(i, j, Color.WHITE.getRGB());
                                        waterImage.setRGB(i, j, Color.WHITE.getRGB());
                                    }
                                }
                                //这里把背景设为透明
                                else { blockImage.setRGB(i, j, Color.TRANSLUCENT);
                                    waterImage.setRGB(i, j, Color.TRANSLUCENT);
                                }
                            } catch (ArrayIndexOutOfBoundsException e) { //防止数组下标越界异常
                            }
                        }
                    }
                    //在画布上添加阻塞块水印
                    addBlockWatermark(canvasImage, waterImage, blockX, blockY);
                }
                /**
                 * 构建拼图轮廓轨迹
                 **/
                private static int[][] getBlockData(int blockWidth, int blockHeight, int blockRadius) { int[][] data = new int[blockWidth][blockHeight];
                    double po = Math.pow(blockRadius, 2);
                    //随机生成两个圆的坐标,在4个方向上 随机找到2个方向添加凸/凹
                    //凸/凹1
                    int face1 = RandomUtils.nextInt(0,4);
                    //凸/凹2
                    int face2;
                    //保证两个凸/凹不在同一位置
                    do { face2 = RandomUtils.nextInt(0,4);
                    } while (face1 == face2);
                    //获取凸/凹起位置坐标
                    int[] circle1 = getCircleCoords(face1, blockWidth, blockHeight, blockRadius);
                    int[] circle2 = getCircleCoords(face2, blockWidth, blockHeight, blockRadius);
                    //随机凸/凹类型
                    int shape = getNonceByRange(0, 1);
                    //圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
                    //计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
                    for (int i = 0; i < blockWidth; i++) { for (int j = 0; j < blockHeight; j++) { data[i][j] = 0;
                            //创建中间的方形区域
                            if ((i >= blockRadius && i <= blockWidth - blockRadius && j >= blockRadius && j <= blockHeight - blockRadius)) { data[i][j] = 1;
                            }
                            double d1 = Math.pow(i - Objects.requireNonNull(circle1)[0], 2) + Math.pow(j - circle1[1], 2);
                            double d2 = Math.pow(i - Objects.requireNonNull(circle2)[0], 2) + Math.pow(j - circle2[1], 2);
                            //创建两个凸/凹
                            if (d1 <= po || d2 <= po) { data[i][j] = shape;
                            }
                        }
                    }
                    return data;
                }
                /**
                 * 根据朝向获取圆心坐标
                 */
                private static int[] getCircleCoords(int face, int blockWidth, int blockHeight, int blockRadius) { //上
                    if (0 == face) { return new int[]{blockWidth / 2 - 1, blockRadius};
                    }
                    //左
                    else if (1 == face) { return new int[]{blockRadius, blockHeight / 2 - 1};
                    }
                    //下
                    else if (2 == face) { return new int[]{blockWidth / 2 - 1, blockHeight - blockRadius - 1};
                    }
                    //右
                    else if (3 == face) { return new int[]{blockWidth - blockRadius - 1, blockHeight / 2 - 1};
                    }
                    return null;
                }
                /**
                 * 在画布上添加阻塞块水印
                 */
                private static void addBlockWatermark(BufferedImage canvasImage, BufferedImage blockImage, int x, int y) { Graphics2D graphics2D = canvasImage.createGraphics();
                    graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f));
                    graphics2D.drawImage(blockImage, x, y, null);
                    graphics2D.dispose();
                }
                /**
                 * BufferedImage转BASE64
                 */
                public static String toBase64(BufferedImage bufferedImage, String type) { try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                        ImageIO.write(bufferedImage, type, byteArrayOutputStream);
                        String base64 = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
                        return String.format("data:image/%s;base64,%s", type, base64);
                    } catch (IOException e) { System.out.println("图片资源转换BASE64失败");
                        //异常处理
                        return null;
                    }
                }
            }
            

            1.3 新建一个 service 类

            代码如下(示例):

            @Service
            public class CaptchaService { /**
                 * 拼图验证码允许偏差
                 **/
                private static Integer ALLOW_DEVIATION = 3;
                @Autowired
                private StringRedisTemplate stringRedisTemplate;
                /**
                 * 校验验证码
                 * @param imageKey
                 * @param imageCode
                 * @return boolean
                 **/
                public String checkImageCode(String imageKey, String imageCode) { ValueOperations ops = stringRedisTemplate.opsForValue();
                    String text = ops.get("imageCode:" + imageKey);
                    if(StrUtil.isBlank(text)){ return "验证码已失效";
                    }
                    // 根据移动距离判断验证是否成功
                    if (Math.abs(Integer.parseInt(text) - Integer.parseInt(imageCode)) > ALLOW_DEVIATION) { return "验证失败,请控制拼图对齐缺口";
                    }
                    return null;
                }
                /**
                 * 缓存验证码,有效期15分钟
                 * @param key
                 * @param code
                 **/
                public void saveImageCode(String key, String code) { ValueOperations ops = stringRedisTemplate.opsForValue();
                    ops.set("imageCode:" + key, code, 15, TimeUnit.MINUTES);
                }
                /**
                 * 获取验证码拼图(生成的抠图和带抠图阴影的大图及抠图坐标)
                 **/
                public Object getCaptcha(Captcha captcha) { //参数校验
                    CaptchaUtils.checkCaptcha(captcha);
                    //获取画布的宽高
                    int canvasWidth = captcha.getCanvasWidth();
                    int canvasHeight = captcha.getCanvasHeight();
                    //获取阻塞块的宽高/半径
                    int blockWidth = captcha.getBlockWidth();
                    int blockHeight = captcha.getBlockHeight();
                    int blockRadius = captcha.getBlockRadius();
                    //获取资源图
                    BufferedImage canvasImage = CaptchaUtils.getBufferedImage(captcha.getPlace());
                    //调整原图到指定大小
                    canvasImage = CaptchaUtils.imageResize(canvasImage, canvasWidth, canvasHeight);
                    //随机生成阻塞块坐标
                    int blockX = CaptchaUtils.getNonceByRange(blockWidth, canvasWidth - blockWidth - 10);
                    int blockY = CaptchaUtils.getNonceByRange(10, canvasHeight - blockHeight + 1);
                    //阻塞块
                    BufferedImage blockImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
                    //新建的图像根据轮廓图颜色赋值,源图生成遮罩
                    CaptchaUtils.cutByTemplate(canvasImage, blockImage, blockWidth, blockHeight, blockRadius, blockX, blockY);
                    // 移动横坐标
                    String nonceStr = UUID.randomUUID().toString().replaceAll("-", "");
                    // 缓存
                    saveImageCode(nonceStr,String.valueOf(blockX));
                    //设置返回参数
                    captcha.setNonceStr(nonceStr);
                    captcha.setBlockY(blockY);
                    captcha.setBlockSrc(CaptchaUtils.toBase64(blockImage, "png"));
                    captcha.setCanvasSrc(CaptchaUtils.toBase64(canvasImage, "png"));
                    return captcha;
                }
            }
            

            1.4 新建一个 controller 类

            代码如下(示例):

            @RestController
            @RequestMapping("/captcha")
            public class CaptchaController { @Autowired
                private CaptchaService captchaService;
                @ApiOperation(value = "生成验证码拼图")
                @PostMapping("get-captcha")
                public R getCaptcha(@RequestBody Captcha captcha) { return R.ok(captchaService.getCaptcha(captcha));
                }
            }
            

            1.5 登录接口

            代码如下(示例):

             @ApiOperation(value = "登录")
                @PostMapping(value = "login")
                public R login(@RequestBody LoginVo loginVo) { // 只有开启了验证码功能才需要验证
                    if (needAuthCode) { String msg = captchaService.checkImageCode(loginVo.getNonceStr(),loginVo.getValue());
                        if (StringUtils.isNotBlank(msg)) { return R.error(msg);
                        }
                    }
                    String token = loginService.login(loginVo.getUserName(), loginVo.getPassWord());
                    if (StringUtils.isBlank(token)) { return R.error("用户名或密码错误");
                    }
                    Map tokenMap = new HashMap<>();
                    tokenMap.put("token", token);
                    tokenMap.put("tokenHead", tokenHead);
                    return R.ok(tokenMap);
                }
            

            2. 前端 vue 代码

            2.1 新建一个 sliderVerify 组件

            代码如下(示例):

            2.2 在登录页使用滑块组件

            代码如下(示例):

             

            总结

            好了,以上就是本文的全部内容了,感谢您的阅读。

            若觉得本文对你有帮助的话,还不忘点赞评论支持一下,感谢~