From 0fcb8555e2e5fd3b0ed22b5fd6cdaf3c2e114257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BB=81=E7=A6=A7?= Date: Fri, 27 Jun 2025 10:35:15 +0800 Subject: [PATCH 1/2] feat: add picture word function use SPI --- core/captcha-plus/pom.xml | 2 +- .../captcha/model/common/CaptchaTypeEnum.java | 4 + .../impl/BlockPuzzleCaptchaServiceImpl.java | 6 - .../impl/ClickWordCaptchaServiceImpl.java | 6 - .../impl/DefaultCaptchaServiceImpl.java | 6 - .../impl/PictureWordCaptchaServiceImpl.java | 166 ++++++++++++++++++ ...com.xingyuv.captcha.service.CaptchaService | 1 + core/spring-boot-starter-captcha-plus/pom.xml | 2 +- pom.xml | 2 +- 9 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/PictureWordCaptchaServiceImpl.java diff --git a/core/captcha-plus/pom.xml b/core/captcha-plus/pom.xml index 2338436..751573b 100644 --- a/core/captcha-plus/pom.xml +++ b/core/captcha-plus/pom.xml @@ -6,7 +6,7 @@ com.xingyuv captcha-plus - 1.0.10 + 1.0.11 ${project.groupId}:${project.artifactId} captcha-plus.jar diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/model/common/CaptchaTypeEnum.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/model/common/CaptchaTypeEnum.java index ce3451e..145e9b6 100644 --- a/core/captcha-plus/src/main/java/com/xingyuv/captcha/model/common/CaptchaTypeEnum.java +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/model/common/CaptchaTypeEnum.java @@ -9,6 +9,10 @@ public enum CaptchaTypeEnum { * 滑块拼图. */ BLOCKPUZZLE("blockPuzzle", "滑块拼图"), + /** + * 文字图片. + */ + PICTURE_WORD("pictureWord", "文字图片"), /** * 文字点选. */ diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java index 32c9a83..5313517 100644 --- a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java @@ -1,9 +1,3 @@ -/* - *Copyright © 2018 anji-plus - *安吉加加信息技术有限公司 - *http://www.anji-plus.com - *All rights reserved. - */ package com.xingyuv.captcha.service.impl; import com.xingyuv.captcha.model.common.CaptchaTypeEnum; diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/ClickWordCaptchaServiceImpl.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/ClickWordCaptchaServiceImpl.java index 78f967c..dfdc6ce 100644 --- a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/ClickWordCaptchaServiceImpl.java +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/ClickWordCaptchaServiceImpl.java @@ -1,9 +1,3 @@ -/* - *Copyright © 2018 anji-plus - *安吉加加信息技术有限公司 - *http://www.anji-plus.com - *All rights reserved. - */ package com.xingyuv.captcha.service.impl; import com.xingyuv.captcha.model.common.CaptchaTypeEnum; diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/DefaultCaptchaServiceImpl.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/DefaultCaptchaServiceImpl.java index eac7bbc..c89ab38 100644 --- a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/DefaultCaptchaServiceImpl.java +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/DefaultCaptchaServiceImpl.java @@ -1,9 +1,3 @@ -/* - *Copyright © 2018 anji-plus - *安吉加加信息技术有限公司 - *http://www.anji-plus.com - *All rights reserved. - */ package com.xingyuv.captcha.service.impl; import com.xingyuv.captcha.model.common.RepCodeEnum; diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/PictureWordCaptchaServiceImpl.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/PictureWordCaptchaServiceImpl.java new file mode 100644 index 0000000..bfc464a --- /dev/null +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/PictureWordCaptchaServiceImpl.java @@ -0,0 +1,166 @@ +package com.xingyuv.captcha.service.impl; + +import com.xingyuv.captcha.model.common.CaptchaTypeEnum; +import com.xingyuv.captcha.model.common.RepCodeEnum; +import com.xingyuv.captcha.model.common.ResponseModel; +import com.xingyuv.captcha.model.vo.CaptchaVO; +import com.xingyuv.captcha.model.vo.PointVO; +import com.xingyuv.captcha.util.*; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.Properties; +import java.util.Random; + +/** + * 图片文字验证码 + * + * @author Rex + * @since 2025/6/25 16:44 + */ +public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService { + + private static final int WIDTH = 120; + private static final int HEIGHT = 40; + private static final int LINES = 5; + private static final Random RANDOM = new Random(); + + @Override + public void init(Properties config) { + super.init(config); + } + + @Override + public void destroy(Properties config) { + logger.info("start-clear-history-data-{}", captchaType()); + } + + @Override + public String captchaType() { + return CaptchaTypeEnum.PICTURE_WORD.getCodeValue(); + } + + @Override + public ResponseModel get(CaptchaVO captchaVO) { + String text = captchaVO.getCaptchaId(); + BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + + // 设置背景色 + g.setColor(getRandomColor(200, 250)); + g.fillRect(0, 0, WIDTH, HEIGHT); + + // 绘制干扰线 + for (int i = 0; i < LINES; i++) { + g.setColor(getRandomColor(100, 200)); + int x1 = RANDOM.nextInt(WIDTH); + int y1 = RANDOM.nextInt(HEIGHT); + int x2 = RANDOM.nextInt(WIDTH); + int y2 = RANDOM.nextInt(HEIGHT); + g.drawLine(x1, y1, x2, y2); + } + + // 设置字体 + g.setFont(new Font("Arial", Font.BOLD, 24)); + + // 绘制验证码文本 + for (int i = 0; i < text.length(); i++) { + g.setColor(getRandomColor(20, 130)); + g.drawString(text.charAt(i) + "", 20 + i * 20, 28 + RANDOM.nextInt(8)); + } + + // 添加噪点 + for (int i = 0; i < 100; i++) { + int x = RANDOM.nextInt(WIDTH); + int y = RANDOM.nextInt(HEIGHT); + image.setRGB(x, y, getRandomColor(0, 255).getRGB()); + } + + g.dispose(); + return ResponseModel.successData(image); + } + + @Override + public ResponseModel check(CaptchaVO captchaVO) { + ResponseModel r = super.check(captchaVO); + if (!validatedReq(r)) { + return r; + } + //取坐标信息 + String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken()); + if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) { + return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID); + } + String s = CaptchaServiceFactory.getCache(cacheType).get(codeKey); + //验证码只用一次,即刻失效 + CaptchaServiceFactory.getCache(cacheType).delete(codeKey); + PointVO point; + PointVO point1; + String pointJson; + try { + point = JsonUtil.parseObject(s, PointVO.class); + //aes解密 + pointJson = decrypt(captchaVO.getPointJson(), point.getSecretKey()); + point1 = JsonUtil.parseObject(pointJson, PointVO.class); + } catch (Exception e) { + logger.error("验证码坐标解析失败", e); + afterValidateFail(captchaVO); + return ResponseModel.errorMsg(e.getMessage()); + } + boolean exists = point.x - Integer.parseInt(slipOffset) > point1.x + || point1.x > point.x + Integer.parseInt(slipOffset) + || point.y != point1.y; + if (exists) { + afterValidateFail(captchaVO); + return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR); + } + //校验成功,将信息存入缓存 + String secretKey = point.getSecretKey(); + String value; + try { + value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(pointJson), secretKey); + } catch (Exception e) { + logger.error("AES加密失败", e); + afterValidateFail(captchaVO); + return ResponseModel.errorMsg(e.getMessage()); + } + String secondKey = String.format(REDIS_SECOND_CAPTCHA_KEY, value); + CaptchaServiceFactory.getCache(cacheType).set(secondKey, captchaVO.getToken(), EXPIRESIN_THREE); + captchaVO.setResult(true); + captchaVO.resetClientFlag(); + return ResponseModel.successData(captchaVO); + } + + @Override + public ResponseModel verification(CaptchaVO captchaVO) { + ResponseModel r = super.verification(captchaVO); + if (!validatedReq(r)) { + return r; + } + try { + String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification()); + if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) { + return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID); + } + //二次校验取值后,即刻失效 + CaptchaServiceFactory.getCache(cacheType).delete(codeKey); + } catch (Exception e) { + logger.error("验证码坐标解析失败", e); + return ResponseModel.errorMsg(e.getMessage()); + } + return ResponseModel.success(); + } + + private Color getRandomColor(int min, int max) { + if (min > max) { + int temp = min; + min = max; + max = temp; + } + int r = min + RANDOM.nextInt(max - min); + int g = min + RANDOM.nextInt(max - min); + int b = min + RANDOM.nextInt(max - min); + return new Color(r, g, b); + } + +} diff --git a/core/captcha-plus/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaService b/core/captcha-plus/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaService index 13751ab..0830562 100644 --- a/core/captcha-plus/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaService +++ b/core/captcha-plus/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaService @@ -1,4 +1,5 @@ com.xingyuv.captcha.service.impl.BlockPuzzleCaptchaServiceImpl com.xingyuv.captcha.service.impl.ClickWordCaptchaServiceImpl +com.xingyuv.captcha.service.impl.PictureWordCaptchaServiceImpl com.xingyuv.captcha.service.impl.DefaultCaptchaServiceImpl #com.xingyuv.captcha.service.impl.RotatePuzzleCaptchaServiceImpl \ No newline at end of file diff --git a/core/spring-boot-starter-captcha-plus/pom.xml b/core/spring-boot-starter-captcha-plus/pom.xml index 2729311..1251573 100644 --- a/core/spring-boot-starter-captcha-plus/pom.xml +++ b/core/spring-boot-starter-captcha-plus/pom.xml @@ -12,7 +12,7 @@ com.xingyuv spring-boot-starter-captcha-plus - 1.0.10 + 1.0.11 spring-boot-starter-captcha-plus spring-boot-starter-captcha-plus diff --git a/pom.xml b/pom.xml index ebddc95..bbb1dd6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.xingyuv captcha-plus-parent - 1.0.10 + 1.0.11 pom -- Gitee From f9083e8a8a963164964fb0cb242bd8807241e968 Mon Sep 17 00:00:00 2001 From: Rex Date: Fri, 27 Jun 2025 02:58:55 +0000 Subject: [PATCH 2/2] update README.md. Signed-off-by: Rex --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae6851c..18e846b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## 简介 -* Captcha-plus行为验证码,包含滑动拼图、文字点选两种方式,UI支持弹出和嵌入两种方式。后端提供Java实现,前端提供了php、angular、html、vue、uni-app、flutter、android、ios等代码示例。 +* Captcha-plus行为验证码,包含滑动拼图、文字点选、文字图片三种方式,UI支持弹出和嵌入两种方式。后端提供Java实现,前端提供了php、angular、html、vue、uni-app、flutter、android、ios等代码示例。 ## 集成项目 -- Gitee