mirror of
https://github.com/dromara/tianai-captcha.git
synced 2026-05-18 06:14:04 +08:00
优化
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package cloud.tianai.captcha.generator;
|
||||
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/22 16:30
|
||||
* @Description 抽象的验证码生成器
|
||||
*/
|
||||
public abstract class AbstractImageCaptchaGenerator implements ImageCaptchaGenerator {
|
||||
public static String DEFAULT_BG_IMAGE_TYPE = "jpeg";
|
||||
public static String DEFAULT_SLIDER_IMAGE_TYPE = "png";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
/** 默认背景图片类型. */
|
||||
public String defaultBgImageType = DEFAULT_BG_IMAGE_TYPE;
|
||||
@Getter
|
||||
@Setter
|
||||
/** 默认滑块图片类型. */
|
||||
public String defaultSliderImageType = DEFAULT_SLIDER_IMAGE_TYPE;
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(String type) {
|
||||
return generateCaptchaImage(type, defaultBgImageType, defaultSliderImageType);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(String type, String backgroundFormatName, String sliderFormatName) {
|
||||
return generateCaptchaImage(GenerateParam.builder()
|
||||
.type(type)
|
||||
.backgroundFormatName(backgroundFormatName)
|
||||
.sliderFormatName(sliderFormatName)
|
||||
.obfuscate(false)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片转换成字符串格式
|
||||
*
|
||||
* @param bufferedImage 图片
|
||||
* @param formatType 格式化类型
|
||||
* @return String
|
||||
*/
|
||||
@SneakyThrows(IOException.class)
|
||||
public String transform(BufferedImage bufferedImage, String formatType) {
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
ImageIO.write(bufferedImage, formatType, byteArrayOutputStream);
|
||||
//转换成字节码
|
||||
byte[] data = byteArrayOutputStream.toByteArray();
|
||||
String base64 = Base64.getEncoder().encodeToString(data);
|
||||
return "data:image/" + formatType + ";base64,".concat(base64);
|
||||
}
|
||||
|
||||
protected InputStream getTemplateFile(Map<String, Resource> templateImages, String imageName) {
|
||||
Resource resource = templateImages.get(imageName);
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException("查找模板异常, 该模板下未找到 ".concat(imageName));
|
||||
}
|
||||
return getImageResourceManager().getResourceInputStream(resource);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cloud.tianai.captcha.generator;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2020/10/19 18:37
|
||||
* @Description 图片验证码生成器
|
||||
*/
|
||||
public interface ImageCaptchaGenerator {
|
||||
|
||||
|
||||
/**
|
||||
* 生成验证码图片
|
||||
*
|
||||
* @param type 类型 {@link CaptchaTypeConstant}
|
||||
* @return SliderCaptchaInfo
|
||||
*/
|
||||
ImageCaptchaInfo generateCaptchaImage(String type);
|
||||
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*
|
||||
* @param type type {@link CaptchaTypeConstant}
|
||||
* @param targetFormatName jpeg或者webp格式
|
||||
* @param matrixFormatName png或者webp格式
|
||||
* @return SliderCaptchaInfo
|
||||
*/
|
||||
ImageCaptchaInfo generateCaptchaImage(String type, String targetFormatName, String matrixFormatName);
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*
|
||||
* @param param 生成参数
|
||||
* @return SliderCaptchaInfo
|
||||
*/
|
||||
ImageCaptchaInfo generateCaptchaImage(GenerateParam param);
|
||||
|
||||
|
||||
/**
|
||||
* 获取滑块验证码资源管理器
|
||||
*
|
||||
* @return SliderCaptchaResourceManager
|
||||
*/
|
||||
ImageCaptchaResourceManager getImageResourceManager();
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package cloud.tianai.captcha.generator.common.constant;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 17:14
|
||||
* @Description 滑块验证码常量
|
||||
*/
|
||||
public interface SliderCaptchaConstant {
|
||||
|
||||
/** 模板滑块固定名称. */
|
||||
String TEMPLATE_ACTIVE_IMAGE_NAME = "active.png";
|
||||
/** 模板凹槽固定名称. */
|
||||
String TEMPLATE_FIXED_IMAGE_NAME = "fixed.png";
|
||||
/** 模板背景固定名称. */
|
||||
String TEMPLATE_MATRIX_IMAGE_NAME = "matrix.png";
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/28 16:51
|
||||
* @Description 点击图片校验描述
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ClickImageCheckDefinition {
|
||||
/** 提示.*/
|
||||
private String tip;
|
||||
/** x.*/
|
||||
private Integer x;
|
||||
/** y.*/
|
||||
private Integer y;
|
||||
/** 宽.*/
|
||||
private Integer width;
|
||||
/** 高.*/
|
||||
private Integer height;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/11 9:44
|
||||
* @Description 生成参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class GenerateParam {
|
||||
/** 背景格式化名称.*/
|
||||
private String backgroundFormatName = "jpeg";
|
||||
/** 滑块格式化名称.*/
|
||||
private String sliderFormatName = "png";
|
||||
/** 是否混淆.*/
|
||||
private Boolean obfuscate = false;
|
||||
/** 类型.*/
|
||||
private String type = CaptchaTypeConstant.SLIDER;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @Date 2020/5/29 8:04
|
||||
* @Description 滑块验证码
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ImageCaptchaInfo {
|
||||
|
||||
/**
|
||||
* 背景图
|
||||
*/
|
||||
private String backgroundImage;
|
||||
/**
|
||||
* 移动图
|
||||
*/
|
||||
private String sliderImage;
|
||||
/** 背景图片宽度. */
|
||||
private Integer bgImageWidth;
|
||||
/** 背景图片高度. */
|
||||
private Integer bgImageHeight;
|
||||
/** 滑块图片宽度. */
|
||||
private Integer sliderImageWidth;
|
||||
/** 滑块图片高度. */
|
||||
private Integer sliderImageHeight;
|
||||
/** 随机值. */
|
||||
private Integer randomX;
|
||||
/** 容错值, 可以为空 默认 0.02容错,校验的时候用. */
|
||||
private Float tolerant;
|
||||
/** 验证码类型. */
|
||||
private String type;
|
||||
/** 透传字段,用于传给前端.*/
|
||||
private Object data;
|
||||
/**
|
||||
* 扩展字段
|
||||
*/
|
||||
public Object expand;
|
||||
|
||||
public ImageCaptchaInfo(String backgroundImage,
|
||||
String sliderImage,
|
||||
Integer bgImageWidth,
|
||||
Integer bgImageHeight,
|
||||
Integer sliderImageWidth,
|
||||
Integer sliderImageHeight,
|
||||
Integer randomX,
|
||||
String type) {
|
||||
this.backgroundImage = backgroundImage;
|
||||
this.sliderImage = sliderImage;
|
||||
this.bgImageWidth = bgImageWidth;
|
||||
this.bgImageHeight = bgImageHeight;
|
||||
this.sliderImageWidth = sliderImageWidth;
|
||||
this.sliderImageHeight = sliderImageHeight;
|
||||
this.randomX = randomX;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static ImageCaptchaInfo of(String backgroundImage,
|
||||
String sliderImage,
|
||||
Integer bgImageWidth,
|
||||
Integer bgImageHeight,
|
||||
Integer sliderImageWidth,
|
||||
Integer sliderImageHeight,
|
||||
Integer randomKey,
|
||||
String type) {
|
||||
return new ImageCaptchaInfo(backgroundImage, sliderImage, bgImageWidth, bgImageHeight, sliderImageWidth, sliderImageHeight, randomKey, type);
|
||||
}
|
||||
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/22 15:49
|
||||
* @Description 旋转图片
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class RotateImageCaptchaInfo extends ImageCaptchaInfo {
|
||||
/**
|
||||
* 旋转多少度
|
||||
*/
|
||||
private Double degree;
|
||||
/** 旋转图片的容错值大一点. */
|
||||
public static final Float DEFAULT_TOLERANT = 0.03F;
|
||||
|
||||
public static RotateImageCaptchaInfo of(Double degree,
|
||||
Integer randomX,
|
||||
String backgroundImage,
|
||||
String sliderImage,
|
||||
Integer bgImageWidth,
|
||||
Integer bgImageHeight,
|
||||
Integer sliderImageWidth,
|
||||
Integer sliderImageHeight) {
|
||||
RotateImageCaptchaInfo rotateImageCaptchaInfo = new RotateImageCaptchaInfo();
|
||||
rotateImageCaptchaInfo.setDegree(degree);
|
||||
rotateImageCaptchaInfo.setRandomX(randomX);
|
||||
rotateImageCaptchaInfo.setBackgroundImage(backgroundImage);
|
||||
rotateImageCaptchaInfo.setTolerant(DEFAULT_TOLERANT);
|
||||
rotateImageCaptchaInfo.setSliderImage(sliderImage);
|
||||
rotateImageCaptchaInfo.setBgImageWidth(bgImageWidth);
|
||||
rotateImageCaptchaInfo.setBgImageHeight(bgImageHeight);
|
||||
rotateImageCaptchaInfo.setSliderImageWidth(sliderImageWidth);
|
||||
rotateImageCaptchaInfo.setSliderImageHeight(sliderImageHeight);
|
||||
// 类型为旋转图片验证码
|
||||
rotateImageCaptchaInfo.setType(CaptchaTypeConstant.ROTATE);
|
||||
return rotateImageCaptchaInfo;
|
||||
}
|
||||
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class SliderImageCaptchaInfo extends ImageCaptchaInfo {
|
||||
/**
|
||||
* x轴
|
||||
*/
|
||||
private Integer x;
|
||||
/**
|
||||
* y轴
|
||||
*/
|
||||
private Integer y;
|
||||
|
||||
|
||||
public static SliderImageCaptchaInfo of(Integer x,
|
||||
Integer y,
|
||||
String backgroundImage,
|
||||
String sliderImage,
|
||||
Integer bgImageWidth,
|
||||
Integer bgImageHeight,
|
||||
Integer sliderImageWidth,
|
||||
Integer sliderImageHeight) {
|
||||
SliderImageCaptchaInfo sliderImageCaptchaInfo = new SliderImageCaptchaInfo();
|
||||
sliderImageCaptchaInfo.setX(x);
|
||||
sliderImageCaptchaInfo.setY(y);
|
||||
sliderImageCaptchaInfo.setRandomX(x);
|
||||
sliderImageCaptchaInfo.setBackgroundImage(backgroundImage);
|
||||
sliderImageCaptchaInfo.setSliderImage(sliderImage);
|
||||
sliderImageCaptchaInfo.setBgImageWidth(bgImageWidth);
|
||||
sliderImageCaptchaInfo.setBgImageHeight(bgImageHeight);
|
||||
sliderImageCaptchaInfo.setSliderImageWidth(sliderImageWidth);
|
||||
sliderImageCaptchaInfo.setSliderImageHeight(sliderImageHeight);
|
||||
sliderImageCaptchaInfo.setType(CaptchaTypeConstant.SLIDER);
|
||||
return sliderImageCaptchaInfo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
package cloud.tianai.captcha.generator.common.util;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import sun.font.FontDesignMetrics;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.CubicCurve2D;
|
||||
import java.awt.geom.QuadCurve2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.PixelGrabber;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/16 9:46
|
||||
* @Description image Utils
|
||||
*/
|
||||
public class CaptchaImageUtils {
|
||||
|
||||
@SneakyThrows
|
||||
public static BufferedImage wrapFile2BufferedImage(URL resourceImage) {
|
||||
if (resourceImage == null) {
|
||||
throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空");
|
||||
}
|
||||
// 关闭磁盘缓存
|
||||
ImageIO.setUseCache(false);
|
||||
return ImageIO.read(resourceImage);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static BufferedImage wrapFile2BufferedImage(InputStream resource) {
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空");
|
||||
}
|
||||
// 关闭磁盘缓存
|
||||
ImageIO.setUseCache(false);
|
||||
return ImageIO.read(resource);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 图片覆盖(覆盖图压缩到width*height大小,覆盖到底图上)
|
||||
*
|
||||
* @param baseBufferedImage 底图
|
||||
* @param coverBufferedImage 覆盖图
|
||||
* @param x 起始x轴
|
||||
* @param y 起始y轴
|
||||
*/
|
||||
public static void overlayImage(BufferedImage baseBufferedImage, BufferedImage coverBufferedImage,
|
||||
int x, int y) {
|
||||
// 创建Graphics2D对象,用在底图对象上绘图
|
||||
Graphics2D g2d = baseBufferedImage.createGraphics();
|
||||
// 绘制
|
||||
g2d.drawImage(coverBufferedImage, x, y, coverBufferedImage.getWidth(), coverBufferedImage.getHeight(), null);
|
||||
// 释放图形上下文使用的系统资源
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Image图像中的透明/不透明部分转换为Shape图形
|
||||
*
|
||||
* @param img 图片信息
|
||||
* @param transparent 是否透明
|
||||
* @return Shape
|
||||
* @throws InterruptedException 异常
|
||||
*/
|
||||
public static Shape getImageShape(Image img, boolean transparent) throws InterruptedException {
|
||||
ArrayList<Integer> x = new ArrayList<>();
|
||||
ArrayList<Integer> y = new ArrayList<>();
|
||||
int width = img.getWidth(null);
|
||||
int height = img.getHeight(null);
|
||||
|
||||
// 首先获取图像所有的像素信息
|
||||
PixelGrabber pgr = new PixelGrabber(img, 0, 0, -1, -1, true);
|
||||
pgr.grabPixels();
|
||||
int[] pixels = (int[]) pgr.getPixels();
|
||||
|
||||
// 循环像素
|
||||
for (int i = 0; i < pixels.length; i++) {
|
||||
// 筛选,将不透明的像素的坐标加入到坐标ArrayList x和y中
|
||||
int alpha = (pixels[i] >> 24) & 0xff;
|
||||
if (alpha != 0) {
|
||||
x.add(i % width > 0 ? i % width - 1 : 0);
|
||||
y.add(i % width == 0 ? (i == 0 ? 0 : i / width - 1) : i / width);
|
||||
}
|
||||
}
|
||||
|
||||
// 建立图像矩阵并初始化(0为透明,1为不透明)
|
||||
int[][] matrix = new int[height][width];
|
||||
for (int i = 0; i < height; i++) {
|
||||
for (int j = 0; j < width; j++) {
|
||||
matrix[i][j] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 导入坐标ArrayList中的不透明坐标信息
|
||||
for (int c = 0; c < x.size(); c++) {
|
||||
matrix[y.get(c)][x.get(c)] = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 逐一水平"扫描"图像矩阵的每一行,将透明(这里也可以取不透明的)的像素生成为Rectangle,
|
||||
* 再将每一行的Rectangle通过Area类的rec对象进行合并, 最后形成一个完整的Shape图形
|
||||
*/
|
||||
Area rec = new Area();
|
||||
int temp = 0;
|
||||
//生成Shape时是1取透明区域还是取非透明区域的flag
|
||||
int flag = transparent ? 0 : 1;
|
||||
|
||||
for (int i = 0; i < height; i++) {
|
||||
for (int j = 0; j < width; j++) {
|
||||
if (matrix[i][j] == flag) {
|
||||
if (temp == 0) {
|
||||
temp = j;
|
||||
}
|
||||
} else {
|
||||
if (temp != 0) {
|
||||
rec.add(new Area(new Rectangle(temp, i, j - temp, 1)));
|
||||
temp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
temp = 0;
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度拷贝图片
|
||||
*
|
||||
* @param bi 原图片
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage deepCopyBufferedImage(BufferedImage bi) {
|
||||
ColorModel cm = bi.getColorModel();
|
||||
boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
|
||||
WritableRaster raster = bi.copyData(bi.getRaster().createCompatibleWritableRaster());
|
||||
return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过模板图片抠图(不透明部分)
|
||||
*
|
||||
* @param origin 源图片
|
||||
* @param template 模板图片
|
||||
* @param x 坐标轴x
|
||||
* @param y 坐标轴y
|
||||
* @return BufferedImage
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static BufferedImage cutImage(BufferedImage origin, BufferedImage template, int x, int y) {
|
||||
int bw = template.getWidth(null);
|
||||
int bh = template.getHeight(null);
|
||||
int lw = origin.getWidth(null);
|
||||
int lh = origin.getHeight(null);
|
||||
//得到透明的区域(人物轮廓)
|
||||
Shape imageShape = getImageShape(template, false);
|
||||
//合成后的图片
|
||||
BufferedImage image = new BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
//设置画布为透明
|
||||
image = graphics.getDeviceConfiguration().createCompatibleImage(bw, bh, Transparency.TRANSLUCENT);
|
||||
graphics.dispose();
|
||||
Graphics2D graphics2 = image.createGraphics();
|
||||
//取交集(限制可以画的范围为shape的范围)
|
||||
graphics2.clip(imageShape);
|
||||
//抗锯齿
|
||||
graphics2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
graphics2.setStroke(new BasicStroke(5, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
|
||||
graphics2.drawImage(origin, -x, -y, lw, lh, null);
|
||||
graphics2.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
public static BufferedImage rotateImage(final BufferedImage bufferedimage,
|
||||
final double degree) {
|
||||
// 得到图片宽度。
|
||||
int w = bufferedimage.getWidth();
|
||||
// 得到图片高度。
|
||||
int h = bufferedimage.getHeight();
|
||||
// 得到图片透明度。
|
||||
int type = bufferedimage.getColorModel().getTransparency();
|
||||
BufferedImage img;// 空的图片。
|
||||
Graphics2D graphics2d;// 空的画笔。
|
||||
(graphics2d = (img = new BufferedImage(w, h, type))
|
||||
.createGraphics()).setRenderingHint(
|
||||
RenderingHints.KEY_INTERPOLATION,
|
||||
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
// 旋转,degree是整型,度数,比如垂直90度。
|
||||
graphics2d.rotate(Math.toRadians(degree), w / 2, h / 2);
|
||||
// 从bufferedimagecopy图片至img,0,0是img的坐标。
|
||||
graphics2d.drawImage(bufferedimage, 0, 0, null);
|
||||
graphics2d.dispose();
|
||||
// 返回复制好的图片,原图片依然没有变,没有旋转,下次还可以使用。
|
||||
return img;
|
||||
}
|
||||
|
||||
public static void centerOverlayAndRotateImage(BufferedImage baseBufferedImage, BufferedImage coverBufferedImage,
|
||||
final double degree) {
|
||||
coverBufferedImage = rotateImage(coverBufferedImage, degree);
|
||||
int bw = baseBufferedImage.getWidth();
|
||||
int bh = baseBufferedImage.getHeight();
|
||||
int cw = coverBufferedImage.getWidth();
|
||||
int ch = coverBufferedImage.getHeight();
|
||||
overlayImage(baseBufferedImage, coverBufferedImage, bw / 2 - cw / 2, bh / 2 - ch / 2);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过x和y轴截取图片
|
||||
*
|
||||
* @param x x
|
||||
* @param y y
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
* @param img 截取的图片
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage subImage(int x, int y, int width, int height, BufferedImage img) {
|
||||
int[] simgRgb = new int[width * height];
|
||||
img.getRGB(x, y, width, height, simgRgb, 0, width);
|
||||
// 得到图片透明度。
|
||||
int type = img.getColorModel().getTransparency();
|
||||
BufferedImage newImage = new BufferedImage(width, height, type);
|
||||
newImage.setRGB(0, 0, width, height, simgRgb, 0, width);
|
||||
return newImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分隔图片
|
||||
*
|
||||
* @param pos 分隔点
|
||||
* @param direction true为水平方向, false为垂直方向
|
||||
* @param img 待分割的图片
|
||||
* @return BufferedImage[]
|
||||
*/
|
||||
public static BufferedImage[] splitImage(int pos, boolean direction, BufferedImage img) {
|
||||
int startImageWidth;
|
||||
int startImageHeight;
|
||||
int endImageWidth;
|
||||
int endImageHeight;
|
||||
int endScanX;
|
||||
int endScanY;
|
||||
if (direction) {
|
||||
startImageHeight = img.getHeight() - pos;
|
||||
startImageWidth = img.getWidth();
|
||||
endImageWidth = img.getWidth();
|
||||
endImageHeight = pos;
|
||||
endScanX = 0;
|
||||
endScanY = startImageHeight;
|
||||
} else {
|
||||
startImageWidth = pos;
|
||||
startImageHeight = img.getHeight();
|
||||
endImageWidth = img.getWidth() - startImageWidth;
|
||||
endImageHeight = img.getHeight();
|
||||
endScanX = pos;
|
||||
endScanY = 0;
|
||||
}
|
||||
|
||||
// start
|
||||
int[] rgbArr = new int[startImageWidth * startImageHeight];
|
||||
img.getRGB(0, 0, startImageWidth, startImageHeight, rgbArr, 0, startImageWidth);
|
||||
int type = img.getColorModel().getTransparency();
|
||||
BufferedImage startImg = new BufferedImage(startImageWidth, startImageHeight, type);
|
||||
startImg.setRGB(0, 0, startImageWidth, startImageHeight, rgbArr, 0, startImageWidth);
|
||||
// end
|
||||
rgbArr = new int[endImageWidth * endImageHeight];
|
||||
img.getRGB(endScanX, endScanY, endImageWidth, endImageHeight, rgbArr, 0, endImageWidth);
|
||||
BufferedImage endImg = new BufferedImage(endImageWidth, endImageHeight, type);
|
||||
endImg.setRGB(0, 0, endImageWidth, endImageHeight, rgbArr, 0, endImageWidth);
|
||||
|
||||
BufferedImage[] splitImageArr = new BufferedImage[2];
|
||||
splitImageArr[0] = startImg;
|
||||
splitImageArr[1] = endImg;
|
||||
return splitImageArr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 拼接图片
|
||||
*
|
||||
* @param direction rue为水平方向, false为垂直方向
|
||||
* @param width 拼接后图片宽度
|
||||
* @param height 拼接后图片高度
|
||||
* @param imgArr 拼接的图片数组
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage concatImage(boolean direction, int width, int height, BufferedImage... imgArr) {
|
||||
int pos = 0;
|
||||
BufferedImage newImage = new BufferedImage(width, height, imgArr[0].getColorModel().getTransparency());
|
||||
for (BufferedImage img : imgArr) {
|
||||
int[] rgbArr = new int[width * height];
|
||||
img.getRGB(0, 0, img.getWidth(), img.getHeight(), rgbArr, 0, img.getWidth());
|
||||
if (direction) {
|
||||
newImage.setRGB(pos, 0, img.getWidth(), img.getHeight(), rgbArr, 0, img.getWidth());
|
||||
pos += img.getWidth();
|
||||
// 水平方向
|
||||
} else {
|
||||
// 垂直方向
|
||||
newImage.setRGB(0, pos, img.getWidth(), img.getHeight(), rgbArr, 0, img.getWidth());
|
||||
pos += img.getHeight();
|
||||
}
|
||||
}
|
||||
return newImage;
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
char randomChar = getRandomChar();
|
||||
System.out.println(randomChar);
|
||||
}
|
||||
|
||||
public static char getRandomChar() {
|
||||
return (char) (0x4e00 + (int) (Math.random() * (0x9fa5 - 0x4e00 + 1)));
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public static BufferedImage drawWordImg(Color fontColor,
|
||||
String word,
|
||||
Font font,
|
||||
FontDesignMetrics metrics,
|
||||
int imgWidth,
|
||||
int imgHeight,
|
||||
float deg) {
|
||||
BufferedImage fillRect = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = fillRect.createGraphics();
|
||||
g.setColor(new Color(255, 255, 255, 0));
|
||||
g.fillRect(0, 0, imgWidth, imgHeight);
|
||||
g.setColor(fontColor);
|
||||
g.setFont(font);
|
||||
float left = (imgWidth - font.getSize()) / 2f;
|
||||
float top = (imgHeight - font.getSize()) / 2f + metrics.getAscent() - 6;
|
||||
g.rotate(Math.toRadians(deg), imgWidth / 2f, imgHeight / 2f);
|
||||
g.drawString(word, left, top);
|
||||
g.dispose();
|
||||
return fillRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机画干扰圆
|
||||
*
|
||||
* @param num 数量
|
||||
* @param color 颜色
|
||||
* @param g Graphics2D
|
||||
*/
|
||||
public static void drawOval(int num,
|
||||
Color color,
|
||||
Graphics2D g,
|
||||
int width,
|
||||
int height,
|
||||
Random random) {
|
||||
for (int i = 0; i < num; i++) {
|
||||
g.setColor(color == null ? getRandomColor(random) : color);
|
||||
int w = 5 + random.nextInt(10);
|
||||
int x = random.nextInt(width - 25);
|
||||
int y = random.nextInt(height - 25);
|
||||
g.drawOval(x, y, w, w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 随机画贝塞尔曲线
|
||||
*
|
||||
* @param num 数量
|
||||
* @param color 颜色
|
||||
* @param g Graphics2D
|
||||
*/
|
||||
public static void drawBesselLine(int num, Color color,
|
||||
Graphics2D g,
|
||||
int width,
|
||||
int height,
|
||||
ThreadLocalRandom random) {
|
||||
for (int i = 0; i < num; i++) {
|
||||
g.setColor(color == null ? getRandomColor(random) : color);
|
||||
int x1 = 5, y1 = random.nextInt(5, height / 2);
|
||||
int x2 = width - 5, y2 = random.nextInt(height / 2, height - 5);
|
||||
int ctrlx = random.nextInt(width / 4, width / 4 * 3);
|
||||
int ctrly = random.nextInt(5, height - 5);
|
||||
if (random.nextInt(2) == 0) {
|
||||
int ty = y1;
|
||||
y1 = y2;
|
||||
y2 = ty;
|
||||
}
|
||||
// 二阶贝塞尔曲线
|
||||
if (random.nextInt(2) == 0) {
|
||||
QuadCurve2D shape = new QuadCurve2D.Double();
|
||||
shape.setCurve(x1, y1, ctrlx, ctrly, x2, y2);
|
||||
g.draw(shape);
|
||||
} else { // 三阶贝塞尔曲线
|
||||
int ctrlx1 = random.nextInt(width / 4, width / 4 * 3);
|
||||
int ctrly1 = random.nextInt(5, height - 5);
|
||||
CubicCurve2D shape = new CubicCurve2D.Double(x1, y1, ctrlx, ctrly, ctrlx1, ctrly1, x2, y2);
|
||||
g.draw(shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成简单的验证码图片
|
||||
*
|
||||
* @param data 验证码内容
|
||||
* @param font 字体包
|
||||
* @param metrics FontDesignMetrics
|
||||
* @param width 验证码宽度
|
||||
* @param height 验证码高度
|
||||
* @param startX 起始X
|
||||
* @param startY 起始Y
|
||||
* @param interferenceLineNum 干扰线数量
|
||||
* @param interferencePointNum 干扰点数量
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage genSimpleImgCaptcha(String data,
|
||||
Font font,
|
||||
FontDesignMetrics metrics,
|
||||
int width,
|
||||
int height,
|
||||
float startX,
|
||||
float startY,
|
||||
int interferenceLineNum,
|
||||
int interferencePointNum) {
|
||||
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = bufferedImage.createGraphics();
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
g.setFont(font);
|
||||
char[] chars = data.toCharArray();
|
||||
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
g.setColor(getRandomColor(random));
|
||||
g.drawString(String.valueOf(chars[i]), startX + i * font.getSize(), startY);
|
||||
}
|
||||
// 干扰点
|
||||
if (interferencePointNum > 0) {
|
||||
drawOval(interferencePointNum, null, g, width, height, random);
|
||||
}
|
||||
if (interferencePointNum > 0) {
|
||||
g.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
|
||||
// 干扰线
|
||||
drawBesselLine(interferenceLineNum, null, g, width, height, random);
|
||||
}
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机获取颜色
|
||||
*
|
||||
* @return Color
|
||||
*/
|
||||
public static Color getRandomColor(Random random) {
|
||||
return new Color(
|
||||
random.nextInt(255),
|
||||
random.nextInt(255),
|
||||
random.nextInt(255));
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ClickImageCheckDefinition;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ClickImageCheckDefinition;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import lombok.*;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import static cloud.tianai.captcha.generator.common.util.CaptchaImageUtils.wrapFile2BufferedImage;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/27 11:46
|
||||
* @Description 点选验证码 点选验证码分为点选文字和点选图标等
|
||||
*/
|
||||
public abstract class AbstractClickImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
/** 参与校验的数量. */
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer checkClickCount = 4;
|
||||
/** 干扰数量. */
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer interferenceCount = 2;
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
// 文字点选验证码不需要模板 只需要背景图
|
||||
Collection<InputStream> inputStreams = new LinkedList<>();
|
||||
try {
|
||||
Resource resourceImage = getImageResourceManager().randomGetResource(param.getType());
|
||||
InputStream resourceInputStream = getImageResourceManager().getResourceInputStream(resourceImage);
|
||||
inputStreams.add(resourceInputStream);
|
||||
BufferedImage bgImage = CaptchaImageUtils.wrapFile2BufferedImage(resourceInputStream);
|
||||
|
||||
List<ClickImageCheckDefinition> clickImageCheckDefinitionList = new ArrayList<>(interferenceCount);
|
||||
int allImages = interferenceCount + checkClickCount;
|
||||
int avg = bgImage.getWidth() / allImages;
|
||||
for (int i = 0; i < allImages; i++) {
|
||||
// 随机获取点击图片
|
||||
ImgWrapper imgWrapper = randomGetClickImg();
|
||||
BufferedImage image = imgWrapper.getImage();
|
||||
int clickImgWidth = image.getWidth();
|
||||
int clickImgHeight = image.getHeight();
|
||||
// 随机x
|
||||
int randomX;
|
||||
if (i == 0) {
|
||||
randomX = 1;
|
||||
} else {
|
||||
randomX = avg * i;
|
||||
}
|
||||
// 随机y
|
||||
int randomY = ThreadLocalRandom.current().nextInt(10, bgImage.getHeight() - clickImgHeight);
|
||||
// 通过随机x和y 进行覆盖图片
|
||||
CaptchaImageUtils.overlayImage(bgImage, imgWrapper.getImage(), randomX, randomY);
|
||||
ClickImageCheckDefinition clickImageCheckDefinition = new ClickImageCheckDefinition();
|
||||
clickImageCheckDefinition.setTip(imgWrapper.getTip());
|
||||
clickImageCheckDefinition.setX(randomX + clickImgWidth / 2);
|
||||
clickImageCheckDefinition.setY(randomY + clickImgHeight / 2);
|
||||
clickImageCheckDefinition.setWidth(clickImgWidth);
|
||||
clickImageCheckDefinition.setHeight(clickImgHeight);
|
||||
clickImageCheckDefinitionList.add(clickImageCheckDefinition);
|
||||
}
|
||||
// 打乱
|
||||
Collections.shuffle(clickImageCheckDefinitionList);
|
||||
// 拿出参与校验的数据
|
||||
List<ClickImageCheckDefinition> checkClickImageCheckDefinitionList = new ArrayList<>(checkClickCount);
|
||||
for (int i = 0; i < checkClickCount; i++) {
|
||||
ClickImageCheckDefinition clickImageCheckDefinition = clickImageCheckDefinitionList.get(i);
|
||||
checkClickImageCheckDefinitionList.add(clickImageCheckDefinition);
|
||||
}
|
||||
// 将校验的文字生成提示图片
|
||||
ImgWrapper tipImage = genTipImage(checkClickImageCheckDefinitionList);
|
||||
return wrapClickImageCaptchaInfo(param, bgImage, tipImage.getImage(), checkClickImageCheckDefinitionList);
|
||||
|
||||
} finally {
|
||||
// 使用完后关闭流
|
||||
for (InputStream inputStream : inputStreams) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 随机获取点击的图片
|
||||
*
|
||||
* @return ImgWrapper
|
||||
*/
|
||||
public abstract ImgWrapper randomGetClickImg();
|
||||
|
||||
/**
|
||||
* 生成 tip 图片
|
||||
*
|
||||
* @param imageCheckDefinitions imageCheckDefinitions
|
||||
* @return ImgWrapper
|
||||
*/
|
||||
public abstract ImgWrapper genTipImage(List<ClickImageCheckDefinition> imageCheckDefinitions);
|
||||
|
||||
/**
|
||||
* 包装 ImageCaptchaInfo
|
||||
*
|
||||
* @param param param
|
||||
* @param bgImage bgImage
|
||||
* @param tipImage tipImage
|
||||
* @param checkClickImageCheckDefinitionList checkClickImageCheckDefinitionList
|
||||
* @return ImageCaptchaInfo
|
||||
*/
|
||||
public abstract ImageCaptchaInfo wrapClickImageCaptchaInfo(GenerateParam param, BufferedImage bgImage,
|
||||
BufferedImage tipImage,
|
||||
List<ClickImageCheckDefinition> checkClickImageCheckDefinitionList);
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/28 14:26
|
||||
* @Description 点击图片包装
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ImgWrapper {
|
||||
/** 图片. */
|
||||
private BufferedImage image;
|
||||
/** 提示. */
|
||||
private String tip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.util.NamedThreadFactory;
|
||||
import cloud.tianai.captcha.common.util.NamedThreadFactory;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2020/10/20 9:23
|
||||
* @Description 滑块验证码缓冲器
|
||||
*/
|
||||
@Slf4j
|
||||
public class CacheImageCaptchaGenerator implements ImageCaptchaGenerator {
|
||||
|
||||
protected final ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("slider-captcha-queue"));
|
||||
protected Map<GenerateParam, ConcurrentLinkedQueue<ImageCaptchaInfo>> queueMap = new ConcurrentHashMap<>(8);
|
||||
protected Map<GenerateParam, AtomicInteger> posMap = new ConcurrentHashMap<>(8);
|
||||
protected Map<GenerateParam, Long> lastUpdateMap = new ConcurrentHashMap<>(8);
|
||||
protected ImageCaptchaGenerator target;
|
||||
protected int size;
|
||||
/** 等待时间,一般报错或者拉取为空时会休眠一段时间再试. */
|
||||
protected int waitTime = 1000;
|
||||
/** 调度器检查缓存的间隔时间. */
|
||||
protected int period = 5000;
|
||||
/** 10天内没有任何操作就删除已缓存的数据. */
|
||||
protected long expireTime = TimeUnit.DAYS.toMillis(10);
|
||||
@Getter
|
||||
@Setter
|
||||
protected boolean requiredGetCaptcha = true;
|
||||
|
||||
public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size) {
|
||||
this.target = target;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size, int waitTime, int period) {
|
||||
this.target = target;
|
||||
this.size = size;
|
||||
this.waitTime = waitTime;
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size, int waitTime, int period, Long expireTime) {
|
||||
this.target = target;
|
||||
this.size = size;
|
||||
this.waitTime = waitTime;
|
||||
this.period = period;
|
||||
this.expireTime = expireTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记的初始化调度器
|
||||
*/
|
||||
public void initSchedule() {
|
||||
init(size);
|
||||
}
|
||||
|
||||
private void init(int z) {
|
||||
this.size = z;
|
||||
// 初始化一个队列扫描
|
||||
scheduledExecutor.scheduleAtFixedRate(() -> {
|
||||
queueMap.forEach((k, queue) -> {
|
||||
try {
|
||||
AtomicInteger pos = posMap.computeIfAbsent(k, k1 -> new AtomicInteger(0));
|
||||
int addCount = 0;
|
||||
while (pos.get() < this.size) {
|
||||
if (pos.get() >= size) {
|
||||
return;
|
||||
}
|
||||
ImageCaptchaInfo slideImageInfo = target.generateCaptchaImage(k);
|
||||
if (slideImageInfo != null) {
|
||||
boolean addStatus = queue.offer(slideImageInfo);
|
||||
addCount++;
|
||||
if (addStatus) {
|
||||
// 添加记录
|
||||
pos.incrementAndGet();
|
||||
}
|
||||
} else {
|
||||
sleep();
|
||||
}
|
||||
}
|
||||
if (addCount == 0) {
|
||||
// 没有添加,检测最新更新时间 如果时间过长,直接清除数据
|
||||
Long lastUpdate = lastUpdateMap.get(k);
|
||||
if (lastUpdate != null && System.currentTimeMillis() - lastUpdate > expireTime) {
|
||||
queueMap.remove(k);
|
||||
posMap.remove(k);
|
||||
lastUpdateMap.remove(k);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// cache所有
|
||||
log.error("缓存队列扫描时出错, ex", e);
|
||||
// 删掉它
|
||||
queueMap.remove(k);
|
||||
posMap.remove(k);
|
||||
lastUpdateMap.remove(k);
|
||||
// 休眠
|
||||
sleep();
|
||||
}
|
||||
});
|
||||
|
||||
}, 0, period, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void sleep() {
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(waitTime);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(String type) {
|
||||
GenerateParam generateParam = new GenerateParam();
|
||||
generateParam.setType(type);
|
||||
return generateCaptchaImage(generateParam, this.requiredGetCaptcha);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam generateParam, boolean requiredGetCaptcha) {
|
||||
ConcurrentLinkedQueue<ImageCaptchaInfo> queue = queueMap.get(generateParam);
|
||||
ImageCaptchaInfo captchaInfo = null;
|
||||
if (queue != null) {
|
||||
captchaInfo = queue.poll();
|
||||
if (captchaInfo == null) {
|
||||
log.warn("滑块验证码缓存不足, genParam:{}", generateParam);
|
||||
} else {
|
||||
AtomicInteger pos = posMap.get(generateParam);
|
||||
if (pos != null) {
|
||||
pos.decrementAndGet();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
queueMap.putIfAbsent(generateParam, new ConcurrentLinkedQueue<>());
|
||||
posMap.putIfAbsent(generateParam, new AtomicInteger(0));
|
||||
}
|
||||
if (captchaInfo == null && requiredGetCaptcha) {
|
||||
// 直接生成 不走缓存
|
||||
captchaInfo = target.generateCaptchaImage(generateParam);
|
||||
}
|
||||
if (captchaInfo != null) {
|
||||
// 记录最新时间
|
||||
lastUpdateMap.put(generateParam, System.currentTimeMillis());
|
||||
}
|
||||
return captchaInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(String type, String targetFormatName, String matrixFormatName) {
|
||||
return generateCaptchaImage(GenerateParam.builder()
|
||||
.type(type)
|
||||
.backgroundFormatName(targetFormatName)
|
||||
.sliderFormatName(matrixFormatName)
|
||||
.build(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
return generateCaptchaImage(param, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageResourceManager() {
|
||||
return target.getImageResourceManager();
|
||||
}
|
||||
|
||||
|
||||
// public static void main(String[] args) throws InterruptedException {
|
||||
// SliderCaptchaTemplate captchaTemplate = new DefaultSliderCaptchaTemplate("jpeg", "png", true);
|
||||
//
|
||||
// captchaTemplate = new CacheSliderCaptchaTemplate(captchaTemplate, 20);
|
||||
// TimeUnit.SECONDS.sleep(5);
|
||||
// for (int i = 0; i < 100; i++) {
|
||||
// long start = System.currentTimeMillis();
|
||||
// SliderCaptchaInfo info = captchaTemplate.getSlideImageInfo();
|
||||
// long end = System.currentTimeMillis();
|
||||
// System.out.println("耗时:" + (end - start));
|
||||
// TimeUnit.MILLISECONDS.sleep(10);
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.util.ObjectUtils;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/24 9:27
|
||||
* @Description 根据type 匹配对应的验证码生成器
|
||||
*/
|
||||
public class MultiImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
private Map<String, ImageCaptchaGenerator> imageCaptchaGeneratorMap = new HashMap<>(4);
|
||||
|
||||
private ImageCaptchaResourceManager imageCaptchaResourceManager;
|
||||
private boolean initDefaultResource;
|
||||
|
||||
private String defaultCaptcha = CaptchaTypeConstant.SLIDER;
|
||||
|
||||
public MultiImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, boolean initDefaultResource) {
|
||||
this.imageCaptchaResourceManager = imageCaptchaResourceManager;
|
||||
this.initDefaultResource = initDefaultResource;
|
||||
init();
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
// 滑块验证码
|
||||
addImageCaptchaGenerator(CaptchaTypeConstant.SLIDER, new StandardSliderImageCaptchaGenerator(imageCaptchaResourceManager, initDefaultResource));
|
||||
// 旋转验证码
|
||||
addImageCaptchaGenerator(CaptchaTypeConstant.ROTATE, new StandardRotateImageCaptchaGenerator(imageCaptchaResourceManager, initDefaultResource));
|
||||
// 拼接验证码
|
||||
addImageCaptchaGenerator(CaptchaTypeConstant.CONCAT, new StandardConcatImageCaptchaGenerator(imageCaptchaResourceManager, initDefaultResource));
|
||||
// 点选文字验证码
|
||||
addImageCaptchaGenerator(CaptchaTypeConstant.WORD_IMAGE_CLICK, new StandardRandomWordClickImageCaptchaGenerator(imageCaptchaResourceManager, initDefaultResource));
|
||||
}
|
||||
|
||||
public void addImageCaptchaGenerator(String key, ImageCaptchaGenerator captchaGenerator) {
|
||||
imageCaptchaGeneratorMap.put(key, captchaGenerator);
|
||||
}
|
||||
|
||||
public ImageCaptchaGenerator removeImageCaptchaGenerator(String key) {
|
||||
return imageCaptchaGeneratorMap.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
String type = param.getType();
|
||||
if (ObjectUtils.isEmpty(type)) {
|
||||
param.setType(defaultCaptcha);
|
||||
type = defaultCaptcha;
|
||||
}
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = imageCaptchaGeneratorMap.get(type);
|
||||
if (imageCaptchaGenerator == null) {
|
||||
throw new IllegalArgumentException("生成验证码失败,错误的type类型:" + type);
|
||||
}
|
||||
|
||||
return imageCaptchaGenerator.generateCaptchaImage(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageResourceManager() {
|
||||
return imageCaptchaResourceManager;
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import static cloud.tianai.captcha.generator.common.util.CaptchaImageUtils.*;
|
||||
import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/25 15:44
|
||||
* @Description 图片拼接滑动验证码生成器
|
||||
*/
|
||||
public class StandardConcatImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
protected ImageCaptchaResourceManager imageCaptchaResourceManager;
|
||||
|
||||
public StandardConcatImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, boolean initDefaultResource) {
|
||||
this.imageCaptchaResourceManager = imageCaptchaResourceManager;
|
||||
if (initDefaultResource) {
|
||||
initDefaultResource();
|
||||
}
|
||||
}
|
||||
|
||||
public void initDefaultResource() {
|
||||
ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore();
|
||||
// 添加一些系统的资源文件
|
||||
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
// 拼接验证码不需要模板 只需要背景图
|
||||
Collection<InputStream> inputStreams = new LinkedList<>();
|
||||
try {
|
||||
Resource resourceImage = imageCaptchaResourceManager.randomGetResource(param.getType());
|
||||
InputStream resourceInputStream = imageCaptchaResourceManager.getResourceInputStream(resourceImage);
|
||||
inputStreams.add(resourceInputStream);
|
||||
BufferedImage bgImage = wrapFile2BufferedImage(resourceInputStream);
|
||||
int spacingY = bgImage.getHeight() / 4;
|
||||
int randomY = ThreadLocalRandom.current().nextInt(spacingY, bgImage.getHeight() - spacingY);
|
||||
BufferedImage[] bgImageSplit = splitImage(randomY, true, bgImage);
|
||||
int spacingX = bgImage.getWidth() / 8;
|
||||
int randomX = ThreadLocalRandom.current().nextInt(spacingX, bgImage.getWidth() - bgImage.getWidth() / 5);
|
||||
BufferedImage[] bgImageTopSplit = splitImage(randomX, false, bgImageSplit[0]);
|
||||
|
||||
BufferedImage sliderImage = concatImage(true,
|
||||
bgImageTopSplit[0].getWidth()
|
||||
+ bgImageTopSplit[1].getWidth(), bgImageTopSplit[0].getHeight(), bgImageTopSplit[1], bgImageTopSplit[0]);
|
||||
bgImage = concatImage(false, bgImageSplit[1].getWidth(), sliderImage.getHeight() + bgImageSplit[1].getHeight(),
|
||||
sliderImage, bgImageSplit[1]);
|
||||
return wrapConcatCaptchaInfo(randomX, randomY,bgImage, param);
|
||||
} finally {
|
||||
// 使用完后关闭流
|
||||
for (InputStream inputStream : inputStreams) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private ImageCaptchaInfo wrapConcatCaptchaInfo(int randomX, int randomY, BufferedImage bgImage, GenerateParam param) {
|
||||
String backGroundImageBase64 = transform(bgImage, param.getBackgroundFormatName());
|
||||
ImageCaptchaInfo imageCaptchaInfo = ImageCaptchaInfo.of(backGroundImageBase64,
|
||||
null,
|
||||
bgImage.getWidth(),
|
||||
bgImage.getHeight(),
|
||||
null,
|
||||
null,
|
||||
randomX,
|
||||
CaptchaTypeConstant.CONCAT);
|
||||
imageCaptchaInfo.setData(randomY);
|
||||
imageCaptchaInfo.setTolerant(0.05F);
|
||||
return imageCaptchaInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageResourceManager() {
|
||||
return imageCaptchaResourceManager;
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.util.FontUtils;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ClickImageCheckDefinition;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.util.FontUtils;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ClickImageCheckDefinition;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
import lombok.Data;
|
||||
import lombok.SneakyThrows;
|
||||
import sun.font.FontDesignMetrics;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/27 11:46
|
||||
* @Description 点选验证码
|
||||
*/
|
||||
@Data
|
||||
public class StandardRandomWordClickImageCaptchaGenerator extends AbstractClickImageCaptchaGenerator {
|
||||
|
||||
protected ImageCaptchaResourceManager imageCaptchaResourceManager;
|
||||
/** 字体包. */
|
||||
protected Font font;
|
||||
protected FontDesignMetrics metrics;
|
||||
protected Integer clickImgWidth = 80;
|
||||
protected Integer clickImgHeight = 80;
|
||||
protected int tipImageInterferenceLineNum = 2;
|
||||
protected int tipImageInterferencePointNum = 5;
|
||||
|
||||
@SneakyThrows
|
||||
public StandardRandomWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, boolean initDefaultResource) {
|
||||
this.imageCaptchaResourceManager = imageCaptchaResourceManager;
|
||||
if (initDefaultResource) {
|
||||
initDefaultResource();
|
||||
}
|
||||
// 使用默认字体
|
||||
Resource fontResource = new Resource(null, "META-INF/fonts/SIMSUN.TTC");
|
||||
InputStream inputStream = new ClassPathResourceProvider().doGetResourceInputStream(fontResource);
|
||||
Font font = Font.createFont(Font.TRUETYPE_FONT, inputStream);
|
||||
font = font.deriveFont(Font.BOLD, 70);
|
||||
this.metrics = FontDesignMetrics.getMetrics(font);
|
||||
this.font = font;
|
||||
setClickImgHeight(clickImgWidth);
|
||||
setClickImgWidth(clickImgHeight);
|
||||
}
|
||||
|
||||
public StandardRandomWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager,
|
||||
boolean initDefaultResource,
|
||||
Font font) {
|
||||
this.imageCaptchaResourceManager = imageCaptchaResourceManager;
|
||||
this.font = font;
|
||||
this.metrics = FontDesignMetrics.getMetrics(font);
|
||||
setClickImgWidth(font.getSize() + 10);
|
||||
setClickImgHeight(font.getSize() + 10);
|
||||
if (initDefaultResource) {
|
||||
initDefaultResource();
|
||||
}
|
||||
}
|
||||
|
||||
public void initDefaultResource() {
|
||||
ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore();
|
||||
// 添加一些系统的资源文件
|
||||
resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImgWrapper genTipImage(List<ClickImageCheckDefinition> imageCheckDefinitions) {
|
||||
String tips = imageCheckDefinitions.stream().map(ClickImageCheckDefinition::getTip).collect(Collectors.joining());
|
||||
// 生成随机颜色
|
||||
int fontWidth = metrics.stringWidth(tips);
|
||||
int width = fontWidth + 5;
|
||||
int height = metrics.getHeight() + 5;
|
||||
float left = (width - fontWidth) / 2f;
|
||||
float top = 5 / 2f + metrics.getAscent();
|
||||
BufferedImage bufferedImage = CaptchaImageUtils.genSimpleImgCaptcha(tips,
|
||||
font, metrics, width, height, left, top, tipImageInterferenceLineNum, tipImageInterferencePointNum);
|
||||
return new ImgWrapper(bufferedImage, tips);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImgWrapper randomGetClickImg() {
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
// 随机文字
|
||||
String randomWord = FontUtils.getRandomChar(random);
|
||||
// 随机颜色
|
||||
Color randomColor = CaptchaImageUtils.getRandomColor(random);
|
||||
// 随机角度
|
||||
int randomDeg = ThreadLocalRandom.current().nextInt(0, 85);
|
||||
BufferedImage fontImage = CaptchaImageUtils.drawWordImg(randomColor,
|
||||
randomWord,
|
||||
font,
|
||||
this.metrics,
|
||||
clickImgWidth,
|
||||
clickImgHeight,
|
||||
randomDeg);
|
||||
return new ImgWrapper(fontImage, randomWord);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo wrapClickImageCaptchaInfo(GenerateParam param, BufferedImage bgImage,
|
||||
BufferedImage tipImage,
|
||||
List<ClickImageCheckDefinition> checkClickImageCheckDefinitionList) {
|
||||
ImageCaptchaInfo clickImageCaptchaInfo = new ImageCaptchaInfo();
|
||||
clickImageCaptchaInfo.setBackgroundImage(transform(bgImage, param.getBackgroundFormatName()));
|
||||
clickImageCaptchaInfo.setSliderImage(transform(tipImage, param.getSliderFormatName()));
|
||||
clickImageCaptchaInfo.setBgImageWidth(bgImage.getWidth());
|
||||
clickImageCaptchaInfo.setBgImageHeight(bgImage.getHeight());
|
||||
clickImageCaptchaInfo.setSliderImageWidth(tipImage.getWidth());
|
||||
clickImageCaptchaInfo.setSliderImageHeight(tipImage.getHeight());
|
||||
clickImageCaptchaInfo.setRandomX(null);
|
||||
clickImageCaptchaInfo.setTolerant(null);
|
||||
clickImageCaptchaInfo.setType(CaptchaTypeConstant.WORD_IMAGE_CLICK);
|
||||
clickImageCaptchaInfo.setExpand(checkClickImageCheckDefinitionList);
|
||||
return clickImageCaptchaInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageResourceManager() {
|
||||
return imageCaptchaResourceManager;
|
||||
}
|
||||
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.RotateImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.RotateImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import static cloud.tianai.captcha.generator.common.util.CaptchaImageUtils.*;
|
||||
import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH;
|
||||
import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/22 16:43
|
||||
* @Description 旋转图片验证码生成器
|
||||
*/
|
||||
public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
protected final ImageCaptchaResourceManager imageCaptchaResourceManager;
|
||||
|
||||
public StandardRotateImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, boolean initDefaultResource) {
|
||||
this.imageCaptchaResourceManager = imageCaptchaResourceManager;
|
||||
if (initDefaultResource) {
|
||||
initDefaultResource();
|
||||
}
|
||||
}
|
||||
|
||||
public void initDefaultResource() {
|
||||
ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore();
|
||||
// 添加一些系统的资源文件
|
||||
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg")));
|
||||
|
||||
// 添加一些系统的 模板文件
|
||||
Map<String, Resource> template1 = new HashMap<>(4);
|
||||
template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/active.png")));
|
||||
template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/fixed.png")));
|
||||
template1.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/matrix.png")));
|
||||
resourceStore.addTemplate(CaptchaTypeConstant.ROTATE, template1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
// 旋转验证码没有混淆
|
||||
Map<String, Resource> templateImages = imageCaptchaResourceManager.randomGetTemplate(param.getType());
|
||||
if (templateImages == null || templateImages.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Collection<InputStream> inputStreams = new LinkedList<>();
|
||||
try {
|
||||
Resource resourceImage = imageCaptchaResourceManager.randomGetResource(param.getType());
|
||||
InputStream resourceInputStream = imageCaptchaResourceManager.getResourceInputStream(resourceImage);
|
||||
inputStreams.add(resourceInputStream);
|
||||
BufferedImage cutBackground = CaptchaImageUtils.wrapFile2BufferedImage(resourceInputStream);
|
||||
// 拷贝一份图片
|
||||
BufferedImage targetBackground = CaptchaImageUtils.deepCopyBufferedImage(cutBackground);
|
||||
|
||||
InputStream fixedTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME);
|
||||
inputStreams.add(fixedTemplateInput);
|
||||
BufferedImage fixedTemplate = CaptchaImageUtils.wrapFile2BufferedImage(fixedTemplateInput);
|
||||
|
||||
InputStream activeTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME);
|
||||
inputStreams.add(activeTemplateInput);
|
||||
BufferedImage activeTemplate = CaptchaImageUtils.wrapFile2BufferedImage(activeTemplateInput);
|
||||
|
||||
InputStream matrixTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME);
|
||||
inputStreams.add(matrixTemplateInput);
|
||||
BufferedImage matrixTemplate = CaptchaImageUtils.wrapFile2BufferedImage(matrixTemplateInput);
|
||||
|
||||
// 算出居中的x和y
|
||||
int x = targetBackground.getWidth() / 2 - fixedTemplate.getWidth() / 2;
|
||||
int y = targetBackground.getHeight() / 2 - fixedTemplate.getHeight() / 2;
|
||||
CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, x, y);
|
||||
// 抠图部分
|
||||
BufferedImage cutImage = CaptchaImageUtils.cutImage(cutBackground, fixedTemplate, x, y);
|
||||
CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0);
|
||||
// 随机旋转抠图部分
|
||||
// 随机x, 转换为角度
|
||||
int randomX = ThreadLocalRandom.current().nextInt(fixedTemplate.getWidth() + 10, targetBackground.getWidth() - 10);
|
||||
double degree = 360d - randomX / ((targetBackground.getWidth()) / 360d);
|
||||
CaptchaImageUtils.centerOverlayAndRotateImage(matrixTemplate, cutImage, degree);
|
||||
return wrapRotateCaptchaInfo(degree, randomX, targetBackground, matrixTemplate, param);
|
||||
} finally {
|
||||
// 使用完后关闭流
|
||||
for (InputStream inputStream : inputStreams) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private ImageCaptchaInfo wrapRotateCaptchaInfo(double degree, int randomX, BufferedImage backgroundImage, BufferedImage sliderImage, GenerateParam param) {
|
||||
String backgroundFormatName = param.getBackgroundFormatName();
|
||||
String sliderFormatName = param.getSliderFormatName();
|
||||
String backGroundImageBase64 = transform(backgroundImage, backgroundFormatName);
|
||||
String sliderImageBase64 = transform(sliderImage, sliderFormatName);
|
||||
return RotateImageCaptchaInfo.of(degree,
|
||||
randomX,
|
||||
backGroundImageBase64,
|
||||
sliderImageBase64,
|
||||
backgroundImage.getWidth(), backgroundImage.getHeight(),
|
||||
sliderImage.getWidth(), sliderImage.getHeight()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageResourceManager() {
|
||||
return imageCaptchaResourceManager;
|
||||
}
|
||||
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.SliderImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import static cloud.tianai.captcha.generator.common.util.CaptchaImageUtils.*;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @Date 2020/5/29 8:06
|
||||
* @Description 滑块验证码模板
|
||||
*/
|
||||
@Slf4j
|
||||
public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
/**
|
||||
* 默认的resource资源文件路径.
|
||||
*/
|
||||
public static final String DEFAULT_SLIDER_IMAGE_RESOURCE_PATH = "META-INF/cut-image/resource";
|
||||
/**
|
||||
* 默认的template资源文件路径.
|
||||
*/
|
||||
public static final String DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH = "META-INF/cut-image/template";
|
||||
|
||||
protected final ImageCaptchaResourceManager imageCaptchaResourceManager;
|
||||
|
||||
|
||||
public StandardSliderImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager,
|
||||
boolean initDefaultResource) {
|
||||
this.imageCaptchaResourceManager = imageCaptchaResourceManager;
|
||||
if (initDefaultResource) {
|
||||
initDefaultResource();
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
Boolean obfuscate = param.getObfuscate();
|
||||
Map<String, Resource> templateImages = imageCaptchaResourceManager.randomGetTemplate(param.getType());
|
||||
if (templateImages == null || templateImages.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Collection<InputStream> inputStreams = new LinkedList<>();
|
||||
try {
|
||||
Resource resourceImage = imageCaptchaResourceManager.randomGetResource(param.getType());
|
||||
InputStream resourceInputStream = imageCaptchaResourceManager.getResourceInputStream(resourceImage);
|
||||
inputStreams.add(resourceInputStream);
|
||||
BufferedImage cutBackground = CaptchaImageUtils.wrapFile2BufferedImage(resourceInputStream);
|
||||
// 拷贝一份图片
|
||||
BufferedImage targetBackground = CaptchaImageUtils.deepCopyBufferedImage(cutBackground);
|
||||
|
||||
InputStream fixedTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME);
|
||||
inputStreams.add(fixedTemplateInput);
|
||||
BufferedImage fixedTemplate = CaptchaImageUtils.wrapFile2BufferedImage(fixedTemplateInput);
|
||||
|
||||
InputStream activeTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME);
|
||||
inputStreams.add(activeTemplateInput);
|
||||
BufferedImage activeTemplate = CaptchaImageUtils.wrapFile2BufferedImage(activeTemplateInput);
|
||||
|
||||
|
||||
InputStream matrixTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME);
|
||||
inputStreams.add(matrixTemplateInput);
|
||||
BufferedImage matrixTemplate = CaptchaImageUtils.wrapFile2BufferedImage(matrixTemplateInput);
|
||||
|
||||
// BufferedImage cutTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, CUT_IMAGE_NAME));
|
||||
|
||||
// 获取随机的 x 和 y 轴
|
||||
int randomX = ThreadLocalRandom.current().nextInt(fixedTemplate.getWidth() + 5, targetBackground.getWidth() - fixedTemplate.getWidth() - 10);
|
||||
int randomY = ThreadLocalRandom.current().nextInt(targetBackground.getHeight() - fixedTemplate.getHeight());
|
||||
|
||||
CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, randomX, randomY);
|
||||
if (obfuscate) {
|
||||
// 加入混淆滑块
|
||||
int obfuscateX = randomObfuscateX(randomX, fixedTemplate.getWidth(), targetBackground.getWidth());
|
||||
CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, obfuscateX, randomY);
|
||||
}
|
||||
BufferedImage cutImage = CaptchaImageUtils.cutImage(cutBackground, fixedTemplate, randomX, randomY);
|
||||
CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0);
|
||||
CaptchaImageUtils.overlayImage(matrixTemplate, cutImage, 0, randomY);
|
||||
return wrapSliderCaptchaInfo(randomX, randomY, targetBackground, matrixTemplate, param);
|
||||
} finally {
|
||||
// 使用完后关闭流
|
||||
for (InputStream inputStream : inputStreams) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装成 SliderCaptchaInfo
|
||||
*
|
||||
* @param randomX 随机生成的 x轴
|
||||
* @param randomY 随机生成的 y轴
|
||||
* @param backgroundImage 背景图片
|
||||
* @param sliderImage 滑块图片
|
||||
* @param param 接口传入参数
|
||||
* @return SliderCaptchaInfo
|
||||
*/
|
||||
@SneakyThrows
|
||||
public SliderImageCaptchaInfo wrapSliderCaptchaInfo(int randomX,
|
||||
int randomY,
|
||||
BufferedImage backgroundImage,
|
||||
BufferedImage sliderImage,
|
||||
GenerateParam param) {
|
||||
String backgroundFormatName = param.getBackgroundFormatName();
|
||||
String sliderFormatName = param.getSliderFormatName();
|
||||
String backGroundImageBase64 = transform(backgroundImage, backgroundFormatName);
|
||||
String sliderImageBase64 = transform(sliderImage, sliderFormatName);
|
||||
return SliderImageCaptchaInfo.of(randomX, randomY,
|
||||
backGroundImageBase64,
|
||||
sliderImageBase64,
|
||||
backgroundImage.getWidth(), backgroundImage.getHeight(),
|
||||
sliderImage.getWidth(), sliderImage.getHeight()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageResourceManager() {
|
||||
return imageCaptchaResourceManager;
|
||||
}
|
||||
|
||||
protected int randomObfuscateX(int sliderX, int slWidth, int bgWidth) {
|
||||
if (bgWidth / 2 > (sliderX + (slWidth / 2))) {
|
||||
// 右边混淆
|
||||
return ThreadLocalRandom.current().nextInt(sliderX + slWidth, bgWidth - slWidth);
|
||||
}
|
||||
// 左边混淆
|
||||
return ThreadLocalRandom.current().nextInt(slWidth, sliderX - slWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认资源
|
||||
*/
|
||||
public void initDefaultResource() {
|
||||
ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore();
|
||||
// 添加一些系统的资源文件
|
||||
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg")));
|
||||
|
||||
// 添加一些系统的 模板文件
|
||||
Map<String, Resource> template1 = new HashMap<>(4);
|
||||
template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
|
||||
template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
|
||||
template1.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/matrix.png")));
|
||||
resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template1);
|
||||
|
||||
|
||||
Map<String, Resource> template2 = new HashMap<>(4);
|
||||
template2.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
|
||||
template2.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
|
||||
template2.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/matrix.png")));
|
||||
resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user