diff --git a/README.md b/README.md
index ae6851cbe2ed9c778cd47a6af6a6f81b92ed8d02..18e846beaf4f8a26949457d45d8e84038907272d 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等代码示例。
## 集成项目
diff --git a/core/captcha-plus/pom.xml b/core/captcha-plus/pom.xml
index 2338436f0f58889289ccfdb5c9da01073f1a5dcd..751573b1e1be2c4ba8c757c982cc32c97b65944f 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 ce3451e300159b8c7d0927b9b0b641cab5d3d23d..145e9b6521f47171bce2123357a4ac4be01afba6 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 32c9a833ce18013bff1768547dfb66c2ac7e3f99..5313517413298a208cfe400c7e53322fafdfdb89 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 78f967c6bd5c63dd6efb5a55c25472a11587704a..dfdc6ce97bac84a91779ecba9afc6e0fc84b8007 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 eac7bbc02da6879425556168da88bdc9d0ac13cd..c89ab382e8d6960b9d714ef53da72b77d89717dd 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 0000000000000000000000000000000000000000..bfc464a881359e647a6b8e2bf9389b510c107295
--- /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 13751ab44553da8d6330129807a1f578a2f5620f..0830562e6aa2f20a5dca14664557753d2b9abcd9 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 2729311c102b82e7cc039e1c9cb1605e79053f35..125157327501b35a477aadcb061119cb9422c939 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 ebddc95985a489bcd0154fa7c39b07bbc1491652..bbb1dd654c780d53a06d0fb8ac363cbc6d0a9ce4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.xingyuv
captcha-plus-parent
- 1.0.10
+ 1.0.11
pom