马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
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 总结
滑动验证码功能不算复杂,可以和项目当前已有的验证码共存,调用差异的接口,返回差异范例的验证码,固然这个就根据项目具体环境确定了。
|