spring 小红书 旋转圆形验证码,实现方案
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*/@SneakyThrowspublic 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*/@SneakyThrowspublic 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*/@SneakyThrowspublic 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实现


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
