From 2effcf403282dcb11e0d6ee1dc14409771a09e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A9=E7=88=B1=E6=9C=89=E6=83=85?= Date: Wed, 16 Feb 2022 10:43:19 +0800 Subject: [PATCH] =?UTF-8?q?+=20=E5=A2=9E=E5=8A=A0=20CaptchaImageConverter?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=99=A8=EF=BC=8C=E8=B4=9F=E8=B4=A3=E5=B0=86?= =?UTF-8?q?=E7=94=9F=E6=88=90=E7=9A=84=E5=9B=BE=E7=89=87=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E6=88=90=E8=87=AA=E5=B7=B1=E6=83=B3=E8=A6=81=E7=9A=84=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E6=AF=94=E5=A6=82=E5=8A=A0=E5=AF=86=E8=BD=AC?= =?UTF-8?q?=E7=A0=81=E7=AD=89..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../slider/CaptchaImageConverter.java | 11 + .../template/slider/CaptchaImageUtils.java | 175 ++++++++ .../slider/DefaultCaptchaImageConverter.java | 41 ++ .../slider/DefaultSliderCaptchaTemplate.java | 386 ------------------ .../template/slider/OriginalSliderData.java | 40 ++ .../template/slider/SliderCaptchaInfo.java | 15 + .../slider/StandardSliderCaptchaTemplate.java | 205 ++++++++++ 8 files changed, 488 insertions(+), 387 deletions(-) create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/CaptchaImageConverter.java create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/CaptchaImageUtils.java create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/DefaultCaptchaImageConverter.java delete mode 100644 src/main/java/cloud/tianai/captcha/template/slider/DefaultSliderCaptchaTemplate.java create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/OriginalSliderData.java create mode 100644 src/main/java/cloud/tianai/captcha/template/slider/StandardSliderCaptchaTemplate.java diff --git a/pom.xml b/pom.xml index 57ca940..696cbbf 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 cloud.tianai.captcha tianai-captcha - 1.2.4 + 1.2.5 tianai-captcha 滑块验证码 diff --git a/src/main/java/cloud/tianai/captcha/template/slider/CaptchaImageConverter.java b/src/main/java/cloud/tianai/captcha/template/slider/CaptchaImageConverter.java new file mode 100644 index 0000000..7e3cbfd --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/CaptchaImageConverter.java @@ -0,0 +1,11 @@ +package cloud.tianai.captcha.template.slider; + +/** + * @Author: 天爱有情 + * @date 2022/2/16 10:04 + * @Description 验证码图片转换器,将生成的 OriginalSliderData 转换成 SliderCaptchaInfo, 可以对齐进行加密,转换成base64、url等扩展 + */ +public interface CaptchaImageConverter { + + SliderCaptchaInfo convert(OriginalSliderData originalSliderData); +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/CaptchaImageUtils.java b/src/main/java/cloud/tianai/captcha/template/slider/CaptchaImageUtils.java new file mode 100644 index 0000000..747766f --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/CaptchaImageUtils.java @@ -0,0 +1,175 @@ +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.InputStream; +import java.net.URL; +import java.util.ArrayList; + +/** + * @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不能为空"); + } + return ImageIO.read(resourceImage); + } + + @SneakyThrows + public static BufferedImage wrapFile2BufferedImage(InputStream resource) { + if (resource == null) { + throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空"); + } + 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 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; + } + } 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; + } + + +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/DefaultCaptchaImageConverter.java b/src/main/java/cloud/tianai/captcha/template/slider/DefaultCaptchaImageConverter.java new file mode 100644 index 0000000..ced6ec7 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/DefaultCaptchaImageConverter.java @@ -0,0 +1,41 @@ +package cloud.tianai.captcha.template.slider; + +import lombok.SneakyThrows; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; + +/** + * @Author: 天爱有情 + * @date 2022/2/16 10:23 + * @Description 默认 CaptchaImageConverter + */ +public class DefaultCaptchaImageConverter implements CaptchaImageConverter { + + @Override + @SneakyThrows + public SliderCaptchaInfo convert(OriginalSliderData originalSliderData) { + GenerateParam param = originalSliderData.getGenerateParam(); + BufferedImage backgroundImage = originalSliderData.getBackgroundImage(); + BufferedImage sliderImage = originalSliderData.getSliderImage(); + String backgroundFormatName = param.getBackgroundFormatName(); + String sliderFormatName = param.getSliderFormatName(); + String backGroundImageBase64 = transform(backgroundImage, backgroundFormatName); + String sliderImageBase64 = transform(sliderImage, sliderFormatName); + return SliderCaptchaInfo.of(originalSliderData, + backGroundImageBase64, + sliderImageBase64); + } + + public String transform(BufferedImage bufferedImage, String formatName) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, formatName, byteArrayOutputStream); + //转换成字节码 + byte[] data = byteArrayOutputStream.toByteArray(); + String base64 = Base64.getEncoder().encodeToString(data); + return "data:image/" + formatName + ";base64,".concat(base64); + } +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/DefaultSliderCaptchaTemplate.java b/src/main/java/cloud/tianai/captcha/template/slider/DefaultSliderCaptchaTemplate.java deleted file mode 100644 index b205d4b..0000000 --- a/src/main/java/cloud/tianai/captcha/template/slider/DefaultSliderCaptchaTemplate.java +++ /dev/null @@ -1,386 +0,0 @@ -package cloud.tianai.captcha.template.slider; - -import cloud.tianai.captcha.template.slider.provider.ClassPathResourceProvider; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - -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.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.*; -import java.util.concurrent.ThreadLocalRandom; - -/** - * @Author: 天爱有情 - * @Date 2020/5/29 8:06 - * @Description 滑块验证码模板 - */ -@Slf4j -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"; - - - private final SliderCaptchaResourceManager sliderCaptchaResourceManager; - - - public void initDefaultResource() { - ResourceStore resourceStore = sliderCaptchaResourceManager.getResourceStore(); - // 添加一些系统的资源文件 - resourceStore.addResource(new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg"))); - - // 添加一些系统的 模板文件 - Map 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(template1); - - - Map 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(template2); - - } - - public DefaultSliderCaptchaTemplate(SliderCaptchaResourceManager sliderCaptchaResourceManager, boolean initDefaultResource) { - this.sliderCaptchaResourceManager = sliderCaptchaResourceManager; - if (initDefaultResource) { - initDefaultResource(); - } - } - - - 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("jpeg", "png"); - } - - - @SneakyThrows - @Override - public SliderCaptchaInfo getSlideImageInfo(String backgroundFormatName, String sliderFormatName) { - return getSlideImageInfo(GenerateParam.builder() - .backgroundFormatName(backgroundFormatName) - .sliderFormatName(sliderFormatName) - .obfuscate(true) - .build()); - } - - @SneakyThrows - @Override - public SliderCaptchaInfo getSlideImageInfo(GenerateParam param) { - - String backgroundFormatName = param.getBackgroundFormatName(); - String sliderFormatName = param.getSliderFormatName(); - Boolean obfuscate = param.getObfuscate(); - - Map templateImages = sliderCaptchaResourceManager.randomGetTemplate(); - if (templateImages == null || templateImages.isEmpty()) { - return null; - } - Collection inputStreams = new LinkedList<>(); - try { - Resource resourceImage = sliderCaptchaResourceManager.randomGetResource(); - InputStream resourceInputStream = sliderCaptchaResourceManager.getResourceInputStream(resourceImage); - inputStreams.add(resourceInputStream); - BufferedImage cutBackground = wrapFile2BufferedImage(resourceInputStream); - // 拷贝一份图片 - BufferedImage targetBackground = deepCopyBufferedImage(cutBackground); - - InputStream fixedTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME); - inputStreams.add(fixedTemplateInput); - BufferedImage fixedTemplate = wrapFile2BufferedImage(fixedTemplateInput); - - InputStream activeTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME); - inputStreams.add(activeTemplateInput); - BufferedImage activeTemplate = wrapFile2BufferedImage(activeTemplateInput); - - - InputStream matrixTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME); - inputStreams.add(matrixTemplateInput); - BufferedImage matrixTemplate = wrapFile2BufferedImage(matrixTemplateInput); - -// 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); - if (obfuscate) { - // 加入混淆滑块 - int obfuscateX = randomObfuscateX(randomX, fixedTemplate.getWidth(), targetBackground.getWidth()); - coverImage(targetBackground, fixedTemplate, obfuscateX, 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, backgroundFormatName); - String sliderImageBase64 = transformBase64(matrixTemplate, sliderFormatName); - - return SliderCaptchaInfo.of(randomX, xPercent, randomY, backGroundImageBase64, sliderImageBase64); - } finally { - // 使用完后关闭流 - for (InputStream inputStream : inputStreams) { - try { - inputStream.close(); - } catch (IOException e) { - // ignore - } - } - } - } - - private 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); - } - - /** - * 百分比对比 - * - * @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; - } - - @Override - public SliderCaptchaResourceManager getSlideImageResourceManager() { - return sliderCaptchaResourceManager; - } - - - private String transformBase64(BufferedImage bufferedImage, String formatName) throws IOException { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - ImageIO.write(bufferedImage, formatName, byteArrayOutputStream); - //转换成字节码 - byte[] data = byteArrayOutputStream.toByteArray(); - 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; - } - } 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 InputStream getTemplateFile(Map templateImages, String imageName) { - Resource resource = templateImages.get(imageName); - if (resource == null) { - throw new IllegalArgumentException("查找模板异常, 该模板下未找到 ".concat(imageName)); - } - return sliderCaptchaResourceManager.getResourceInputStream(resource); - } - - - @SneakyThrows - private static BufferedImage wrapFile2BufferedImage(URL resourceImage) { - if (resourceImage == null) { - throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空"); - } - return ImageIO.read(resourceImage); - } - - @SneakyThrows - private static BufferedImage wrapFile2BufferedImage(InputStream resource) { - if (resource == null) { - throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空"); - } - return ImageIO.read(resource); - } - - - public static void main(String[] args) throws InterruptedException { - SliderCaptchaResourceManager sliderCaptchaResourceManager = new DefaultSliderCaptchaResourceManager(); - DefaultSliderCaptchaTemplate sliderCaptchaTemplate = new DefaultSliderCaptchaTemplate(sliderCaptchaResourceManager, true); - // 生成滑块图片 - SliderCaptchaInfo slideImageInfo = sliderCaptchaTemplate.getSlideImageInfo(); - // 获取背景图片的base64 - String backgroundImage = slideImageInfo.getBackgroundImage(); - // 获取滑块图片 - slideImageInfo.getSliderImage(); - // 获取滑块被背景图片的百分比, (校验图片使用) - Float xPercent = slideImageInfo.getXPercent(); - System.out.println(backgroundImage); - System.out.println(slideImageInfo); - } -} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/OriginalSliderData.java b/src/main/java/cloud/tianai/captcha/template/slider/OriginalSliderData.java new file mode 100644 index 0000000..fa0b76d --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/OriginalSliderData.java @@ -0,0 +1,40 @@ +package cloud.tianai.captcha.template.slider; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.awt.image.BufferedImage; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OriginalSliderData { + /** + * x轴 + */ + private Integer x; + /** + * y轴 + */ + private Integer y; + + /** 滑块要凹槽的百分比. */ + private float xPercent; + /** + * 背景图 + */ + private BufferedImage backgroundImage; + /** + * 移动图 + */ + private BufferedImage sliderImage; + /** + * 生成参数 + */ + private GenerateParam generateParam; + + public static OriginalSliderData of(Integer x, Integer y, float xPercent, BufferedImage backgroundImage, BufferedImage sliderImage, GenerateParam generateParam) { + return new OriginalSliderData(x, y, xPercent, backgroundImage, sliderImage, generateParam); + } +} \ No newline at end of file diff --git a/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaInfo.java b/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaInfo.java index ddfcd78..669839d 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaInfo.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/SliderCaptchaInfo.java @@ -32,8 +32,23 @@ public class SliderCaptchaInfo { * 移动图 */ private String sliderImage; + /** + * 扩展字段 + */ + public Object expand; + public SliderCaptchaInfo(Integer x, Float xPercent, Integer y, String backgroundImage, String sliderImage) { + this.x = x; + this.xPercent = xPercent; + this.y = y; + this.backgroundImage = backgroundImage; + this.sliderImage = sliderImage; + } public static SliderCaptchaInfo of(Integer x, Float xPercent, Integer y, String backgroundImage, String sliderImage) { return new SliderCaptchaInfo(x, xPercent, y, backgroundImage, sliderImage); } + + public static SliderCaptchaInfo of(OriginalSliderData originalSliderData, String backgroundImage, String sliderImage) { + return new SliderCaptchaInfo(originalSliderData.getX(), originalSliderData.getXPercent(), originalSliderData.getY(), backgroundImage, sliderImage); + } } diff --git a/src/main/java/cloud/tianai/captcha/template/slider/StandardSliderCaptchaTemplate.java b/src/main/java/cloud/tianai/captcha/template/slider/StandardSliderCaptchaTemplate.java new file mode 100644 index 0000000..379220c --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/StandardSliderCaptchaTemplate.java @@ -0,0 +1,205 @@ +package cloud.tianai.captcha.template.slider; + +import cloud.tianai.captcha.template.slider.provider.ClassPathResourceProvider; +import lombok.Getter; +import lombok.Setter; +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.template.slider.CaptchaImageUtils.*; + +/** + * @Author: 天爱有情 + * @Date 2020/5/29 8:06 + * @Description 滑块验证码模板 + */ +@Slf4j +public class StandardSliderCaptchaTemplate 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 String DEFAULT_BG_IMAGE_TYPE = "jpeg"; + public static String DEFAULT_SLIDER_IMAGE_TYPE = "png"; + public static float DEFAULT_TOLERANT = 0.02f; + + protected final SliderCaptchaResourceManager sliderCaptchaResourceManager; + protected final CaptchaImageConverter captchaImageConverter; + + @Getter + @Setter + /** 容错值. */ + public float tolerant = DEFAULT_TOLERANT; + @Getter + @Setter + /** 默认背景图片类型. */ + public String defaultBgImageType = DEFAULT_BG_IMAGE_TYPE; + @Getter + @Setter + /** 默认滑块图片类型. */ + public String defaultSliderImageType = DEFAULT_SLIDER_IMAGE_TYPE; + + public StandardSliderCaptchaTemplate(SliderCaptchaResourceManager sliderCaptchaResourceManager, + CaptchaImageConverter captchaImageConverter, + boolean initDefaultResource) { + this.sliderCaptchaResourceManager = sliderCaptchaResourceManager; + this.captchaImageConverter = captchaImageConverter; + if (initDefaultResource) { + initDefaultResource(); + } + } + + @Override + public SliderCaptchaInfo getSlideImageInfo() { + return getSlideImageInfo(defaultBgImageType, defaultSliderImageType); + } + + @SneakyThrows + @Override + public SliderCaptchaInfo getSlideImageInfo(String backgroundFormatName, String sliderFormatName) { + return getSlideImageInfo(GenerateParam.builder() + .backgroundFormatName(backgroundFormatName) + .sliderFormatName(sliderFormatName) + .obfuscate(false) + .build()); + } + + @SneakyThrows + @Override + public SliderCaptchaInfo getSlideImageInfo(GenerateParam param) { + Boolean obfuscate = param.getObfuscate(); + Map templateImages = sliderCaptchaResourceManager.randomGetTemplate(); + if (templateImages == null || templateImages.isEmpty()) { + return null; + } + Collection inputStreams = new LinkedList<>(); + try { + Resource resourceImage = sliderCaptchaResourceManager.randomGetResource(); + InputStream resourceInputStream = sliderCaptchaResourceManager.getResourceInputStream(resourceImage); + inputStreams.add(resourceInputStream); + BufferedImage cutBackground = wrapFile2BufferedImage(resourceInputStream); + // 拷贝一份图片 + BufferedImage targetBackground = deepCopyBufferedImage(cutBackground); + + InputStream fixedTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME); + inputStreams.add(fixedTemplateInput); + BufferedImage fixedTemplate = wrapFile2BufferedImage(fixedTemplateInput); + + InputStream activeTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME); + inputStreams.add(activeTemplateInput); + BufferedImage activeTemplate = wrapFile2BufferedImage(activeTemplateInput); + + + InputStream matrixTemplateInput = getTemplateFile(templateImages, SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME); + inputStreams.add(matrixTemplateInput); + BufferedImage matrixTemplate = wrapFile2BufferedImage(matrixTemplateInput); + +// 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()); + + overlayImage(targetBackground, fixedTemplate, randomX, randomY); + if (obfuscate) { + // 加入混淆滑块 + int obfuscateX = randomObfuscateX(randomX, fixedTemplate.getWidth(), targetBackground.getWidth()); + overlayImage(targetBackground, fixedTemplate, obfuscateX, randomY); + } + BufferedImage cutImage = cutImage(cutBackground, fixedTemplate, randomX, randomY); + overlayImage(cutImage, activeTemplate, 0, 0); + overlayImage(matrixTemplate, cutImage, 0, randomY); + // 计算滑块百分比 + float xPercent = (float) randomX / targetBackground.getWidth(); + return captchaImageConverter.convert(OriginalSliderData.of(randomX, randomY, xPercent, targetBackground, matrixTemplate, param)); + } finally { + // 使用完后关闭流 + for (InputStream inputStream : inputStreams) { + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + /** + * 百分比对比 + * + * @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 maxTolerant = oriPercentage + tolerant; + float minTolerant = oriPercentage - tolerant; + return newPercentage >= minTolerant && newPercentage <= maxTolerant; + } + + @Override + public SliderCaptchaResourceManager getSlideImageResourceManager() { + return sliderCaptchaResourceManager; + } + + 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); + } + + protected InputStream getTemplateFile(Map templateImages, String imageName) { + Resource resource = templateImages.get(imageName); + if (resource == null) { + throw new IllegalArgumentException("查找模板异常, 该模板下未找到 ".concat(imageName)); + } + return sliderCaptchaResourceManager.getResourceInputStream(resource); + } + + /** + * 初始化默认资源 + */ + public void initDefaultResource() { + ResourceStore resourceStore = sliderCaptchaResourceManager.getResourceStore(); + // 添加一些系统的资源文件 + resourceStore.addResource(new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg"))); + + // 添加一些系统的 模板文件 + Map 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(template1); + + + Map 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(template2); + } +}