spring框架实现滑动验证码功能

avatar
作者
筋斗云
阅读量:0

spring框架实现滑动验证码功能

1. 整体描述

之前项目需要在验证码模块,增加滑动验证码,用来给手机端使用的,大概看了下,主要方法就是将图片切割,然后记住偏移量,进行滑动,前端验证的时候,需要用前端传入的偏移量和生成的偏移量进行对比,如果在阈值之内,就验证通过,否则就不通过。具体实现方式见下文。之前没时间写,最近记录一下。

2. 具体实现

本工程主要依赖springboot框架,并且需要redis存验证码的信息,还需要几个图片,用来生成验证码。

2.1 滑动验证码实体类

package com.thcb.captchademo.captcha.domain;  import lombok.Data;   /**  * 滑动验证码  *  * @author thcb  * @date 2023-05-25  */  @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; }  

2.2 滑动验证码登录VO

package com.thcb.captchademo.captcha.domain;  import lombok.Data;  /**  * 滑动验证码登录Vo  *  * @author thcb  * @date 2023-05-25  */  @Data public class LoginVo {     /**      * 随机字符串      **/     private String nonceStr;     /**      * 验证值      **/     private String value; }  

2.3 滑动验证码接口返回类

package com.thcb.captchademo.captcha.utils;  import java.util.HashMap;  /**  * 操作消息提醒  *  * @author thcb  */ public class AjaxResult extends HashMap<String, Object> {     private static final long serialVersionUID = 1L;      /**      * 状态码      */     public static final String CODE_TAG = "code";      /**      * 返回内容      */     public static final String MSG_TAG = "msg";      /**      * 数据对象      */     public static final String DATA_TAG = "data";      /**      * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。      */     public AjaxResult() {     }      /**      * 初始化一个新创建的 AjaxResult 对象      *      * @param code 状态码      * @param msg  返回内容      */     public AjaxResult(int code, String msg) {         super.put(CODE_TAG, code);         super.put(MSG_TAG, msg);     }      /**      * 初始化一个新创建的 AjaxResult 对象      *      * @param code 状态码      * @param msg  返回内容      * @param data 数据对象      */     public AjaxResult(int code, String msg, Object data) {         super.put(CODE_TAG, code);         super.put(MSG_TAG, msg);         if (data != null && !data.equals("")) {             super.put(DATA_TAG, data);         }     }      /**      * 返回成功消息      *      * @return 成功消息      */     public static AjaxResult success() {         return AjaxResult.success("操作成功");     }      /**      * 返回成功数据      *      * @return 成功消息      */     public static AjaxResult success(Object data) {         return AjaxResult.success("操作成功", data);     }      /**      * 返回成功消息      *      * @param msg 返回内容      * @return 成功消息      */     public static AjaxResult success(String msg) {         return AjaxResult.success(msg, null);     }      /**      * 返回成功消息      *      * @param msg  返回内容      * @param data 数据对象      * @return 成功消息      */     public static AjaxResult success(String msg, Object data) {         return new AjaxResult(200, msg, data);     }      /**      * 返回错误消息      *      * @return      */     public static AjaxResult error() {         return AjaxResult.error("操作失败");     }      /**      * 返回错误消息      *      * @param msg 返回内容      * @return 警告消息      */     public static AjaxResult error(String msg) {         return AjaxResult.error(msg, null);     }      /**      * 返回错误消息      *      * @param msg  返回内容      * @param data 数据对象      * @return 警告消息      */     public static AjaxResult error(String msg, Object data) {         return new AjaxResult(500, msg, data);     }      /**      * 返回错误消息      *      * @param code 状态码      * @param msg  返回内容      * @return 警告消息      */     public static AjaxResult error(int code, String msg) {         return new AjaxResult(code, msg, null);     } }  

2.4 滑动验证码工具类

此类是核心类,主要实现了图片的切割功能。

package com.thcb.captchademo.captcha.utils;  import com.thcb.captchademo.captcha.domain.Captcha;  import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.Base64; import java.util.Objects; import java.util.Random;  /**  * 滑动验证码工具类  *  * @author thcb  * @date 2023-05-25  */ public class CaptchaUtils {      /**      * 网络图片地址      **/     private final static String IMG_URL = "https://loyer.wang/view/ftp/wallpaper/%s.jpg";      /**      * 本地图片地址      **/     private final static String IMG_PATH = "E:\\caphcha\\%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(1);         }     }      /**      * 获取指定范围内的随机数      **/     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 {             //随机图片              //获取网络资源图片             if (0 == place) {                 int nonce = getNonceByRange(0, 1000);                 String imgUrl = String.format(IMG_URL, nonce);                 URL url = new URL(imgUrl);                 return ImageIO.read(url.openStream());             }             //获取本地图片             else {                 int nonce = getNonceByRange(0, 20);                 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         Random random1 = new Random();         int face1 = random1.nextInt(4);         //凸/凹2         int face2;         //保证两个凸/凹不在同一位置         do {             Random random2 = new Random();             face2 = random2.nextInt(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;         }     } } 

2.5 滑动验证码Service

service层,,封装了工具类的一些方法,这里为了简单没写接口,正常应该是service和impl两个类。

package com.thcb.captchademo.captcha.service;  import com.thcb.captchademo.captcha.domain.Captcha; import com.thcb.captchademo.captcha.utils.CaptchaUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service;  import java.awt.image.BufferedImage; import java.util.UUID; import java.util.concurrent.TimeUnit;  /**  * 滑动验证码Service  *  * @author thcb  * @date 2023-05-25  */  @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<String, String> ops = stringRedisTemplate.opsForValue();         String key = "imageCode:" + imageKey;         String text = ops.get(key);         if (text == null || text.equals("")) {             return "验证码已失效";         }         // 根据移动距离判断验证是否成功         if (Math.abs(Integer.parseInt(text) - Integer.parseInt(imageCode)) > ALLOW_DEVIATION) {             return "验证失败,请控制拼图对齐缺口";         }         // 验证成功,删除redis缓存         stringRedisTemplate.delete(key);         return null;     }      /**      * 缓存验证码,有效期1分钟      *      * @param key      * @param code      **/     public void saveImageCode(String key, String code) {         ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();         ops.set("imageCode:" + key, code, 60, TimeUnit.SECONDS);     }      /**      * 获取验证码拼图(生成的抠图和带抠图阴影的大图及抠图坐标)      **/     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;     } } 

2.6 滑动验证码Controller

controller层有两个方法,一个是获取验证码的方法,一个是验证码校验的方法。

package com.thcb.captchademo.captcha.controller;  import com.thcb.captchademo.captcha.domain.Captcha; import com.thcb.captchademo.captcha.domain.LoginVo; import com.thcb.captchademo.captcha.service.CaptchaService; import com.thcb.captchademo.captcha.utils.AjaxResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;  /**  * 滑动验证码Controller  *  * @author thcb  * @date 2023-05-25  */  @RestController @RequestMapping("/captcha") public class CaptchaController {      @Autowired     private CaptchaService captchaService;      @PostMapping("/slideCaptchaImage")     public AjaxResult getCaptcha() {         return AjaxResult.success(captchaService.getCaptcha(new Captcha()));     }      @PostMapping(value = "/login")     public AjaxResult login(@RequestBody LoginVo loginVo) {         // 验证码校验         String msg = captchaService.checkImageCode(loginVo.getNonceStr(), loginVo.getValue());         if (msg != null && !msg.equals("")) {             return AjaxResult.error(msg);         }         return AjaxResult.success();     }  }  

3 工程源码

工程不算复杂,源码上传到GitCode,项目地址,上传有点问题,而且目前只能设置成私密项目,等之后可以调整的时候再改成公开的。

4 总结

滑动验证码功能不算复杂,可以和项目当前已有的验证码共存,调用不同的接口,返回不同类型的验证码,当然这个就根据项目具体情况确定了。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!