This commit is contained in:
天爱有情
2022-05-07 11:14:15 +08:00
parent d802fd481a
commit afbb88c67d
46 changed files with 376 additions and 260 deletions
@@ -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();
}
@@ -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";
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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图片至img0,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));
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}