2021年实现 Java Vue uni-app 三端 滑动拼图验证码,两年前参考小红书实现了一版 滑动验证码,代码比较简陋,作为一个测试性的demo来说已经够了。
近期发现小红书更新了验证方式为 圆形旋转验证码,随即实现一波。
- 2021年实现方案的缺点
- 每次图形的生成 都是后台读取模板图和背景图,随机位置切图,返回base64图片编码,浪费系统资源。
- 切图方法仅实现了功能,纯依赖循环坐标覆盖色值, 不够优雅。
新版实现方案
-
背景图 600px * 400px。剪裁中心区域200px * 200px,4px留白。核心工具类 已提供
-
创建验证码管理模块,不断更新和改进滑块图像和背景图像增加破解的难度,背景图上传,后端进行切图。
-
保存至资源服务器。仅返回图片的资源访问地址。
-
前端UI设置参考 小红书实现。
效果如下:
核心切图工具类
依赖 Thumbnails
package io.github.smilexizheng.smileboot.common.utils.captcha;
import lombok.SneakyThrows;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
/**
* 旋转图形验证码
* 抗锯齿化,旋转图形不变形 等等
* 工具类
*
* @author smile
*/
public class CaptchaImageUtil {
private final static Logger log = LogManager.getLogger(CaptchaImageUtil.class);
/**
* 中心区域
*/
private static final int cropSize = 200;
/**
* 描边区域
*/
private static final int borderSize = 4;
/**
* 去图片的中心 为圆形
*
* @param is
* @return
*/
@SneakyThrows
public static BufferedImage cutCenterToCircle(InputStream is) {
BufferedImage originalImage = ImageIO.read(is);
int x = (originalImage.getWidth() - cropSize) / 2;
int y = (originalImage.getHeight() - cropSize) / 2;
BufferedImage croppedImage = Thumbnails.of(originalImage)
.sourceRegion(x, y, cropSize, cropSize)
.size(cropSize, cropSize)
.asBufferedImage();
BufferedImage resultImage = new BufferedImage(cropSize + borderSize * 2, cropSize + borderSize * 2, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = resultImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.WHITE);
g.fillOval(0, 0, cropSize + borderSize * 2, cropSize + borderSize * 2);
g.setComposite(AlphaComposite.SrcIn);
g.setClip(new Ellipse2D.Float(borderSize, borderSize, cropSize, cropSize));
g.drawImage(croppedImage, borderSize, borderSize, null);
g.dispose();
return resultImage;
}
/**
* 图片中心置为为黑色
*
* @param is
* @return
*/
public static BufferedImage centerToBlack(InputStream is) {
return centerToColor(is, Color.BLACK, false);
}
/**
* 将图片中心改成任意颜色
*
* @param is 流
* @param color 颜色
* @param alpha 完全透明
* @return
*/
@SneakyThrows
public static BufferedImage centerToColor(InputStream is, Color color, boolean alpha) {
BufferedImage originalImage = ImageIO.read(is);
int width = originalImage.getWidth();
int height = originalImage.getHeight();
int centerX = width / 2;
int centerY = height / 2;
BufferedImage resultImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = resultImage.createGraphics();
g.drawImage(originalImage, 0, 0, null);
int x = centerX - cropSize / 2;
int y = centerY - cropSize / 2;
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
// 设置抗锯齿渲染
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (alpha) {
// 设置透明度为0,即完全透明
g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0));
} else {
g.setColor(color);
}
g.fillOval(x, y, cropSize, cropSize);
g.dispose();
return resultImage;
}
/**
* 旋转图片
*
* @param originalImage
* @param angle
* @return
*/
@SneakyThrows
public static BufferedImage rotate(BufferedImage originalImage, double angle) {
int width = originalImage.getWidth();
int height = originalImage.getHeight();
double radians = Math.toRadians(angle);
AffineTransform at = new AffineTransform();
at.rotate(radians, width / 2.0, height / 2.0);
BufferedImage rotatedImage = new BufferedImage(width, height, originalImage.getType());
Graphics2D g = rotatedImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
// 设置抗锯齿渲染
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.drawImage(originalImage, at, null);
g.dispose();
return rotatedImage;
}
public static String toFile(BufferedImage bufferedImage, String fileName) throws Exception {
Assert.hasText(fileName, "输出路径不可为空!");
String path = new File(ResourceUtils.getURL("classpath:").getPath()).getAbsolutePath();
String s = "/static/" + fileName;
File file = new File(path, s);
Thumbnails.of(bufferedImage).scale(1).toFile(file);
return s;
}
public static String toFile(BufferedImage bufferedImage, String fileName, int width, int height) throws Exception {
Assert.hasText(fileName, "输出路径不可为空!");
String path = new File(ResourceUtils.getURL("classpath:").getPath()).getAbsolutePath();
String s = "/static/" + fileName;
File file = new File(path, s);
Thumbnails.of(bufferedImage).size(width, height).toFile(file);
return s;
}
}
切图效果:
有了这个核心工具切图,仅需要上传一张600*400背景图,后端循环随机生成 100-360度 多张中心图,都上传至静态服务器,将背景图片地址和角度数据保存到数据库中,项目启动时 将验证码加载到redis缓存中。
剩下的功能实现都比较简单了,具体实现 可以参考如下方案。
1.register 获取验证码,根据rid加密数据
1.根据请求头 进行MD5加密算法+随机UUID,作为Redis缓存key,返回背景图,旋转图的资源地址
2.check 校验,根据rid加密数据
1.根据滑动距离+时间,校验平均速度。
2.验证旋转角度。
前端 React实现