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

[复制链接]
发表于 2026-2-19 18:35:33 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
1. 团体形貌

之前项目须要在验证码模块,增长滑动验证码,用来给手机端利用的,大概看了下,紧张方法就是将图片切割,然跋文着偏移量,举行滑动,前端验证的时间,须要用前端传入的偏移量和天生的偏移量举行对比,如果在阈值之内,就验证通过,否则就不通过。具体实现方式见下文。之前没时间写,迩来记载一下。
2. 具体实现

本工程紧张依赖springboot框架,而且须要redis存验证码的信息,还须要几个图片,用来天生验证码。
2.1 滑动验证码实体类

  1. package com.thcb.captchademo.captcha.domain;
  2. import lombok.Data;
  3. /**
  4. * 滑动验证码
  5. *
  6. * @author thcb
  7. * @date 2023-05-25
  8. */
  9. @Data
  10. public class Captcha {
  11.     /**
  12.      * 随机字符串
  13.      **/
  14.     private String nonceStr;
  15.     /**
  16.      * 验证值
  17.      **/
  18.     private String value;
  19.     /**
  20.      * 生成的画布的base64
  21.      **/
  22.     private String canvasSrc;
  23.     /**
  24.      * 画布宽度
  25.      **/
  26.     private Integer canvasWidth;
  27.     /**
  28.      * 画布高度
  29.      **/
  30.     private Integer canvasHeight;
  31.     /**
  32.      * 生成的阻塞块的base64
  33.      **/
  34.     private String blockSrc;
  35.     /**
  36.      * 阻塞块宽度
  37.      **/
  38.     private Integer blockWidth;
  39.     /**
  40.      * 阻塞块高度
  41.      **/
  42.     private Integer blockHeight;
  43.     /**
  44.      * 阻塞块凸凹半径
  45.      **/
  46.     private Integer blockRadius;
  47.     /**
  48.      * 阻塞块的横轴坐标
  49.      **/
  50.     private Integer blockX;
  51.     /**
  52.      * 阻塞块的纵轴坐标
  53.      **/
  54.     private Integer blockY;
  55.     /**
  56.      * 图片获取位置
  57.      **/
  58.     private Integer place;
  59. }
复制代码
2.2 滑动验证码登录VO

  1. package com.thcb.captchademo.captcha.domain;
  2. import lombok.Data;
  3. /**
  4. * 滑动验证码登录Vo
  5. *
  6. * @author thcb
  7. * @date 2023-05-25
  8. */
  9. @Data
  10. public class LoginVo {
  11.     /**
  12.      * 随机字符串
  13.      **/
  14.     private String nonceStr;
  15.     /**
  16.      * 验证值
  17.      **/
  18.     private String value;
  19. }
复制代码
2.3 滑动验证码接口返回类

  1. package com.thcb.captchademo.captcha.utils;
  2. import java.util.HashMap;
  3. /**
  4. * 操作消息提醒
  5. *
  6. * @author thcb
  7. */
  8. public class AjaxResult extends HashMap<String, Object> {
  9.     private static final long serialVersionUID = 1L;
  10.     /**
  11.      * 状态码
  12.      */
  13.     public static final String CODE_TAG = "code";
  14.     /**
  15.      * 返回内容
  16.      */
  17.     public static final String MSG_TAG = "msg";
  18.     /**
  19.      * 数据对象
  20.      */
  21.     public static final String DATA_TAG = "data";
  22.     /**
  23.      * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
  24.      */
  25.     public AjaxResult() {
  26.     }
  27.     /**
  28.      * 初始化一个新创建的 AjaxResult 对象
  29.      *
  30.      * @param code 状态码
  31.      * @param msg  返回内容
  32.      */
  33.     public AjaxResult(int code, String msg) {
  34.         super.put(CODE_TAG, code);
  35.         super.put(MSG_TAG, msg);
  36.     }
  37.     /**
  38.      * 初始化一个新创建的 AjaxResult 对象
  39.      *
  40.      * @param code 状态码
  41.      * @param msg  返回内容
  42.      * @param data 数据对象
  43.      */
  44.     public AjaxResult(int code, String msg, Object data) {
  45.         super.put(CODE_TAG, code);
  46.         super.put(MSG_TAG, msg);
  47.         if (data != null && !data.equals("")) {
  48.             super.put(DATA_TAG, data);
  49.         }
  50.     }
  51.     /**
  52.      * 返回成功消息
  53.      *
  54.      * @return 成功消息
  55.      */
  56.     public static AjaxResult success() {
  57.         return AjaxResult.success("操作成功");
  58.     }
  59.     /**
  60.      * 返回成功数据
  61.      *
  62.      * @return 成功消息
  63.      */
  64.     public static AjaxResult success(Object data) {
  65.         return AjaxResult.success("操作成功", data);
  66.     }
  67.     /**
  68.      * 返回成功消息
  69.      *
  70.      * @param msg 返回内容
  71.      * @return 成功消息
  72.      */
  73.     public static AjaxResult success(String msg) {
  74.         return AjaxResult.success(msg, null);
  75.     }
  76.     /**
  77.      * 返回成功消息
  78.      *
  79.      * @param msg  返回内容
  80.      * @param data 数据对象
  81.      * @return 成功消息
  82.      */
  83.     public static AjaxResult success(String msg, Object data) {
  84.         return new AjaxResult(200, msg, data);
  85.     }
  86.     /**
  87.      * 返回错误消息
  88.      *
  89.      * @return
  90.      */
  91.     public static AjaxResult error() {
  92.         return AjaxResult.error("操作失败");
  93.     }
  94.     /**
  95.      * 返回错误消息
  96.      *
  97.      * @param msg 返回内容
  98.      * @return 警告消息
  99.      */
  100.     public static AjaxResult error(String msg) {
  101.         return AjaxResult.error(msg, null);
  102.     }
  103.     /**
  104.      * 返回错误消息
  105.      *
  106.      * @param msg  返回内容
  107.      * @param data 数据对象
  108.      * @return 警告消息
  109.      */
  110.     public static AjaxResult error(String msg, Object data) {
  111.         return new AjaxResult(500, msg, data);
  112.     }
  113.     /**
  114.      * 返回错误消息
  115.      *
  116.      * @param code 状态码
  117.      * @param msg  返回内容
  118.      * @return 警告消息
  119.      */
  120.     public static AjaxResult error(int code, String msg) {
  121.         return new AjaxResult(code, msg, null);
  122.     }
  123. }
复制代码
2.4 滑动验证码工具类

此类是焦点类,紧张实现了图片的切割功能。
  1. package com.thcb.captchademo.captcha.utils;
  2. import com.thcb.captchademo.captcha.domain.Captcha;
  3. import javax.imageio.ImageIO;
  4. import java.awt.*;
  5. import java.awt.image.BufferedImage;
  6. import java.io.ByteArrayOutputStream;
  7. import java.io.File;
  8. import java.io.IOException;
  9. import java.net.URL;
  10. import java.util.Base64;
  11. import java.util.Objects;
  12. import java.util.Random;
  13. /**
  14. * 滑动验证码工具类
  15. *
  16. * @author thcb
  17. * @date 2023-05-25
  18. */
  19. public class CaptchaUtils {
  20.     /**
  21.      * 网络图片地址
  22.      **/
  23.     private final static String IMG_URL = "https://loyer.wang/view/ftp/wallpaper/%s.jpg";
  24.     /**
  25.      * 本地图片地址
  26.      **/
  27.     private final static String IMG_PATH = "E:\\caphcha\\%s.jpg";
  28.     /**
  29.      * 入参校验设置默认值
  30.      **/
  31.     public static void checkCaptcha(Captcha captcha) {
  32.         //设置画布宽度默认值
  33.         if (captcha.getCanvasWidth() == null) {
  34.             captcha.setCanvasWidth(320);
  35.         }
  36.         //设置画布高度默认值
  37.         if (captcha.getCanvasHeight() == null) {
  38.             captcha.setCanvasHeight(155);
  39.         }
  40.         //设置阻塞块宽度默认值
  41.         if (captcha.getBlockWidth() == null) {
  42.             captcha.setBlockWidth(65);
  43.         }
  44.         //设置阻塞块高度默认值
  45.         if (captcha.getBlockHeight() == null) {
  46.             captcha.setBlockHeight(55);
  47.         }
  48.         //设置阻塞块凹凸半径默认值
  49.         if (captcha.getBlockRadius() == null) {
  50.             captcha.setBlockRadius(9);
  51.         }
  52.         //设置图片来源默认值
  53.         if (captcha.getPlace() == null) {
  54.             captcha.setPlace(1);
  55.         }
  56.     }
  57.     /**
  58.      * 获取指定范围内的随机数
  59.      **/
  60.     public static int getNonceByRange(int start, int end) {
  61.         Random random = new Random();
  62.         return random.nextInt(end - start + 1) + start;
  63.     }
  64.     /**
  65.      * 获取验证码资源图
  66.      **/
  67.     public static BufferedImage getBufferedImage(Integer place) {
  68.         try {
  69.             //随机图片
  70.             //获取网络资源图片
  71.             if (0 == place) {
  72.                 int nonce = getNonceByRange(0, 1000);
  73.                 String imgUrl = String.format(IMG_URL, nonce);
  74.                 URL url = new URL(imgUrl);
  75.                 return ImageIO.read(url.openStream());
  76.             }
  77.             //获取本地图片
  78.             else {
  79.                 int nonce = getNonceByRange(0, 20);
  80.                 String imgPath = String.format(IMG_PATH, nonce);
  81.                 File file = new File(imgPath);
  82.                 return ImageIO.read(file);
  83.             }
  84.         } catch (Exception e) {
  85.             System.out.println("获取拼图资源失败");
  86.             //异常处理
  87.             return null;
  88.         }
  89.     }
  90.     /**
  91.      * 调整图片大小
  92.      **/
  93.     public static BufferedImage imageResize(BufferedImage bufferedImage, int width, int height) {
  94.         Image image = bufferedImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
  95.         BufferedImage resultImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
  96.         Graphics2D graphics2D = resultImage.createGraphics();
  97.         graphics2D.drawImage(image, 0, 0, null);
  98.         graphics2D.dispose();
  99.         return resultImage;
  100.     }
  101.     /**
  102.      * 抠图,并生成阻塞块
  103.      **/
  104.     public static void cutByTemplate(BufferedImage canvasImage, BufferedImage blockImage, int blockWidth, int blockHeight, int blockRadius, int blockX, int blockY) {
  105.         BufferedImage waterImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
  106.         //阻塞块的轮廓图
  107.         int[][] blockData = getBlockData(blockWidth, blockHeight, blockRadius);
  108.         //创建阻塞块具体形状
  109.         for (int i = 0; i < blockWidth; i++) {
  110.             for (int j = 0; j < blockHeight; j++) {
  111.                 try {
  112.                     //原图中对应位置变色处理
  113.                     if (blockData[i][j] == 1) {
  114.                         //背景设置为黑色
  115.                         waterImage.setRGB(i, j, Color.BLACK.getRGB());
  116.                         blockImage.setRGB(i, j, canvasImage.getRGB(blockX + i, blockY + j));
  117.                         //轮廓设置为白色,取带像素和无像素的界点,判断该点是不是临界轮廓点
  118.                         if (blockData[i + 1][j] == 0 || blockData[i][j + 1] == 0 || blockData[i - 1][j] == 0 || blockData[i][j - 1] == 0) {
  119.                             blockImage.setRGB(i, j, Color.WHITE.getRGB());
  120.                             waterImage.setRGB(i, j, Color.WHITE.getRGB());
  121.                         }
  122.                     }
  123.                     //这里把背景设为透明
  124.                     else {
  125.                         blockImage.setRGB(i, j, Color.TRANSLUCENT);
  126.                         waterImage.setRGB(i, j, Color.TRANSLUCENT);
  127.                     }
  128.                 } catch (ArrayIndexOutOfBoundsException e) {
  129.                     //防止数组下标越界异常
  130.                 }
  131.             }
  132.         }
  133.         //在画布上添加阻塞块水印
  134.         addBlockWatermark(canvasImage, waterImage, blockX, blockY);
  135.     }
  136.     /**
  137.      * 构建拼图轮廓轨迹
  138.      **/
  139.     private static int[][] getBlockData(int blockWidth, int blockHeight, int blockRadius) {
  140.         int[][] data = new int[blockWidth][blockHeight];
  141.         double po = Math.pow(blockRadius, 2);
  142.         //随机生成两个圆的坐标,在4个方向上 随机找到2个方向添加凸/凹
  143.         //凸/凹1
  144.         Random random1 = new Random();
  145.         int face1 = random1.nextInt(4);
  146.         //凸/凹2
  147.         int face2;
  148.         //保证两个凸/凹不在同一位置
  149.         do {
  150.             Random random2 = new Random();
  151.             face2 = random2.nextInt(4);
  152.         } while (face1 == face2);
  153.         //获取凸/凹起位置坐标
  154.         int[] circle1 = getCircleCoords(face1, blockWidth, blockHeight, blockRadius);
  155.         int[] circle2 = getCircleCoords(face2, blockWidth, blockHeight, blockRadius);
  156.         //随机凸/凹类型
  157.         int shape = getNonceByRange(0, 1);
  158.         //圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
  159.         //计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
  160.         for (int i = 0; i < blockWidth; i++) {
  161.             for (int j = 0; j < blockHeight; j++) {
  162.                 data[i][j] = 0;
  163.                 //创建中间的方形区域
  164.                 if ((i >= blockRadius && i <= blockWidth - blockRadius && j >= blockRadius && j <= blockHeight - blockRadius)) {
  165.                     data[i][j] = 1;
  166.                 }
  167.                 double d1 = Math.pow(i - Objects.requireNonNull(circle1)[0], 2) + Math.pow(j - circle1[1], 2);
  168.                 double d2 = Math.pow(i - Objects.requireNonNull(circle2)[0], 2) + Math.pow(j - circle2[1], 2);
  169.                 //创建两个凸/凹
  170.                 if (d1 <= po || d2 <= po) {
  171.                     data[i][j] = shape;
  172.                 }
  173.             }
  174.         }
  175.         return data;
  176.     }
  177.     /**
  178.      * 根据朝向获取圆心坐标
  179.      */
  180.     private static int[] getCircleCoords(int face, int blockWidth, int blockHeight, int blockRadius) {
  181.         //上
  182.         if (0 == face) {
  183.             return new int[]{blockWidth / 2 - 1, blockRadius};
  184.         }
  185.         //左
  186.         else if (1 == face) {
  187.             return new int[]{blockRadius, blockHeight / 2 - 1};
  188.         }
  189.         //下
  190.         else if (2 == face) {
  191.             return new int[]{blockWidth / 2 - 1, blockHeight - blockRadius - 1};
  192.         }
  193.         //右
  194.         else if (3 == face) {
  195.             return new int[]{blockWidth - blockRadius - 1, blockHeight / 2 - 1};
  196.         }
  197.         return null;
  198.     }
  199.     /**
  200.      * 在画布上添加阻塞块水印
  201.      */
  202.     private static void addBlockWatermark(BufferedImage canvasImage, BufferedImage blockImage, int x, int y) {
  203.         Graphics2D graphics2D = canvasImage.createGraphics();
  204.         graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f));
  205.         graphics2D.drawImage(blockImage, x, y, null);
  206.         graphics2D.dispose();
  207.     }
  208.     /**
  209.      * BufferedImage转BASE64
  210.      */
  211.     public static String toBase64(BufferedImage bufferedImage, String type) {
  212.         try {
  213.             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  214.             ImageIO.write(bufferedImage, type, byteArrayOutputStream);
  215.             String base64 = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
  216.             return String.format("data:image/%s;base64,%s", type, base64);
  217.         } catch (IOException e) {
  218.             System.out.println("图片资源转换BASE64失败");
  219.             //异常处理
  220.             return null;
  221.         }
  222.     }
  223. }
复制代码
2.5 滑动验证码Service

service层,,封装了工具类的一些方法,这里为了简单没写接口,正常应该是service和impl两个类。
  1. package com.thcb.captchademo.captcha.service;
  2. import com.thcb.captchademo.captcha.domain.Captcha;
  3. import com.thcb.captchademo.captcha.utils.CaptchaUtils;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.data.redis.core.StringRedisTemplate;
  6. import org.springframework.data.redis.core.ValueOperations;
  7. import org.springframework.stereotype.Service;
  8. import java.awt.image.BufferedImage;
  9. import java.util.UUID;
  10. import java.util.concurrent.TimeUnit;
  11. /**
  12. * 滑动验证码Service
  13. *
  14. * @author thcb
  15. * @date 2023-05-25
  16. */
  17. @Service
  18. public class CaptchaService {
  19.     /**
  20.      * 拼图验证码允许偏差
  21.      **/
  22.     private static Integer ALLOW_DEVIATION = 3;
  23.     @Autowired
  24.     private StringRedisTemplate stringRedisTemplate;
  25.     /**
  26.      * 校验验证码
  27.      *
  28.      * @param imageKey
  29.      * @param imageCode
  30.      * @return boolean
  31.      **/
  32.     public String checkImageCode(String imageKey, String imageCode) {
  33.         ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
  34.         String key = "imageCode:" + imageKey;
  35.         String text = ops.get(key);
  36.         if (text == null || text.equals("")) {
  37.             return "验证码已失效";
  38.         }
  39.         // 根据移动距离判断验证是否成功
  40.         if (Math.abs(Integer.parseInt(text) - Integer.parseInt(imageCode)) > ALLOW_DEVIATION) {
  41.             return "验证失败,请控制拼图对齐缺口";
  42.         }
  43.         // 验证成功,删除redis缓存
  44.         stringRedisTemplate.delete(key);
  45.         return null;
  46.     }
  47.     /**
  48.      * 缓存验证码,有效期1分钟
  49.      *
  50.      * @param key
  51.      * @param code
  52.      **/
  53.     public void saveImageCode(String key, String code) {
  54.         ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
  55.         ops.set("imageCode:" + key, code, 60, TimeUnit.SECONDS);
  56.     }
  57.     /**
  58.      * 获取验证码拼图(生成的抠图和带抠图阴影的大图及抠图坐标)
  59.      **/
  60.     public Object getCaptcha(Captcha captcha) {
  61.         //参数校验
  62.         CaptchaUtils.checkCaptcha(captcha);
  63.         //获取画布的宽高
  64.         int canvasWidth = captcha.getCanvasWidth();
  65.         int canvasHeight = captcha.getCanvasHeight();
  66.         //获取阻塞块的宽高/半径
  67.         int blockWidth = captcha.getBlockWidth();
  68.         int blockHeight = captcha.getBlockHeight();
  69.         int blockRadius = captcha.getBlockRadius();
  70.         //获取资源图
  71.         BufferedImage canvasImage = CaptchaUtils.getBufferedImage(captcha.getPlace());
  72.         //调整原图到指定大小
  73.         canvasImage = CaptchaUtils.imageResize(canvasImage, canvasWidth, canvasHeight);
  74.         //随机生成阻塞块坐标
  75.         int blockX = CaptchaUtils.getNonceByRange(blockWidth, canvasWidth - blockWidth - 10);
  76.         int blockY = CaptchaUtils.getNonceByRange(10, canvasHeight - blockHeight + 1);
  77.         //阻塞块
  78.         BufferedImage blockImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
  79.         //新建的图像根据轮廓图颜色赋值,源图生成遮罩
  80.         CaptchaUtils.cutByTemplate(canvasImage, blockImage, blockWidth, blockHeight, blockRadius, blockX, blockY);
  81.         // 移动横坐标
  82.         String nonceStr = UUID.randomUUID().toString().replaceAll("-", "");
  83.         // 缓存
  84.         saveImageCode(nonceStr, String.valueOf(blockX));
  85.         //设置返回参数
  86.         captcha.setNonceStr(nonceStr);
  87.         captcha.setBlockY(blockY);
  88.         captcha.setBlockSrc(CaptchaUtils.toBase64(blockImage, "png"));
  89.         captcha.setCanvasSrc(CaptchaUtils.toBase64(canvasImage, "png"));
  90.         return captcha;
  91.     }
  92. }
复制代码
2.6 滑动验证码Controller

controller层有两个方法,一个是获取验证码的方法,一个是验证码校验的方法。
  1. package com.thcb.captchademo.captcha.controller;
  2. import com.thcb.captchademo.captcha.domain.Captcha;
  3. import com.thcb.captchademo.captcha.domain.LoginVo;
  4. import com.thcb.captchademo.captcha.service.CaptchaService;
  5. import com.thcb.captchademo.captcha.utils.AjaxResult;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.web.bind.annotation.PostMapping;
  8. import org.springframework.web.bind.annotation.RequestBody;
  9. import org.springframework.web.bind.annotation.RequestMapping;
  10. import org.springframework.web.bind.annotation.RestController;
  11. /**
  12. * 滑动验证码Controller
  13. *
  14. * @author thcb
  15. * @date 2023-05-25
  16. */
  17. @RestController
  18. @RequestMapping("/captcha")
  19. public class CaptchaController {
  20.     @Autowired
  21.     private CaptchaService captchaService;
  22.     @PostMapping("/slideCaptchaImage")
  23.     public AjaxResult getCaptcha() {
  24.         return AjaxResult.success(captchaService.getCaptcha(new Captcha()));
  25.     }
  26.     @PostMapping(value = "/login")
  27.     public AjaxResult login(@RequestBody LoginVo loginVo) {
  28.         // 验证码校验
  29.         String msg = captchaService.checkImageCode(loginVo.getNonceStr(), loginVo.getValue());
  30.         if (msg != null && !msg.equals("")) {
  31.             return AjaxResult.error(msg);
  32.         }
  33.         return AjaxResult.success();
  34.     }
  35. }
复制代码
3 工程源码

工程不算复杂,源码上传到GitCode,项目所在,上传有点题目,而且现在只能设置成私密项目,等之后可以调解的时间再改成公开的。
4 总结

滑动验证码功能不算复杂,可以和项目当前已有的验证码共存,调用差异的接口,返回差异范例的验证码,固然这个就根据项目具体环境确定了。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表