From 1ba81019bd4837f5c0c28df9a9957fb6a8a4f4d1 Mon Sep 17 00:00:00 2001 From: liushaofeng Date: Mon, 19 Oct 2020 18:39:18 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=9B=B4=E6=94=B9=E6=9E=B6=E6=9E=84=202.?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BC=93=E5=AD=98=E6=B1=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../slider/CacheSliderCaptchaTemplate.java | 115 +++++ .../slider/DefaultSliderCaptchaTemplate.java | 436 ++++++++++++++++++ .../template/slider/NamedThreadFactory.java | 44 ++ .../slider/SliderCaptchaResource.java | 54 +++ .../slider/SliderCaptchaTemplate.java | 389 +--------------- .../exception/SliderCaptchaException.java | 22 + 7 files changed, 681 insertions(+), 381 deletions(-) create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/CacheSliderCaptchaTemplate.java create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/DefaultSliderCaptchaTemplate.java create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/NamedThreadFactory.java create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaResource.java create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/exception/SliderCaptchaException.java diff --git a/pom.xml b/pom.xml index 343450c..d53c137 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 cloud.tianai.captcha tianai-captcha - 1.0.alpha + 1.0.beta tianai-captcha 滑块验证码 diff --git a/src/main/java/cloud/tianai/captcha/template/slider/CacheSliderCaptchaTemplate.java b/src/main/java/cloud/tianai/captcha/template/slider/CacheSliderCaptchaTemplate.java new file mode 100644 index 0000000..4f07bf5 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/CacheSliderCaptchaTemplate.java @@ -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 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 template) { + target.addTemplate(template); + } + + @Override + public void setResource(List resources) { + target.setResource(resources); + } + + @Override + public void setTemplates(List> imageTemplates) { + target.setTemplates(imageTemplates); + } + + @Override + public void deleteResource(URL resource) { + target.deleteResource(resource); + } + + @Override + public void deleteTemplate(Map template) { + target.deleteTemplate(template); + } + + @Override + public boolean percentageContrast(Float newPercentage, Float oriPercentage) { + return target.percentageContrast(newPercentage, oriPercentage); + } +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/DefaultSliderCaptchaTemplate.java b/src/main/java/cloud/tianai/captcha/template/slider/DefaultSliderCaptchaTemplate.java new file mode 100644 index 0000000..7b8a6e7 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/DefaultSliderCaptchaTemplate.java @@ -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 resourceImageFiles = new ArrayList<>(20); + /** + * 模板图片. + */ + private List> 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 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 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 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 r, + List> 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 r, List> 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 resources) { + resourceImageFiles = resources; + } + + @Override + public void setTemplates(List> imageTemplates) { + templateImageFiles = imageTemplates; + } + + @Override + public void deleteResource(URL resource) { + resourceImageFiles.remove(resource); + } + + @Override + public void deleteTemplate(Map template) { + templateImageFiles.remove(template); + } + + @Override + public void addTemplate(Map 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 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 x = new ArrayList<>(); + ArrayList 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 templateImages, String imageName) { + URL url = templateImages.get(imageName); + if (url == null) { + throw new IllegalArgumentException("查找模板异常, 该模板下未找到 "); + } + return url; + } + + private Map 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(); +// } +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/NamedThreadFactory.java b/src/main/java/cloud/tianai/captcha/template/slider/NamedThreadFactory.java new file mode 100644 index 0000000..a9d6e19 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/NamedThreadFactory.java @@ -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; + } +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaResource.java b/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaResource.java new file mode 100644 index 0000000..760386d --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaResource.java @@ -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 template); + + /** + * 设置资源 + * + * @param resources resources + */ + void setResource(List resources); + + /** + * 设置模板 + * + * @param imageTemplates imageTemplates + */ + void setTemplates(List> imageTemplates); + + /** + * 删除资源 + * + * @param resource resource + */ + void deleteResource(URL resource); + + /** + * 删除模板 + * + * @param template template + */ + void deleteTemplate(Map template); +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaTemplate.java b/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaTemplate.java index 76f89bc..ffafc48 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaTemplate.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaTemplate.java @@ -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 resourceImageFiles = new ArrayList<>(20); - /** 模板图片.*/ - private static List> 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 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 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 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 r, List> t) { - resourceImageFiles = r; - templateImageFiles = t; - } - - public static void addResource(URL url) { - resourceImageFiles.remove(url); - resourceImageFiles.add(url); - } - - public static void setResource(List resources) { - resourceImageFiles = resources; - } - - public static void setTemplates(List> imageTemplates) { - templateImageFiles = imageTemplates; - } - - public static void deleteResource(URL resource) { - resourceImageFiles.remove(resource); - } - - public static void deleteTemplate(Map template) { - templateImageFiles.remove(template); - } - - public static void addTemplate(Map 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 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 x = new ArrayList<>(); - ArrayList 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 templateImages, String imageName) { - URL url = templateImages.get(imageName); - if (url == null) { - throw new IllegalArgumentException("查找模板异常, 该模板下未找到 "); - } - return url; - } - - private Map 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); } diff --git a/src/main/java/cloud/tianai/captcha/template/slider/exception/SliderCaptchaException.java b/src/main/java/cloud/tianai/captcha/template/slider/exception/SliderCaptchaException.java new file mode 100644 index 0000000..8a276de --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/exception/SliderCaptchaException.java @@ -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); + } +}