diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/constant/CaptchaTypeConstant.java b/src/main/java/cloud/tianai/captcha/template/slider/common/constant/CaptchaTypeConstant.java similarity index 67% rename from src/main/java/cloud/tianai/captcha/template/slider/generator/common/constant/CaptchaTypeConstant.java rename to src/main/java/cloud/tianai/captcha/template/slider/common/constant/CaptchaTypeConstant.java index 92ab3e9..1a940b8 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/constant/CaptchaTypeConstant.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/common/constant/CaptchaTypeConstant.java @@ -1,4 +1,4 @@ -package cloud.tianai.captcha.template.slider.generator.common.constant; +package cloud.tianai.captcha.template.slider.common.constant; /** * @Author: 天爱有情 @@ -13,6 +13,6 @@ public interface CaptchaTypeConstant { String ROTATE = "ROTATE"; /** 拼接.*/ String CONCAT = "CONCAT"; - /** 文字点选.*/ - String WORD_CLICK = "WORD_CLICK"; + /** 图片点选.*/ + String IMAGE_CLICK = "IMAGE_CLICK"; } diff --git a/src/main/java/cloud/tianai/captcha/template/slider/common/util/CaptchaUtils.java b/src/main/java/cloud/tianai/captcha/template/slider/common/util/CaptchaUtils.java new file mode 100644 index 0000000..92c54cc --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/common/util/CaptchaUtils.java @@ -0,0 +1,32 @@ +package cloud.tianai.captcha.template.slider.common.util; + +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; + +/** + * @Author: 天爱有情 + * @date 2022/4/28 17:03 + * @Description 验证码工具包 + */ +public class CaptchaUtils { + + /** + * 是否是滑动验证码 + * @param type 类型 + * @return boolean + */ + public static boolean isSliderCaptcha(String type) { + return CaptchaTypeConstant.SLIDER.equals(type) + || CaptchaTypeConstant.ROTATE.equals(type) + || CaptchaTypeConstant.CONCAT.equals(type); + } + + /** + * 是否是点击验证码 + * @param type type + * @return boolean + */ + public static boolean isClickCaptcha(String type) { + return CaptchaTypeConstant.IMAGE_CLICK.equals(type); + } + +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/common/util/FontUtils.java b/src/main/java/cloud/tianai/captcha/template/slider/common/util/FontUtils.java index f41c141..2f16db8 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/common/util/FontUtils.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/common/util/FontUtils.java @@ -1,5 +1,11 @@ package cloud.tianai.captcha.template.slider.common.util; +import lombok.SneakyThrows; + +import java.awt.*; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + /** * @Author: 天爱有情 * @date 2022/4/27 11:34 @@ -7,11 +13,17 @@ package cloud.tianai.captcha.template.slider.common.util; */ public class FontUtils { - public static char getRandomChar() { - return (char)(0x4e00 + (int)(Math.random()*(0x9fa5 - 0x4e00 + 1))); + @SneakyThrows + public static String getRandomChar(Random random) { + Integer hightPos, lowPos; // 定义高低位 + hightPos = (176 + Math.abs(random.nextInt(39))); + lowPos = (161 + Math.abs(random.nextInt(93))); + byte[] bytes = new byte[2]; + bytes[0] = hightPos.byteValue(); + bytes[1] = lowPos.byteValue(); + return new String(bytes, "GBK"); } - } diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/ImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/ImageCaptchaGenerator.java index 2f4411e..0d73e77 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/ImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/ImageCaptchaGenerator.java @@ -1,6 +1,6 @@ package cloud.tianai.captcha.template.slider.generator; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import cloud.tianai.captcha.template.slider.generator.common.model.dto.GenerateParam; import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; import cloud.tianai.captcha.template.slider.resource.ImageCaptchaResourceManager; diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/ClickImageCheckDefinition.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/ClickImageCheckDefinition.java new file mode 100644 index 0000000..f38e856 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/ClickImageCheckDefinition.java @@ -0,0 +1,27 @@ +package cloud.tianai.captcha.template.slider.generator.common.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: 天爱有情 + * @date 2022/4/28 16:51 + * @Description 点击图片校验描述 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ClickImageCheckDefinition { + /** 提示.*/ + private String tip; + /** x.*/ + private Integer x; + /** y.*/ + private Integer y; + /** 宽.*/ + private Integer width; + /** 高.*/ + private Integer height; + +} \ No newline at end of file diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/GenerateParam.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/GenerateParam.java index 023cb9d..8b1b4f0 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/GenerateParam.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/GenerateParam.java @@ -1,6 +1,6 @@ package cloud.tianai.captcha.template.slider.generator.common.model.dto; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import lombok.*; /** diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/RotateImageCaptchaInfo.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/RotateImageCaptchaInfo.java index d96fe2f..8a8a9ad 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/RotateImageCaptchaInfo.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/RotateImageCaptchaInfo.java @@ -1,6 +1,6 @@ package cloud.tianai.captcha.template.slider.generator.common.model.dto; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -19,7 +19,7 @@ public class RotateImageCaptchaInfo extends ImageCaptchaInfo { */ private Double degree; /** 旋转图片的容错值大一点. */ - public static final Float DEFAULT_TOLERANT = 0.05F; + public static final Float DEFAULT_TOLERANT = 0.03F; public static RotateImageCaptchaInfo of(Double degree, Integer randomX, diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/SliderImageCaptchaInfo.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/SliderImageCaptchaInfo.java index 24d806d..35021bf 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/SliderImageCaptchaInfo.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/SliderImageCaptchaInfo.java @@ -1,6 +1,6 @@ package cloud.tianai.captcha.template.slider.generator.common.model.dto; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/WordClickImageCaptchaInfo.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/WordClickImageCaptchaInfo.java deleted file mode 100644 index 5d48fd2..0000000 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/common/model/dto/WordClickImageCaptchaInfo.java +++ /dev/null @@ -1,31 +0,0 @@ -package cloud.tianai.captcha.template.slider.generator.common.model.dto; - -import lombok.*; - -import java.util.List; - -/** - * @Author: 天爱有情 - * @date 2022/4/27 15:33 - * @Description 文字点选 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class WordClickImageCaptchaInfo extends ImageCaptchaInfo { - - private List checkWords; - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class WordDefinition { - private String word; - private Integer x; - private Integer y; - private Integer deg; - private Integer wordWidth; - private Integer wordHeight; - - } -} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/common/util/CaptchaImageUtils.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/util/CaptchaImageUtils.java similarity index 67% rename from src/main/java/cloud/tianai/captcha/template/slider/common/util/CaptchaImageUtils.java rename to src/main/java/cloud/tianai/captcha/template/slider/generator/common/util/CaptchaImageUtils.java index 2e5208c..0257958 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/common/util/CaptchaImageUtils.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/common/util/CaptchaImageUtils.java @@ -1,10 +1,13 @@ -package cloud.tianai.captcha.template.slider.common.util; +package cloud.tianai.captcha.template.slider.generator.common.util; import lombok.SneakyThrows; +import sun.font.FontDesignMetrics; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.Area; +import java.awt.geom.CubicCurve2D; +import java.awt.geom.QuadCurve2D; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.PixelGrabber; @@ -12,6 +15,8 @@ import java.awt.image.WritableRaster; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; /** * @Author: 天爱有情 @@ -314,7 +319,146 @@ public class CaptchaImageUtils { } public static char getRandomChar() { - return (char)(0x4e00 + (int)(Math.random()*(0x9fa5 - 0x4e00 + 1))); + return (char) (0x4e00 + (int) (Math.random() * (0x9fa5 - 0x4e00 + 1))); } + + @SneakyThrows + public static BufferedImage drawWordImg(Color fontColor, + String word, + Font font, + FontDesignMetrics metrics, + int imgWidth, + int imgHeight, + float deg) { + BufferedImage fillRect = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = fillRect.createGraphics(); + g.setColor(new Color(255, 255, 255, 0)); + g.fillRect(0, 0, imgWidth, imgHeight); + g.setColor(fontColor); + g.setFont(font); + float left = (imgWidth - font.getSize()) / 2f; + float top = (imgHeight - font.getSize()) / 2f + metrics.getAscent() - 6; + g.rotate(Math.toRadians(deg), imgWidth / 2f, imgHeight / 2f); + g.drawString(word, left, top); + g.dispose(); + return fillRect; + } + + /** + * 随机画干扰圆 + * + * @param num 数量 + * @param color 颜色 + * @param g Graphics2D + */ + public static void drawOval(int num, + Color color, + Graphics2D g, + int width, + int height, + Random random) { + for (int i = 0; i < num; i++) { + g.setColor(color == null ? getRandomColor(random) : color); + int w = 5 + random.nextInt(10); + int x = random.nextInt(width - 25); + int y = random.nextInt(height - 25); + g.drawOval(x, y, w, w); + } + } + + + /** + * 随机画贝塞尔曲线 + * + * @param num 数量 + * @param color 颜色 + * @param g Graphics2D + */ + public static void drawBesselLine(int num, Color color, + Graphics2D g, + int width, + int height, + ThreadLocalRandom random) { + for (int i = 0; i < num; i++) { + g.setColor(color == null ? getRandomColor(random) : color); + int x1 = 5, y1 = random.nextInt(5, height / 2); + int x2 = width - 5, y2 = random.nextInt(height / 2, height - 5); + int ctrlx = random.nextInt(width / 4, width / 4 * 3); + int ctrly = random.nextInt(5, height - 5); + if (random.nextInt(2) == 0) { + int ty = y1; + y1 = y2; + y2 = ty; + } + if (random.nextInt(2) == 0) { // 二阶贝塞尔曲线 + QuadCurve2D shape = new QuadCurve2D.Double(); + shape.setCurve(x1, y1, ctrlx, ctrly, x2, y2); + g.draw(shape); + } else { // 三阶贝塞尔曲线 + int ctrlx1 = random.nextInt(width / 4, width / 4 * 3); + int ctrly1 = random.nextInt(5, height - 5); + CubicCurve2D shape = new CubicCurve2D.Double(x1, y1, ctrlx, ctrly, ctrlx1, ctrly1, x2, y2); + g.draw(shape); + } + } + } + + /** + * 生成简单的验证码图片 + * @param data 验证码内容 + * @param font 字体包 + * @param metrics FontDesignMetrics + * @param width 验证码宽度 + * @param height 验证码高度 + * @param startX 起始X + * @param startY 起始Y + * @param interferenceLineNum 干扰线数量 + * @param interferencePointNum 干扰点数量 + * @return BufferedImage + */ + public static BufferedImage genSimpleImgCaptcha(String data, + Font font, + FontDesignMetrics metrics, + int width, + int height, + float startX, + float startY, + int interferenceLineNum, + int interferencePointNum) { + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = bufferedImage.createGraphics(); + ThreadLocalRandom random = ThreadLocalRandom.current(); + g.setFont(font); + char[] chars = data.toCharArray(); + + for (int i = 0; i < chars.length; i++) { + g.setColor(getRandomColor(random)); + g.drawString(String.valueOf(chars[i]), startX + i * font.getSize(), startY); + } + // 干扰点 + if (interferencePointNum > 0) { + drawOval(interferencePointNum, null, g, width, height, random); + } + if (interferencePointNum > 0) { + g.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + // 干扰线 + drawBesselLine(interferenceLineNum, null, g, width, height, random); + } + return bufferedImage; + } + + /** + * 随机获取颜色 + * @return Color + */ + public static Color getRandomColor(Random random) { + return new Color( + random.nextInt(255), + random.nextInt(255), + random.nextInt(255)); + + } + + } diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/AbstractClickImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/AbstractClickImageCaptchaGenerator.java new file mode 100644 index 0000000..99b606d --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/AbstractClickImageCaptchaGenerator.java @@ -0,0 +1,148 @@ +package cloud.tianai.captcha.template.slider.generator.impl; + +import cloud.tianai.captcha.template.slider.generator.common.util.CaptchaImageUtils; +import cloud.tianai.captcha.template.slider.generator.AbstractImageCaptchaGenerator; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.generator.common.model.dto.ClickImageCheckDefinition; +import cloud.tianai.captcha.template.slider.generator.common.model.dto.GenerateParam; +import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; +import cloud.tianai.captcha.template.slider.resource.common.model.dto.Resource; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +import static cloud.tianai.captcha.template.slider.generator.common.util.CaptchaImageUtils.wrapFile2BufferedImage; + +/** + * @Author: 天爱有情 + * @date 2022/4/27 11:46 + * @Description 点选验证码 点选验证码分为点选文字和点选图标等 + */ +@Data +public abstract class AbstractClickImageCaptchaGenerator extends AbstractImageCaptchaGenerator { + + /** 参与校验的数量.*/ + protected Integer checkClickCount = 4; + /** 干扰数量.*/ + protected Integer interferenceCount = 2; + + @SneakyThrows + @Override + public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) { + // 文字点选验证码不需要模板 只需要背景图 + Collection inputStreams = new LinkedList<>(); + try { + Resource resourceImage = getImageResourceManager().randomGetResource(param.getType()); + InputStream resourceInputStream = getImageResourceManager().getResourceInputStream(resourceImage); + inputStreams.add(resourceInputStream); + BufferedImage bgImage = wrapFile2BufferedImage(resourceInputStream); + + List clickImageCheckDefinitionList = new ArrayList<>(interferenceCount); + int allImages = interferenceCount + checkClickCount; + for (int i = 0; i < allImages; i++) { + // 随机获取点击图片 + ImgWrapper imgWrapper = randomGetClickImg(); + BufferedImage image = imgWrapper.getImage(); + int clickImgWidth = image.getWidth(); + int clickImgHeight = image.getHeight(); + // 随机x + int randomX = ThreadLocalRandom.current().nextInt(10, bgImage.getWidth() - clickImgWidth); + // 随机y + int randomY = ThreadLocalRandom.current().nextInt(10, bgImage.getHeight() - clickImgHeight); + // 通过随机x和y 进行覆盖图片 + CaptchaImageUtils.overlayImage(bgImage, imgWrapper.getImage(), randomX, randomY); + ImageIO.write(imgWrapper.getImage(), "png", new FileOutputStream("C:\\Users\\tianai\\Desktop\\111\\" + i + ".png")); + ClickImageCheckDefinition clickImageCheckDefinition = new ClickImageCheckDefinition(); + clickImageCheckDefinition.setTip(imgWrapper.getTip()); + clickImageCheckDefinition.setX(randomX + clickImgWidth / 2); + clickImageCheckDefinition.setY(randomY + clickImgHeight / 2); + clickImageCheckDefinition.setWidth(clickImgWidth); + clickImageCheckDefinition.setHeight(clickImgHeight); + clickImageCheckDefinitionList.add(clickImageCheckDefinition); + } + // 背景图转换为字符串 +// String bgImageStr = transform(bgImage, param.getBackgroundFormatName()); + try { + ImageIO.write(bgImage, "jpeg", new FileOutputStream("C:\\Users\\tianai\\Desktop\\123.jpg")); + } catch (IOException e) { + e.printStackTrace(); + } + + // 打乱 + Collections.shuffle(clickImageCheckDefinitionList); + // 拿出参与校验的数据 + List checkClickImageCheckDefinitionList = new ArrayList<>(checkClickCount); + for (int i = 0; i < checkClickCount; i++) { + ClickImageCheckDefinition clickImageCheckDefinition = clickImageCheckDefinitionList.get(i); + checkClickImageCheckDefinitionList.add(clickImageCheckDefinition); + } + // 将校验的文字生成提示图片 + ImgWrapper tipImage = genTipImage(checkClickImageCheckDefinitionList); + + try { + ImageIO.write(tipImage.getImage(), "png", new FileOutputStream("C:\\Users\\tianai\\Desktop\\456.png")); + } catch (IOException e) { + e.printStackTrace(); + } + return wrapClickImageCaptchaInfo(param, bgImage, tipImage.getImage(), checkClickImageCheckDefinitionList); + + } finally { + // 使用完后关闭流 + for (InputStream inputStream : inputStreams) { + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } + } + +// return null; + } + + protected ImageCaptchaInfo wrapClickImageCaptchaInfo(GenerateParam param, BufferedImage bgImage, + BufferedImage tipImage, + List checkClickImageCheckDefinitionList) { + ImageCaptchaInfo clickImageCaptchaInfo = new ImageCaptchaInfo(); + clickImageCaptchaInfo.setBackgroundImage(transform(bgImage, param.getBackgroundFormatName())); + clickImageCaptchaInfo.setSliderImage(transform(bgImage, param.getSliderFormatName())); + clickImageCaptchaInfo.setBgImageWidth(bgImage.getWidth()); + clickImageCaptchaInfo.setBgImageHeight(bgImage.getHeight()); + clickImageCaptchaInfo.setSliderImageWidth(tipImage.getWidth()); + clickImageCaptchaInfo.setSliderImageHeight(tipImage.getHeight()); + clickImageCaptchaInfo.setRandomX(null); + clickImageCaptchaInfo.setTolerant(null); + clickImageCaptchaInfo.setType(CaptchaTypeConstant.IMAGE_CLICK); + clickImageCaptchaInfo.setExpand(checkClickImageCheckDefinitionList); + return clickImageCaptchaInfo; + } + + protected abstract ImgWrapper genTipImage(List imageCheckDefinitions); + + protected abstract ImgWrapper randomGetClickImg(); + + /** + * @Author: 天爱有情 + * @date 2022/4/28 14:26 + * @Description 点击图片包装 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ImgWrapper { + /** 图片.*/ + private BufferedImage image; + /** 提示.*/ + private String tip; + } +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/MultiImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/MultiImageCaptchaGenerator.java index 76e8dbd..ea19eea 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/MultiImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/MultiImageCaptchaGenerator.java @@ -3,7 +3,7 @@ package cloud.tianai.captcha.template.slider.generator.impl; import cloud.tianai.captcha.template.slider.common.util.ObjectUtils; import cloud.tianai.captcha.template.slider.generator.AbstractImageCaptchaGenerator; import cloud.tianai.captcha.template.slider.generator.ImageCaptchaGenerator; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import cloud.tianai.captcha.template.slider.generator.common.model.dto.GenerateParam; import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; import cloud.tianai.captcha.template.slider.resource.ImageCaptchaResourceManager; diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardClickImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardClickImageCaptchaGenerator.java deleted file mode 100644 index a00a866..0000000 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardClickImageCaptchaGenerator.java +++ /dev/null @@ -1,217 +0,0 @@ -package cloud.tianai.captcha.template.slider.generator.impl; - -import cloud.tianai.captcha.template.slider.common.util.CaptchaImageUtils; -import cloud.tianai.captcha.template.slider.common.util.FontUtils; -import cloud.tianai.captcha.template.slider.generator.AbstractImageCaptchaGenerator; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; -import cloud.tianai.captcha.template.slider.generator.common.model.dto.GenerateParam; -import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; -import cloud.tianai.captcha.template.slider.generator.common.model.dto.WordClickImageCaptchaInfo; -import cloud.tianai.captcha.template.slider.resource.ImageCaptchaResourceManager; -import cloud.tianai.captcha.template.slider.resource.ResourceStore; -import cloud.tianai.captcha.template.slider.resource.common.model.dto.Resource; -import cloud.tianai.captcha.template.slider.resource.impl.provider.ClassPathResourceProvider; -import cloud.tianai.captcha.template.slider.resource.impl.provider.FileResourceProvider; -import lombok.Data; -import lombok.SneakyThrows; -import sun.font.FontDesignMetrics; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.image.BufferedImage; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.*; -import java.util.concurrent.ThreadLocalRandom; - -import static cloud.tianai.captcha.template.slider.common.util.CaptchaImageUtils.wrapFile2BufferedImage; -import static cloud.tianai.captcha.template.slider.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH; - -/** - * @Author: 天爱有情 - * @date 2022/4/27 11:46 - * @Description 点选验证码 - */ -@Data -public class StandardClickImageCaptchaGenerator extends AbstractImageCaptchaGenerator { - - - protected ImageCaptchaResourceManager imageCaptchaResourceManager; - - protected Integer checkFontCount = 4; - protected Integer interferenceCount = checkFontCount + 2; - protected Font font; - protected List randomColors = Arrays.asList(Color.PINK, Color.BLUE, Color.GREEN, Color.BLACK); - - @SneakyThrows - public StandardClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, boolean initDefaultResource) { - this.imageCaptchaResourceManager = imageCaptchaResourceManager; - if (initDefaultResource) { - initDefaultResource(); - } - Resource fontResource = new Resource("", "META-INF/fonts/SIMSUN.TTC"); - InputStream inputStream = new ClassPathResourceProvider().doGetResourceInputStream(fontResource); - this.font = Font.createFont(Font.TRUETYPE_FONT, inputStream); - this.font = font.deriveFont(Font.BOLD, 50); - } - - public StandardClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, - boolean initDefaultResource, - Font font) { - this.imageCaptchaResourceManager = imageCaptchaResourceManager; - this.font = font; - if (initDefaultResource) { - initDefaultResource(); - } - } - - - public void initDefaultResource() { - ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore(); - // 添加一些系统的资源文件 - resourceStore.addResource(CaptchaTypeConstant.WORD_CLICK, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg"))); - } - - - @Override - public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) { - - // 文字点选验证码不需要模板 只需要背景图 - Collection inputStreams = new LinkedList<>(); - try { - Resource resourceImage = imageCaptchaResourceManager.randomGetResource(param.getType()); - InputStream resourceInputStream = imageCaptchaResourceManager.getResourceInputStream(resourceImage); - inputStreams.add(resourceInputStream); - BufferedImage bgImage = wrapFile2BufferedImage(resourceInputStream); - Graphics2D graphics = (Graphics2D) bgImage.getGraphics(); - - List wordDefinitionList = new ArrayList<>(interferenceCount); - for (int i = 0; i < interferenceCount; i++) { - // 随机角度 - - FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font); - // 随机文字 - String randomWord = String.valueOf(FontUtils.getRandomChar()); - int wordWidth = metrics.stringWidth(randomWord); - int wordHeight = metrics.getHeight(); - // 随机颜色 - Color randomColor = randomColor(); - // 随机x - int randomX = ThreadLocalRandom.current().nextInt(10, bgImage.getWidth() - wordWidth); - // 随机y - int randomY = ThreadLocalRandom.current().nextInt(10, bgImage.getHeight() - wordHeight); - int randomDeg = ThreadLocalRandom.current().nextInt(0, 85); - AffineTransform affineTransform = new AffineTransform(); - affineTransform.rotate(Math.toRadians(randomDeg)); - Font rotatedFont = font.deriveFont(affineTransform); - graphics.setFont(rotatedFont); - graphics.setColor(randomColor); - graphics.drawString(randomWord, randomX, randomY + metrics.getAscent()); - - WordClickImageCaptchaInfo.WordDefinition wordDefinition = new WordClickImageCaptchaInfo.WordDefinition(); - wordDefinition.setWord(randomWord); - wordDefinition.setX(randomX + wordWidth / 2); - wordDefinition.setY(randomY + wordHeight / 2); - wordDefinition.setDeg(randomDeg); - wordDefinition.setWordWidth(wordWidth); - wordDefinition.setWordHeight(wordHeight); - wordDefinitionList.add(wordDefinition); - } - // 背景图转换为字符串 -// String bgImageStr = transform(bgImage, param.getBackgroundFormatName()); - try { - ImageIO.write(bgImage, "jpeg", new FileOutputStream("C:\\Users\\Thinkpad\\Desktop\\123.jpg")); - } catch (IOException e) { - e.printStackTrace(); - } - - // 打乱 word - Collections.shuffle(wordDefinitionList); - // 拿出要校验的文字 - List checkWordDefinitionList = new ArrayList<>(checkFontCount); - for (int i = 0; i < checkFontCount; i++) { - checkWordDefinitionList.add(wordDefinitionList.get(i)); - } - - System.out.println(checkWordDefinitionList); - return null; - - } finally { - // 使用完后关闭流 - for (InputStream inputStream : inputStreams) { - try { - inputStream.close(); - } catch (IOException e) { - // ignore - } - } - } - -// return null; - } - - protected Color randomColor() { - if (randomColors.size() == 1) { - return randomColors.get(0); - } - return randomColors.get(ThreadLocalRandom.current().nextInt(0, randomColors.size() - 1)); - } - - @Override - public ImageCaptchaResourceManager getImageResourceManager() { - return null; - } - - - public static void main(String[] args) throws IOException, FontFormatException { - Resource fontResource = new Resource("", "META-INF/fonts/SIMSUN.TTC"); - InputStream inputStream = new ClassPathResourceProvider().doGetResourceInputStream(fontResource); - Font font = Font.createFont(Font.TRUETYPE_FONT, inputStream); - font = font.deriveFont(Font.BOLD, 100); - - - for (int i = 40; i < 60; i+=1) { - FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font); - - Resource imageResource = new Resource("", "C:\\Users\\Thinkpad\\Desktop\\111.jpg"); - inputStream = new FileResourceProvider().doGetResourceInputStream(imageResource); - BufferedImage bufferedImage = CaptchaImageUtils.wrapFile2BufferedImage(inputStream); - - Graphics graphics = bufferedImage.getGraphics(); - graphics.setColor(Color.PINK); - //设置角度 - AffineTransform affineTransform = new AffineTransform(); - affineTransform.rotate(Math.toRadians(180), i , -i); - Font rotatedFont = font.deriveFont(affineTransform); - graphics.setFont(rotatedFont); - char ch = FontUtils.getRandomChar(); -// graphics.drawString(String.valueOf(ch), 10, 10+metrics.getAscent()); -// graphics.drawString(String.valueOf(ch), 50, 50); - String randomWord = "张"/*String.valueOf(FontUtils.getRandomChar())*/; - int wordWidth = metrics.stringWidth(randomWord); - int wordHeight = metrics.getHeight(); -// //左边位置 - int left = (bufferedImage.getWidth()-wordWidth)/2; -// //顶边位置+上升距离(原本字体基线位置对准画布的y坐标导致字体偏上ascent距离,加上ascent后下移刚好顶边吻合) - int top = (bufferedImage.getHeight()-wordHeight)/2+metrics.getAscent(); - - // 随机生成6个字的居中的 x 和 y -// for (int i = 0; i < 6; i++) { - - -// int randomX = ThreadLocalRandom.current().nextInt(10, bufferedImage.getWidth() - wordWidth); -// int randomY = ThreadLocalRandom.current().nextInt(10, bufferedImage.getHeight() - wordHeight); - - graphics.drawString(randomWord, left, top); - - System.out.println(randomWord + "-->x:" + (left + wordWidth / 2) + ",y:" + (left + wordHeight / 2)); -// } - -// graphics.drawString("居中文字",0,metrics.getAscent()); //基线对齐改为顶边对齐 - ImageIO.write(bufferedImage, "jpeg", new FileOutputStream("C:\\Users\\Thinkpad\\Desktop\\111\\"+i+".jpg")); - } - } -} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardConcatImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardConcatImageCaptchaGenerator.java index a88f65d..01e2d62 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardConcatImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardConcatImageCaptchaGenerator.java @@ -1,7 +1,7 @@ package cloud.tianai.captcha.template.slider.generator.impl; import cloud.tianai.captcha.template.slider.generator.AbstractImageCaptchaGenerator; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import cloud.tianai.captcha.template.slider.generator.common.model.dto.GenerateParam; import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; import cloud.tianai.captcha.template.slider.resource.ImageCaptchaResourceManager; @@ -17,7 +17,7 @@ import java.util.Collection; import java.util.LinkedList; import java.util.concurrent.ThreadLocalRandom; -import static cloud.tianai.captcha.template.slider.common.util.CaptchaImageUtils.*; +import static cloud.tianai.captcha.template.slider.generator.common.util.CaptchaImageUtils.*; import static cloud.tianai.captcha.template.slider.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH; /** diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardRandomWordClickImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardRandomWordClickImageCaptchaGenerator.java new file mode 100644 index 0000000..c78a1f7 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardRandomWordClickImageCaptchaGenerator.java @@ -0,0 +1,115 @@ +package cloud.tianai.captcha.template.slider.generator.impl; + +import cloud.tianai.captcha.template.slider.generator.common.util.CaptchaImageUtils; +import cloud.tianai.captcha.template.slider.common.util.FontUtils; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.generator.common.model.dto.ClickImageCheckDefinition; +import cloud.tianai.captcha.template.slider.resource.ImageCaptchaResourceManager; +import cloud.tianai.captcha.template.slider.resource.ResourceStore; +import cloud.tianai.captcha.template.slider.resource.common.model.dto.Resource; +import cloud.tianai.captcha.template.slider.resource.impl.provider.ClassPathResourceProvider; +import lombok.Data; +import lombok.SneakyThrows; +import sun.font.FontDesignMetrics; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +import static cloud.tianai.captcha.template.slider.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH; + +/** + * @Author: 天爱有情 + * @date 2022/4/27 11:46 + * @Description 点选验证码 + */ +@Data +public class StandardRandomWordClickImageCaptchaGenerator extends AbstractClickImageCaptchaGenerator { + + protected ImageCaptchaResourceManager imageCaptchaResourceManager; + /** 字体包.*/ + protected Font font; + protected FontDesignMetrics metrics; + protected Integer clickImgWidth = 60; + protected Integer clickImgHeight = 60; + protected int tipImageInterferenceLineNum = 2; + protected int tipImageInterferencePointNum = 5; + + @SneakyThrows + public StandardRandomWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, boolean initDefaultResource) { + this.imageCaptchaResourceManager = imageCaptchaResourceManager; + if (initDefaultResource) { + initDefaultResource(); + } + // 使用默认字体 + Resource fontResource = new Resource(null, "META-INF/fonts/SIMSUN.TTC"); + InputStream inputStream = new ClassPathResourceProvider().doGetResourceInputStream(fontResource); + Font font = Font.createFont(Font.TRUETYPE_FONT, inputStream); + font = font.deriveFont(Font.BOLD, 50); + this.metrics = FontDesignMetrics.getMetrics(font); + this.font = font; + setClickImgHeight(60); + setClickImgWidth(60); + } + + public StandardRandomWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, + boolean initDefaultResource, + Font font) { + this.imageCaptchaResourceManager = imageCaptchaResourceManager; + this.font = font; + this.metrics = FontDesignMetrics.getMetrics(font); + setClickImgWidth(font.getSize() + 10); + setClickImgHeight(font.getSize() + 10); + if (initDefaultResource) { + initDefaultResource(); + } + } + + public void initDefaultResource() { + ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore(); + // 添加一些系统的资源文件 + resourceStore.addResource(CaptchaTypeConstant.IMAGE_CLICK, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg"))); + } + + @Override + protected ImgWrapper genTipImage(List imageCheckDefinitions) { + String tips = imageCheckDefinitions.stream().map(ClickImageCheckDefinition::getTip).collect(Collectors.joining()); + // 生成随机颜色 + int fontWidth = metrics.stringWidth(tips); + int width = fontWidth + 5; + int height = metrics.getHeight() + 5; + float left = (width - fontWidth) / 2f; + float top = 5 / 2f + metrics.getAscent(); + BufferedImage bufferedImage = CaptchaImageUtils.genSimpleImgCaptcha(tips, + font, metrics, width, height, left, top, tipImageInterferenceLineNum, tipImageInterferencePointNum); + return new ImgWrapper(bufferedImage, tips); + } + + @Override + protected ImgWrapper randomGetClickImg() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + // 随机文字 + String randomWord = FontUtils.getRandomChar(random); + // 随机颜色 + Color randomColor = CaptchaImageUtils.getRandomColor(random); + // 随机角度 + int randomDeg = ThreadLocalRandom.current().nextInt(0, 85); + BufferedImage fontImage = CaptchaImageUtils.drawWordImg(randomColor, + randomWord, + font, + this.metrics, + clickImgWidth, + clickImgHeight, + randomDeg); + return new ImgWrapper(fontImage, randomWord); + } + + @Override + public ImageCaptchaResourceManager getImageResourceManager() { + return imageCaptchaResourceManager; + } + +} diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardRotateImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardRotateImageCaptchaGenerator.java index f784e57..ac929ce 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardRotateImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardRotateImageCaptchaGenerator.java @@ -1,8 +1,8 @@ package cloud.tianai.captcha.template.slider.generator.impl; -import cloud.tianai.captcha.template.slider.common.util.CaptchaImageUtils; +import cloud.tianai.captcha.template.slider.generator.common.util.CaptchaImageUtils; import cloud.tianai.captcha.template.slider.generator.AbstractImageCaptchaGenerator; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import cloud.tianai.captcha.template.slider.generator.common.constant.SliderCaptchaConstant; import cloud.tianai.captcha.template.slider.generator.common.model.dto.GenerateParam; import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; @@ -25,7 +25,7 @@ import java.util.LinkedList; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; -import static cloud.tianai.captcha.template.slider.common.util.CaptchaImageUtils.*; +import static cloud.tianai.captcha.template.slider.generator.common.util.CaptchaImageUtils.*; import static cloud.tianai.captcha.template.slider.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_RESOURCE_PATH; import static cloud.tianai.captcha.template.slider.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH; diff --git a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardSliderImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardSliderImageCaptchaGenerator.java index 1db1c2a..bf31f77 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardSliderImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/generator/impl/StandardSliderImageCaptchaGenerator.java @@ -1,7 +1,7 @@ package cloud.tianai.captcha.template.slider.generator.impl; import cloud.tianai.captcha.template.slider.generator.AbstractImageCaptchaGenerator; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import cloud.tianai.captcha.template.slider.generator.common.constant.SliderCaptchaConstant; import cloud.tianai.captcha.template.slider.generator.common.model.dto.GenerateParam; import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; @@ -22,7 +22,7 @@ import java.util.LinkedList; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; -import static cloud.tianai.captcha.template.slider.common.util.CaptchaImageUtils.*; +import static cloud.tianai.captcha.template.slider.generator.common.util.CaptchaImageUtils.*; /** * @Author: 天爱有情 diff --git a/src/main/java/cloud/tianai/captcha/template/slider/validator/ImageCaptchaValidator.java b/src/main/java/cloud/tianai/captcha/template/slider/validator/ImageCaptchaValidator.java index 7ea56e6..5c9fc2a 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/validator/ImageCaptchaValidator.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/validator/ImageCaptchaValidator.java @@ -15,11 +15,11 @@ public interface ImageCaptchaValidator { /** * 计算滑块要背景图的百分比,基本校验 * - * @param x 凹槽的x轴 - * @param bgImageWidth 背景图片的宽度 + * @param pos 移动的位置 + * @param maxPos 最大可移动的位置 * @return float */ - float calcPercentage(int x, int bgImageWidth); + float calcPercentage(Number pos, Number maxPos); /** * 校验滑块百分比 @@ -46,7 +46,7 @@ public interface ImageCaptchaValidator { * @param imageCaptchaInfo 生成的验证码数据 * @return Map */ - Map generateSliderCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo); + Map generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo); /** * 校验用户滑动滑块是否正确 diff --git a/src/main/java/cloud/tianai/captcha/template/slider/validator/impl/SimpleImageCaptchaValidator.java b/src/main/java/cloud/tianai/captcha/template/slider/validator/impl/SimpleImageCaptchaValidator.java index cd9383f..3643f6a 100644 --- a/src/main/java/cloud/tianai/captcha/template/slider/validator/impl/SimpleImageCaptchaValidator.java +++ b/src/main/java/cloud/tianai/captcha/template/slider/validator/impl/SimpleImageCaptchaValidator.java @@ -1,8 +1,10 @@ package cloud.tianai.captcha.template.slider.validator.impl; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.util.CaptchaUtils; import cloud.tianai.captcha.template.slider.common.util.CollectionUtils; import cloud.tianai.captcha.template.slider.common.util.ObjectUtils; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.generator.common.model.dto.ClickImageCheckDefinition; import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; import cloud.tianai.captcha.template.slider.validator.ImageCaptchaValidator; import cloud.tianai.captcha.template.slider.validator.common.model.dto.SliderCaptchaTrack; @@ -40,8 +42,8 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator { } @Override - public float calcPercentage(int x, int bgImageWidth) { - return (float) x / bgImageWidth; + public float calcPercentage(Number pos, Number maxPos) { + return pos.floatValue() / maxPos.floatValue(); } @Override @@ -62,9 +64,8 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator { } @Override - public Map generateSliderCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) { + public Map generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) { Map map = new HashMap<>(8); - addPercentage(imageCaptchaInfo, map); // 容错值 Float tolerant = imageCaptchaInfo.getTolerant(); if (tolerant != null && tolerant > 0) { @@ -76,17 +77,58 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator { type = CaptchaTypeConstant.SLIDER; } map.put(TYPE_KEY, type); + + return afterGenerateImageCaptchaValidData(map, imageCaptchaInfo, type); + } + + public Map afterGenerateImageCaptchaValidData(Map map, + ImageCaptchaInfo imageCaptchaInfo, + String type) { + if (CaptchaUtils.isSliderCaptcha(type)) { + // 滑动验证码 + addPercentage(imageCaptchaInfo, map); + } else if (CaptchaUtils.isClickCaptcha(type)) { + // 图片点选验证码 + Object expand = imageCaptchaInfo.getExpand(); + if (expand == null) { + throw new IllegalArgumentException("点选验证码扩展数据转换为 List 失败, info=" + imageCaptchaInfo); + } + List clickImageCheckDefinitionList = null; + try { + clickImageCheckDefinitionList = (List) expand; + } catch (Exception e) { + throw new IllegalArgumentException("点选验证码扩展数据转换为 List 失败, info=" + imageCaptchaInfo); + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < clickImageCheckDefinitionList.size(); i++) { + ClickImageCheckDefinition definition = clickImageCheckDefinitionList.get(i); + Integer x = definition.getX(); + Integer y = definition.getY(); + Integer width = imageCaptchaInfo.getBgImageWidth(); + Integer height = imageCaptchaInfo.getBgImageHeight(); + float vx = calcPercentage(x, width); + float vy = calcPercentage(y, height); + sb.append(vx).append(",").append(vy).append(";"); + if (i == 0 && !map.containsKey(TOLERANT_KEY)) { + // 重新计算容错值 + float minLeft = calcPercentage(x - definition.getWidth() / 2f, width); + float tolerant = vx - minLeft; + map.put(TOLERANT_KEY, tolerant); + } + } + // 添加点选验证 + map.put(PERCENTAGE_KEY, sb.toString()); + } return map; } @Override public boolean valid(SliderCaptchaTrack sliderCaptchaTrack, Map sliderCaptchaValidData) { - Float oriPercentage = getFloatParam(PERCENTAGE_KEY, sliderCaptchaValidData); + // 读容错值 Float tolerantData = getFloatParam(TOLERANT_KEY, sliderCaptchaValidData, defaultTolerant); // 读验证码类型 String type = getStringParam(TYPE_KEY, sliderCaptchaValidData, CaptchaTypeConstant.SLIDER); - Integer bgImageWidth = sliderCaptchaTrack.getBgImageWidth(); if (bgImageWidth == null || bgImageWidth < 1) { // 没有背景图片宽度 @@ -97,34 +139,90 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator { // 没有滑动轨迹 return false; } - return doValid(sliderCaptchaTrack, sliderCaptchaValidData, oriPercentage, tolerantData, type); + return afterValid(sliderCaptchaTrack, sliderCaptchaValidData, tolerantData, type); } - public boolean doValid(SliderCaptchaTrack sliderCaptchaTrack, - Map sliderCaptchaValidData, - Float oriPercentage, - Float tolerant, - String type) { - if (CaptchaTypeConstant.SLIDER.equals(type) - || CaptchaTypeConstant.ROTATE.equals(type) - || CaptchaTypeConstant.CONCAT.equals(type)) { - if (oriPercentage == null) { - // 没读取到百分比 - return false; - } - List trackList = sliderCaptchaTrack.getTrackList(); - // 取最后一个滑动轨迹 - SliderCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); - // 计算百分比 - float calcPercentage = calcPercentage(lastTrack.getX(), sliderCaptchaTrack.getBgImageWidth()); - // 校验百分比 - return checkPercentage(calcPercentage, oriPercentage, tolerant); + public boolean afterValid(SliderCaptchaTrack sliderCaptchaTrack, + Map sliderCaptchaValidData, + Float tolerant, + String type) { + if (CaptchaUtils.isSliderCaptcha(type)) { + // 滑动类型验证码 + return doValidSliderCaptcha(sliderCaptchaTrack, sliderCaptchaValidData, tolerant, type); + } else if (CaptchaUtils.isClickCaptcha(type)) { + // 点选类型验证码 + return doValidClickCaptcha(sliderCaptchaTrack, sliderCaptchaValidData, tolerant, type); } // 不支持的类型 log.warn("校验验证码警告, 不支持的验证码类型:{}, 请手动扩展 cloud.tianai.captcha.template.slider.validator.impl.SimpleImageCaptchaValidator.doValid 进行校验扩展", type); return false; } + /** + * 校验点选验证码 + * @param sliderCaptchaTrack sliderCaptchaTrack + * @param sliderCaptchaValidData sliderCaptchaValidData + * @param tolerant tolerant + * @param type type + * @return boolean + */ + public boolean doValidClickCaptcha(SliderCaptchaTrack sliderCaptchaTrack, + Map sliderCaptchaValidData, + Float tolerant, + String type) { + String validStr = getStringParam(PERCENTAGE_KEY, sliderCaptchaValidData, null); + if (ObjectUtils.isEmpty(validStr)) { + return false; + } + String[] splitArr = validStr.split(";"); + List trackList = sliderCaptchaTrack.getTrackList(); + if (trackList.size() != splitArr.length) { + return false; + } + for (int i = 0; i < splitArr.length; i++) { + SliderCaptchaTrack.Track track = trackList.get(i); + String posStr = splitArr[i]; + String[] posArr = posStr.split(","); + float xPercentage = Float.parseFloat(posArr[0]); + float yPercentage = Float.parseFloat(posArr[1]); + + float calcXPercentage = calcPercentage(track.getX(), sliderCaptchaTrack.getBgImageWidth()); + float calcYPercentage = calcPercentage(track.getY(), sliderCaptchaTrack.getBgImageWidth()); + + if (!checkPercentage(calcXPercentage, xPercentage, tolerant) + || !checkPercentage(calcYPercentage, yPercentage, tolerant)) { + return false; + } + } + return true; + } + + /** + * 校验滑动验证码 + * @param sliderCaptchaTrack sliderCaptchaTrack + * @param sliderCaptchaValidData sliderCaptchaValidData + * @param tolerant tolerant + * @param type type + * @return boolean + */ + public boolean doValidSliderCaptcha(SliderCaptchaTrack sliderCaptchaTrack, + Map sliderCaptchaValidData, + Float tolerant, + String type) { + Float oriPercentage = getFloatParam(PERCENTAGE_KEY, sliderCaptchaValidData); + if (oriPercentage == null) { + // 没读取到百分比 + return false; + } + List trackList = sliderCaptchaTrack.getTrackList(); + // 取最后一个滑动轨迹 + SliderCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); + // 计算百分比 + float calcPercentage = calcPercentage(lastTrack.getX(), sliderCaptchaTrack.getBgImageWidth()); + // 校验百分比 + return checkPercentage(calcPercentage, oriPercentage, tolerant); + } + protected Float getFloatParam(String key, Map sliderCaptchaValidData) { return getFloatParam(key, sliderCaptchaValidData, null); } diff --git a/src/main/test/java/example/AnchorPoint.java b/src/main/test/java/example/AnchorPoint.java new file mode 100644 index 0000000..77b5850 --- /dev/null +++ b/src/main/test/java/example/AnchorPoint.java @@ -0,0 +1,85 @@ +package example; + + +import java.io.Serializable; + +public class AnchorPoint implements Serializable { + + private static final long serialVersionUID = 2L; + + + /** + * 保持charImgWidth的最大值+1 + */ + private Integer x_iv = 26; + /** + * 保持charImgHeight的最大值+1 + */ + private Integer y_iv = 21; + + private Integer x; + private Integer y; + + public AnchorPoint(){ + this.x = 0; + this.y = 0; + } + + + public AnchorPoint(Integer x, Integer y){ + this.x = x; + this.y = y; + } + + public Integer getX() { + return x; + } + + + public Integer getY() { + return y; + } + + public void setX(Integer x) { + this.x = x; + } + + public void setY(Integer y) { + this.y = y; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AnchorPoint that = (AnchorPoint) o; + + //当X位置小于偏移量进行判段Y轴的偏移量 + if(this.x+this.x_iv > that.getX() || this.x - this.x_iv < that.getX()){ + //当Y轴的偏移量符合安全距离 判断锚点有效 + if(this.y+this.y_iv < that.getY() || this.y-this.y_iv > that.getY()){ + return false; + } + }else{ + //两个对象的X点保持在偏移量之外无需比较Y轴位置,判定锚点有效 + return false; + } + + + return true; + + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public String toString() { + return "DrawXInteger{" + + "x=" + x + + ", y=" + y + + '}'; + } +} \ No newline at end of file diff --git a/src/main/test/java/example/DrawCaptchaUtil.java b/src/main/test/java/example/DrawCaptchaUtil.java new file mode 100644 index 0000000..6798520 --- /dev/null +++ b/src/main/test/java/example/DrawCaptchaUtil.java @@ -0,0 +1,250 @@ +package example; + +import cloud.tianai.captcha.template.slider.resource.common.model.dto.Resource; +import cloud.tianai.captcha.template.slider.resource.impl.provider.ClassPathResourceProvider; +import lombok.SneakyThrows; +import sun.font.FontDesignMetrics; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 绘制验证码 + */ +public class DrawCaptchaUtil { + + /** + * 画布宽度 + */ + private static int canvasWidth = 300; + + /** + * 画布高度 + */ + private static int canvasHeight = 150; + + + /** + * 图片宽度 + */ + private static int imgWidth = 300; + + /** + * 图片高度 + */ + private static int imgHeight = 150; + + /** + * 图中字体数量 + */ + private static int charNumber = 5; + + /** + * 字体大小 + */ + private static int fontSize = 50; + /** + * 字体图片宽度 + */ + private static int charImgWidth = 60; + /** + * 字体图片高度 + */ + private static int charImgHeight = 60; + + + /** + * 绘制验证码图片 + * @param checkCode + * @param bkDirPath + * @return + * @throws IOException + */ + public static BufferedImage drawImg(LinkedHashMap checkCode, String bkDirPath) throws IOException { + + BufferedImage imgCanvas = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_BGR); + Graphics graphics = imgCanvas.getGraphics(); + + + //绘制图形 +// graphics.setColor(new Color(0,0,0,0)); + graphics.fillRect(0, 0, imgWidth, imgHeight); + + File bkPath = randomBackGroundImage(bkDirPath); + + BufferedImage read = ImageIO.read(Files.newInputStream(Paths.get(bkPath.getAbsolutePath()))); + graphics.drawImage(read, 0, 0, null, null); + //绘制字体 +// graphics.setColor(getRandomColor()); + //随机在图片中生成5个绘制点每个绘制点半径能不能重叠 + Set point = new HashSet<>(); + + addPoint(point); + Object[] pointArr = point.toArray(); + List charList = new ArrayList<>(); + for (int i = 0; i < charNumber; i++) { + AnchorPoint p = (AnchorPoint) pointArr[i]; + graphics.drawImage(getCharImg(getRandomColor(), charList), p.getX(), p.getY(), null, null); + + } + //随机获取3个字符串作为校验对象 + Object[] charArr = charList.toArray(); + Random random = new Random(); + for (int i = 0; i < 3; i++) { + int num = random.nextInt(pointArr.length - i); + String key = (String) charArr[num]; + AnchorPoint value = (AnchorPoint) pointArr[num]; + checkCode.put(key, value); + //置换位置 + Object a = pointArr[num]; + pointArr[num] = pointArr[pointArr.length - 1 - i]; + pointArr[pointArr.length - 1 - i] = a; + + a = charArr[num]; + charArr[num] = charArr[charArr.length - 1 - i]; + charArr[charArr.length - 1 - i] = a; + } + + return imgCanvas; + } + + /** + * 在指定目录下随机获取背景图片 + * @param dir + * @return + */ + private static File randomBackGroundImage(String dir) { + File file = new File(dir); + if (file.isDirectory()) { + File[] files = file.listFiles(); + Random random = new Random(); + int i = random.nextInt(files.length); + return files[i]; + } else { + return file; + } + } + + /** + * 添加坐标点 + * @param point + */ + private static void addPoint(Set point) { + Random random = new Random(); + //避免因为坐标点过于特殊导致无法获取下一个坐标点进入死循环,到100置空重新获取 + int clearNumber = 0; + //生成前2个点坐标 + while (point.size() < 2) { + + //添加X坐标点 + point.add(new AnchorPoint(random.nextInt(canvasWidth / 3), random.nextInt(canvasHeight - charImgWidth))); + if (clearNumber == 100) { + point.clear(); + clearNumber = 0; + } + clearNumber++; + + } + clearNumber = 0; + //生成后3个点坐标 + while (point.size() < charNumber) { + //添加X坐标点 + point.add(new AnchorPoint(random.nextInt(canvasWidth - charImgWidth), random.nextInt(canvasHeight - charImgHeight))); + if (clearNumber == 100) { + point.clear(); + clearNumber = 0; + addPoint(point); + break; + } + clearNumber++; + } + + + } + + /** + * 获取文字图片 + * @param fontColor + * @param charList + * @return + * @throws UnsupportedEncodingException + */ + @SneakyThrows + private static BufferedImage getCharImg(Color fontColor, List charList) throws IOException { + BufferedImage fillRect = new BufferedImage(charImgWidth, charImgHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = fillRect.createGraphics(); + g.setColor(new Color(0, 0, 0, 0)); + g.fillRect(0, 0, charImgWidth, charImgHeight); + g.setColor(fontColor); + Resource fontResource = new Resource("", "META-INF/fonts/SIMSUN.TTC"); + InputStream inputStream = new ClassPathResourceProvider().doGetResourceInputStream(fontResource); + Font font = Font.createFont(Font.TRUETYPE_FONT, inputStream); + font = font.deriveFont(Font.BOLD, fontSize); +// Font font = new Font("微软雅黑", Font.BOLD, fontSize); + FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font); +// AffineTransform affineTransform = new AffineTransform(); +// affineTransform.rotate(Math.toRadians(85), 0, 0); +// font.deriveFont(affineTransform); + g.setFont(font); + String charStr = getChineseCharacters(); + charList.add(charStr); + + float left = (60 - 50) / 2f; +// //顶边位置+上升距离(原本字体基线位置对准画布的y坐标导致字体偏上ascent距离,加上ascent后下移刚好顶边吻合) + float top = (60 - 50) / 2f + metrics.getAscent() - 10; + + g.rotate(Math.toRadians(ThreadLocalRandom.current().nextInt(0, 85)), 30, 30); + g.drawString(charStr, left, top); + g.dispose(); + return fillRect; + } + + /** + * 随机获取颜色 + * @return + */ + private static Color getRandomColor() { + Random random = new Random(); + return new Color( + random.nextInt(255), + random.nextInt(255), + random.nextInt(255)); + + } + + /** + * 随机获取中文字 + * @return + * @throws UnsupportedEncodingException + */ + private static String getChineseCharacters() throws UnsupportedEncodingException { + Integer hightPos, lowPos; // 定义高低位 + Random random = new Random(); + hightPos = (176 + Math.abs(random.nextInt(39))); + lowPos = (161 + Math.abs(random.nextInt(93))); + byte[] bytes = new byte[2]; + bytes[0] = hightPos.byteValue(); + bytes[1] = lowPos.byteValue(); + return new String(bytes, "GBK"); + + } + + + public static void main(String[] args) throws IOException { + for (int i = 0; i < 100; i++) { + BufferedImage charImg = getCharImg(getRandomColor(), new ArrayList<>()); + ImageIO.write(charImg, "png", new FileOutputStream("C:\\Users\\tianai\\Desktop\\111\\" + i + ".png")); + + } + } + + +} \ No newline at end of file diff --git a/src/main/test/java/example/StandardRotateCaptchaGeneratorTest.java b/src/main/test/java/example/StandardRotateCaptchaGeneratorTest.java index d310386..3c77da5 100644 --- a/src/main/test/java/example/StandardRotateCaptchaGeneratorTest.java +++ b/src/main/test/java/example/StandardRotateCaptchaGeneratorTest.java @@ -1,7 +1,6 @@ package example; -import cloud.tianai.captcha.template.slider.generator.ImageCaptchaGenerator; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; import cloud.tianai.captcha.template.slider.generator.common.model.dto.RotateImageCaptchaInfo; import cloud.tianai.captcha.template.slider.generator.impl.CacheImageCaptchaGenerator; diff --git a/src/main/test/java/example/StandardWordClickImageCaptchaGeneratorTest.java b/src/main/test/java/example/StandardWordClickImageCaptchaGeneratorTest.java index 4ef48f8..9744bda 100644 --- a/src/main/test/java/example/StandardWordClickImageCaptchaGeneratorTest.java +++ b/src/main/test/java/example/StandardWordClickImageCaptchaGeneratorTest.java @@ -1,23 +1,33 @@ package example; -import cloud.tianai.captcha.template.slider.generator.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.template.slider.common.constant.CaptchaTypeConstant; import cloud.tianai.captcha.template.slider.generator.common.model.dto.GenerateParam; import cloud.tianai.captcha.template.slider.generator.common.model.dto.ImageCaptchaInfo; -import cloud.tianai.captcha.template.slider.generator.impl.StandardClickImageCaptchaGenerator; +import cloud.tianai.captcha.template.slider.generator.impl.StandardRandomWordClickImageCaptchaGenerator; import cloud.tianai.captcha.template.slider.resource.ImageCaptchaResourceManager; import cloud.tianai.captcha.template.slider.resource.impl.DefaultImageCaptchaResourceManager; +import cloud.tianai.captcha.template.slider.validator.impl.BasicCaptchaTrackValidator; + +import java.util.Map; public class StandardWordClickImageCaptchaGeneratorTest { public static void main(String[] args) { ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); - StandardClickImageCaptchaGenerator defaultImageCaptchaResourceManager = - new StandardClickImageCaptchaGenerator(imageCaptchaResourceManager, true); + StandardRandomWordClickImageCaptchaGenerator defaultImageCaptchaResourceManager = + new StandardRandomWordClickImageCaptchaGenerator(imageCaptchaResourceManager, true); GenerateParam generateParam = new GenerateParam(); - generateParam.setType(CaptchaTypeConstant.WORD_CLICK); + generateParam.setType(CaptchaTypeConstant.IMAGE_CLICK); + generateParam.setSliderFormatName("png"); ImageCaptchaInfo imageCaptchaInfo = defaultImageCaptchaResourceManager.generateCaptchaImage(generateParam); + BasicCaptchaTrackValidator basicCaptchaTrackValidator = new BasicCaptchaTrackValidator(); + Map stringObjectMap = basicCaptchaTrackValidator.generateImageCaptchaValidData(imageCaptchaInfo); + + System.out.println(stringObjectMap); + + System.out.println(imageCaptchaInfo); } }