1.更改架构

2.增加缓存池
This commit is contained in:
liushaofeng
2020-10-19 18:39:18 +08:00
parent 588613b5e0
commit 1ba81019bd
7 changed files with 681 additions and 381 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.0.alpha</version>
<version>1.0.beta</version>
<name>tianai-captcha</name>
<description>滑块验证码</description>
@@ -0,0 +1,115 @@
package cloud.tianai.captcha.template.slider;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class CacheSliderCaptchaTemplate implements SliderCaptchaTemplate {
private final ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("slider-captcha-queue"));
private Queue<SliderCaptchaInfo> queue;
private AtomicInteger pos = new AtomicInteger(0);
private SliderCaptchaTemplate target;
private int size;
public CacheSliderCaptchaTemplate(SliderCaptchaTemplate target, int size) {
this.target = target;
init(size);
}
private void init(int z) {
this.size = z;
this.pos = new AtomicInteger(0);
queue = new LinkedList<>();
// 初始化一个队列扫描
scheduledExecutor.scheduleAtFixedRate(() -> {
while (pos.get() < this.size) {
int count = pos.incrementAndGet();
if (count > size) {
return;
}
SliderCaptchaInfo slideImageInfo = target.getSlideImageInfo();
queue.add(slideImageInfo);
}
}, 0, 100, TimeUnit.MILLISECONDS);
}
@SneakyThrows
@Override
public SliderCaptchaInfo getSlideImageInfo() {
while (true) {
int i = pos.get();
if (i > 0) {
if (pos.compareAndSet(i, i - 1)) {
SliderCaptchaInfo poll = queue.poll();
if (poll != null) {
return poll;
}
}
}
// 休眠100毫秒
TimeUnit.MILLISECONDS.sleep(100);
}
}
public static void main(String[] args) throws InterruptedException {
SliderCaptchaTemplate captchaTemplate = new DefaultSliderCaptchaTemplate("webp", "webp", 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);
}
}
@Override
public void addResource(URL url) {
target.addResource(url);
}
@Override
public void addTemplate(Map<String, URL> template) {
target.addTemplate(template);
}
@Override
public void setResource(List<URL> resources) {
target.setResource(resources);
}
@Override
public void setTemplates(List<Map<String, URL>> imageTemplates) {
target.setTemplates(imageTemplates);
}
@Override
public void deleteResource(URL resource) {
target.deleteResource(resource);
}
@Override
public void deleteTemplate(Map<String, URL> template) {
target.deleteTemplate(template);
}
@Override
public boolean percentageContrast(Float newPercentage, Float oriPercentage) {
return target.percentageContrast(newPercentage, oriPercentage);
}
}
@@ -0,0 +1,436 @@
package cloud.tianai.captcha.template.slider;
import lombok.SneakyThrows;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.PixelGrabber;
import java.awt.image.WritableRaster;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.util.List;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @Author: 天爱有情
* @Date 2020/5/29 8:06
* @Description 滑块验证码模板
*/
public class DefaultSliderCaptchaTemplate implements SliderCaptchaTemplate {
/**
* 默认的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";
public static final String ACTIVE_IMAGE_NAME = "active.png";
public static final String CUT_IMAGE_NAME = "cut.png";
public static final String FIXED_IMAGE_NAME = "fixed.png";
public static final String MATRIX_IMAGE_NAME = "matrix.png";
/**
* resource图片.
*/
private List<URL> resourceImageFiles = new ArrayList<>(20);
/**
* 模板图片.
*/
private List<Map<String, URL>> templateImageFiles = new ArrayList<>(2);
protected String targetFormatName = "jpeg";
protected String matrixFormatName = "png";
public void initDefaultResource() {
// 添加一些系统的资源文件
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/2.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/3.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/4.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/5.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/6.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/7.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/8.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/9.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/10.jpg")));
// 添加一些系统的 模板文件
Map<String, URL> template1 = new HashMap<>(4);
template1.put(ACTIVE_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
template1.put(CUT_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/cut.png")));
template1.put(FIXED_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
template1.put(MATRIX_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/matrix.png")));
addTemplate(template1);
Map<String, URL> template2 = new HashMap<>(4);
template2.put(ACTIVE_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
template2.put(CUT_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/cut.png")));
template2.put(FIXED_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
template2.put(MATRIX_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/matrix.png")));
addTemplate(template2);
Map<String, URL> template3 = new HashMap<>(4);
template3.put(ACTIVE_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/active.png")));
template3.put(CUT_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/cut.png")));
template3.put(FIXED_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/fixed.png")));
template3.put(MATRIX_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/matrix.png")));
addTemplate(template3);
}
private final AtomicBoolean loadResources = new AtomicBoolean(false);
private String sliderImageResourcePath = DEFAULT_SLIDER_IMAGE_RESOURCE_PATH;
private String sliderImageTemplatePath = DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;
public DefaultSliderCaptchaTemplate(boolean initDefaultResource) {
// 加载系统资源文件
if (initDefaultResource) {
initDefaultResource();
}
}
public DefaultSliderCaptchaTemplate(String targetFormatName, String matrixFormatName, boolean initDefaultResource) {
this.targetFormatName = targetFormatName;
this.matrixFormatName = matrixFormatName;
if (initDefaultResource) {
initDefaultResource();
}
}
public DefaultSliderCaptchaTemplate(String targetFormatName,
String matrixFormatName,
String sliderImageResourcePath,
String sliderImageTemplatePath,
List<URL> r,
List<Map<String, URL>> t,
boolean initDefaultResource) {
this.targetFormatName = targetFormatName;
this.matrixFormatName = matrixFormatName;
this.sliderImageResourcePath = sliderImageResourcePath;
this.sliderImageTemplatePath = sliderImageTemplatePath;
resourceImageFiles = r;
templateImageFiles = t;
if (initDefaultResource) {
initDefaultResource();
}
}
public DefaultSliderCaptchaTemplate(List<URL> r, List<Map<String, URL>> t, boolean initDefaultResource) {
resourceImageFiles = r;
templateImageFiles = t;
if (initDefaultResource) {
initDefaultResource();
}
}
@Override
public void addResource(URL url) {
resourceImageFiles.remove(url);
resourceImageFiles.add(url);
}
@Override
public void setResource(List<URL> resources) {
resourceImageFiles = resources;
}
@Override
public void setTemplates(List<Map<String, URL>> imageTemplates) {
templateImageFiles = imageTemplates;
}
@Override
public void deleteResource(URL resource) {
resourceImageFiles.remove(resource);
}
@Override
public void deleteTemplate(Map<String, URL> template) {
templateImageFiles.remove(template);
}
@Override
public void addTemplate(Map<String, URL> template) {
templateImageFiles.remove(template);
templateImageFiles.add(template);
}
private static ClassLoader getClassLoader() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = DefaultSliderCaptchaTemplate.getClassLoader();
}
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
return classLoader;
}
@Override
public SliderCaptchaInfo getSlideImageInfo() {
return getSlideImageInfo(this.targetFormatName, this.matrixFormatName);
}
@SneakyThrows
public SliderCaptchaInfo getSlideImageInfo(String targetFormatName, String matrixFormatName) {
URL resourceImage = getRandomResourceImage();
Map<String, URL> templateImages = getRandomTemplateImages();
BufferedImage cutBackground = warpFile2BufferedImage(resourceImage);
// 拷贝一份图片
BufferedImage targetBackground = deepCopyBufferedImage(cutBackground);
BufferedImage fixedTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, FIXED_IMAGE_NAME));
BufferedImage activeTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, ACTIVE_IMAGE_NAME));
BufferedImage matrixTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, MATRIX_IMAGE_NAME));
// BufferedImage cutTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, CUT_IMAGE_NAME));
// 获取随机的 x 和 y 轴
int randomX = ThreadLocalRandom.current().nextInt(targetBackground.getWidth() - fixedTemplate.getWidth() * 2) + fixedTemplate.getWidth();
int randomY = ThreadLocalRandom.current().nextInt(targetBackground.getHeight() - fixedTemplate.getHeight());
coverImage(targetBackground, fixedTemplate, randomX, randomY);
BufferedImage cutImage = cutImage(cutBackground, fixedTemplate, randomX, randomY);
coverImage(cutImage, activeTemplate, 0, 0);
coverImage(matrixTemplate, cutImage, 0, randomY);
// 计算滑块百分比
Float xPercent = (float) randomX / targetBackground.getWidth();
String backGroundImageBase64 = transformBase64(targetBackground, targetFormatName);
String sliderImageBase64 = transformBase64(matrixTemplate, matrixFormatName);
return SliderCaptchaInfo.of(randomX, xPercent, randomY, backGroundImageBase64, sliderImageBase64);
}
/**
* 百分比对比
*
* @param newPercentage 用户百分比
* @param oriPercentage 原百分比
* @return true 成功 false 失败
*/
@Override
public boolean percentageContrast(Float newPercentage, Float oriPercentage) {
if (newPercentage == null || Float.isNaN(newPercentage) || Float.isInfinite(newPercentage)
|| oriPercentage == null || Float.isNaN(oriPercentage) || Float.isInfinite(oriPercentage)) {
return false;
}
// 容错值
float tolerant = 0.02f;
float maxTolerant = oriPercentage + tolerant;
float minTolerant = oriPercentage - tolerant;
return newPercentage >= minTolerant && newPercentage <= maxTolerant;
}
private String transformBase64(BufferedImage bufferedImage, String formatName) {
byte[] data = null;
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
ImageIO.write(bufferedImage, formatName, byteArrayOutputStream);
//转换成字节码
data = byteArrayOutputStream.toByteArray();
} catch (Exception e) {
System.out.println(e.getMessage());
}
String base64 = Base64.getEncoder().encodeToString(data);
return "data:image/" + formatName + ";base64,".concat(base64);
}
/**
* 通过模板图片抠图(不透明部分)
*
* @param origin 源图片
* @param template 模板图片
* @param x 坐标轴x
* @param y 坐标轴y
* @return BufferedImage
*/
@SneakyThrows
private 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;
}
/**
* 深度拷贝图片
*
* @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);
}
/**
* 将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;
}
if (j == width) {
if (temp == 0) {
rec.add(new Area(new Rectangle(j, i, 1, 1)));
} else {
rec.add(new Area(new Rectangle(temp, i, j - temp, 1)));
temp = 0;
}
}
} else {
if (temp != 0) {
rec.add(new Area(new Rectangle(temp, i, j - temp, 1)));
temp = 0;
}
}
}
temp = 0;
}
return rec;
}
/**
* 图片覆盖(覆盖图压缩到width*height大小,覆盖到底图上)
*
* @param baseBufferedImage 底图
* @param coverBufferedImage 覆盖图
* @param x 起始x轴
* @param y 起始y轴
*/
private static void coverImage(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();
}
private URL getTemplateFile(Map<String, URL> templateImages, String imageName) {
URL url = templateImages.get(imageName);
if (url == null) {
throw new IllegalArgumentException("查找模板异常, 该模板下未找到 ");
}
return url;
}
private Map<String, URL> getRandomTemplateImages() {
if (templateImageFiles.size() == 1) {
return templateImageFiles.get(0);
}
int templateNo = ThreadLocalRandom.current().nextInt(templateImageFiles.size());
return templateImageFiles.get(templateNo);
}
@SneakyThrows
private static BufferedImage warpFile2BufferedImage(URL resourceImage) {
if (resourceImage == null) {
throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空");
}
return ImageIO.read(resourceImage);
}
private URL getRandomResourceImage() {
int targetNo = ThreadLocalRandom.current().nextInt(resourceImageFiles.size());
return resourceImageFiles.get(targetNo);
}
// public static void main(String[] args) throws InterruptedException {
// DefaultSliderCaptchaTemplate sliderCaptchaTemplate = new DefaultSliderCaptchaTemplate();
// // 生成滑块图片
// SliderCaptchaInfo slideImageInfo = sliderCaptchaTemplate.getSlideImageInfo();
// // 获取背景图片的base64
// String backgroundImage = slideImageInfo.getBackgroundImage();
// // 获取滑块图片
// slideImageInfo.getSliderImage();
// // 获取滑块被背景图片的百分比, (校验图片使用)
// Float xPercent = slideImageInfo.getXPercent();
// }
}
@@ -0,0 +1,44 @@
package cloud.tianai.captcha.template.slider;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A ThreadFactory that allows for custom thread names.
*/
public class NamedThreadFactory implements ThreadFactory {
private static final AtomicInteger THREAD_INDEX = new AtomicInteger(0);
private final String basename;
private final boolean daemon;
/**
* Creates a new instance of the factory.
*
* @param basename Basename of a new tread created by this factory.
*/
public NamedThreadFactory(final String basename) {
this(basename, true);
}
/**
* Creates a new instance of the factory.
*
* @param basename Basename of a new tread created by this factory.
* @param daemon If true, marks new thread as a daemon thread
*/
public NamedThreadFactory(final String basename, final boolean daemon) {
this.basename = basename;
this.daemon = daemon;
}
@Override
public Thread newThread(final Runnable runnable) {
final Thread thread = new Thread(runnable, basename + "-" + THREAD_INDEX.getAndIncrement());
thread.setDaemon(daemon);
return thread;
}
}
@@ -0,0 +1,54 @@
package cloud.tianai.captcha.template.slider;
import java.net.URL;
import java.util.List;
import java.util.Map;
/**
* @Author: 天爱有情
* @date 2020/10/19 18:38
* @Description 滑块验证码资源
*/
public interface SliderCaptchaResource {
/**
* 添加资源
*
* @param url url
*/
void addResource(URL url);
/**
* 添加模板
*
* @param template template
*/
void addTemplate(Map<String, URL> template);
/**
* 设置资源
*
* @param resources resources
*/
void setResource(List<URL> resources);
/**
* 设置模板
*
* @param imageTemplates imageTemplates
*/
void setTemplates(List<Map<String, URL>> imageTemplates);
/**
* 删除资源
*
* @param resource resource
*/
void deleteResource(URL resource);
/**
* 删除模板
*
* @param template template
*/
void deleteTemplate(Map<String, URL> template);
}
@@ -1,181 +1,18 @@
package cloud.tianai.captcha.template.slider;
import lombok.SneakyThrows;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.PixelGrabber;
import java.awt.image.WritableRaster;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.net.URL;
import java.util.List;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @Author: 天爱有情
* @Date 2020/5/29 8:06
* @date 2020/10/19 18:37
* @Description 滑块验证码模板
*/
public class SliderCaptchaTemplate {
public interface SliderCaptchaTemplate extends SliderCaptchaResource {
/** 默认的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";
public static final String ACTIVE_IMAGE_NAME = "active.png";
public static final String CUT_IMAGE_NAME = "cut.png";
public static final String FIXED_IMAGE_NAME = "fixed.png";
public static final String MATRIX_IMAGE_NAME = "matrix.png";
/** resource图片.*/
private static List<URL> resourceImageFiles = new ArrayList<>(20);
/** 模板图片.*/
private static List<Map<String, URL>> templateImageFiles = new ArrayList<>(2);
static {
// 添加一些系统的资源文件
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/2.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/3.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/4.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/5.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/6.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/7.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/8.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/9.jpg")));
addResource(getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/10.jpg")));
// 添加一些系统的 模板文件
Map<String, URL> template1 = new HashMap<>(4);
template1.put(ACTIVE_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
template1.put(CUT_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/cut.png")));
template1.put(FIXED_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
template1.put(MATRIX_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/matrix.png")));
addTemplate(template1);
Map<String, URL> template2 = new HashMap<>(4);
template2.put(ACTIVE_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
template2.put(CUT_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/cut.png")));
template2.put(FIXED_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
template2.put(MATRIX_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/matrix.png")));
addTemplate(template2);
Map<String, URL> template3 = new HashMap<>(4);
template3.put(ACTIVE_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/active.png")));
template3.put(CUT_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/cut.png")));
template3.put(FIXED_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/fixed.png")));
template3.put(MATRIX_IMAGE_NAME, getClassLoader().getResource(DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/matrix.png")));
addTemplate(template3);
}
private final AtomicBoolean loadResources = new AtomicBoolean(false);
private String sliderImageResourcePath = DEFAULT_SLIDER_IMAGE_RESOURCE_PATH;
private String sliderImageTemplatePath = DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;
public SliderCaptchaTemplate() {
// 加载系统资源文件
}
public SliderCaptchaTemplate(String sliderImageResourcePath, String sliderImageTemplatePath) {
this.sliderImageResourcePath = sliderImageResourcePath;
this.sliderImageTemplatePath = sliderImageTemplatePath;
// 加载系统资源文件
}
public SliderCaptchaTemplate(List<URL> r, List<Map<String, URL>> t) {
resourceImageFiles = r;
templateImageFiles = t;
}
public static void addResource(URL url) {
resourceImageFiles.remove(url);
resourceImageFiles.add(url);
}
public static void setResource(List<URL> resources) {
resourceImageFiles = resources;
}
public static void setTemplates(List<Map<String, URL>> imageTemplates) {
templateImageFiles = imageTemplates;
}
public static void deleteResource(URL resource) {
resourceImageFiles.remove(resource);
}
public static void deleteTemplate(Map<String, URL> template) {
templateImageFiles.remove(template);
}
public static void addTemplate(Map<String, URL> template) {
templateImageFiles.remove(template);
templateImageFiles.add(template);
}
private static ClassLoader getClassLoader() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = SliderCaptchaTemplate.getClassLoader();
}
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
return classLoader;
}
public SliderCaptchaInfo getSlideImageInfo(){
return getSlideImageInfo("jpg", "png");
}
public SliderCaptchaInfo getSlideImageInfoForWebp(){
return getSlideImageInfo("webp", "webp");
}
@SneakyThrows
public SliderCaptchaInfo getSlideImageInfo(String targetFormatName, String matrixFormatName) {
URL resourceImage = getRandomResourceImage();
Map<String, URL> templateImages = getRandomTemplateImages();
BufferedImage cutBackground = warpFile2BufferedImage(resourceImage);
// 拷贝一份图片
BufferedImage targetBackground = deepCopyBufferedImage(cutBackground);
BufferedImage fixedTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, FIXED_IMAGE_NAME));
BufferedImage activeTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, ACTIVE_IMAGE_NAME));
BufferedImage matrixTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, MATRIX_IMAGE_NAME));
// BufferedImage cutTemplate = warpFile2BufferedImage(getTemplateFile(templateImages, CUT_IMAGE_NAME));
// 获取随机的 x 和 y 轴
int randomX = ThreadLocalRandom.current().nextInt(targetBackground.getWidth() - fixedTemplate.getWidth() * 2) + fixedTemplate.getWidth();
int randomY =ThreadLocalRandom.current().nextInt(targetBackground.getHeight() - fixedTemplate.getHeight());
coverImage(targetBackground, fixedTemplate, randomX, randomY);
BufferedImage cutImage = cutImage(cutBackground, fixedTemplate, randomX, randomY);
coverImage(cutImage, activeTemplate, 0, 0);
coverImage(matrixTemplate, cutImage, 0, randomY);
// 计算滑块百分比
Float xPercent = (float) randomX / targetBackground.getWidth();
String backGroundImageBase64 = transformBase64(targetBackground, targetFormatName);
String sliderImageBase64 = transformBase64(matrixTemplate, matrixFormatName);
return SliderCaptchaInfo.of(randomX, xPercent, randomY, backGroundImageBase64, sliderImageBase64);
}
/**
* 获取滑块验证码
*
* @return SliderCaptchaInfo
*/
SliderCaptchaInfo getSlideImageInfo();
/**
* 百分比对比
@@ -184,213 +21,5 @@ public class SliderCaptchaTemplate {
* @param oriPercentage 原百分比
* @return true 成功 false 失败
*/
public boolean percentageContrast(Float newPercentage, Float oriPercentage) {
if (newPercentage == null || Float.isNaN(newPercentage) || Float.isInfinite(newPercentage)
|| oriPercentage == null || Float.isNaN(oriPercentage) || Float.isInfinite(oriPercentage)) {
return false;
}
// 容错值
float tolerant = 0.02f;
float maxTolerant = oriPercentage + tolerant;
float minTolerant = oriPercentage - tolerant;
return newPercentage >= minTolerant && newPercentage <= maxTolerant;
}
private String transformBase64(BufferedImage bufferedImage, String formatName) {
byte[] data = null;
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
ImageIO.write(bufferedImage, formatName, byteArrayOutputStream);
//转换成字节码
data = byteArrayOutputStream.toByteArray();
} catch (Exception e) {
System.out.println(e.getMessage());
}
String base64 = Base64.getEncoder().encodeToString(data);
return "data:image/"+formatName+";base64,".concat(base64);
}
/**
* 通过模板图片抠图(不透明部分)
*
* @param origin 源图片
* @param template 模板图片
* @param x 坐标轴x
* @param y 坐标轴y
* @return BufferedImage
*/
@SneakyThrows
private 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;
}
/**
* 深度拷贝图片
* @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);
}
/**
* 将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;
}
if (j == width) {
if (temp == 0) {
rec.add(new Area(new Rectangle(j, i, 1, 1)));
} else {
rec.add(new Area(new Rectangle(temp, i, j - temp, 1)));
temp = 0;
}
}
} else {
if (temp != 0) {
rec.add(new Area(new Rectangle(temp, i, j - temp, 1)));
temp = 0;
}
}
}
temp = 0;
}
return rec;
}
/**
* 图片覆盖(覆盖图压缩到width*height大小,覆盖到底图上)
*
* @param baseBufferedImage 底图
* @param coverBufferedImage 覆盖图
* @param x 起始x轴
* @param y 起始y轴
*/
private static void coverImage(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();
}
private URL getTemplateFile(Map<String, URL> templateImages, String imageName) {
URL url = templateImages.get(imageName);
if (url == null) {
throw new IllegalArgumentException("查找模板异常, 该模板下未找到 ");
}
return url;
}
private Map<String, URL> getRandomTemplateImages() {
if (templateImageFiles.size() == 1) {
return templateImageFiles.get(0);
}
int templateNo = ThreadLocalRandom.current().nextInt(templateImageFiles.size());
return templateImageFiles.get(templateNo);
}
@SneakyThrows
private static BufferedImage warpFile2BufferedImage(URL resourceImage) {
if (resourceImage == null) {
throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空");
}
return ImageIO.read(resourceImage);
}
private URL getRandomResourceImage() {
int targetNo = ThreadLocalRandom.current().nextInt(resourceImageFiles.size());
return resourceImageFiles.get(targetNo);
}
public static void main(String[] args) {
SliderCaptchaTemplate sliderCaptchaTemplate = new SliderCaptchaTemplate();
// 生成滑块图片
SliderCaptchaInfo slideImageInfo = sliderCaptchaTemplate.getSlideImageInfo();
// 获取背景图片的base64
String backgroundImage = slideImageInfo.getBackgroundImage();
// 获取滑块图片
slideImageInfo.getSliderImage();
// 获取滑块被背景图片的百分比, (校验图片使用)
Float xPercent = slideImageInfo.getXPercent();
}
boolean percentageContrast(Float newPercentage, Float oriPercentage);
}
@@ -0,0 +1,22 @@
package cloud.tianai.captcha.template.slider.exception;
public class SliderCaptchaException extends RuntimeException{
public SliderCaptchaException() {
}
public SliderCaptchaException(String message) {
super(message);
}
public SliderCaptchaException(String message, Throwable cause) {
super(message, cause);
}
public SliderCaptchaException(Throwable cause) {
super(cause);
}
public SliderCaptchaException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}