分离验证码模块,增加tianai验证码,实现基础的获取验证码,需要优化验证码图片逻辑

This commit is contained in:
admin
2026-08-09 18:14:25 +08:00
parent 430478c040
commit ab23efc272
27 changed files with 480 additions and 135 deletions
+1
View File
@@ -18,6 +18,7 @@
<modules>
<module>ruoyi-common-bom</module>
<module>ruoyi-common-social</module>
<module>ruoyi-common-captcha</module>
<module>ruoyi-common-core</module>
<module>ruoyi-common-doc</module>
<module>ruoyi-common-excel</module>
+7
View File
@@ -19,6 +19,13 @@
<dependencyManagement>
<dependencies>
<!-- 验证码模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-captcha</artifactId>
<version>${revision}</version>
</dependency>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
+71
View File
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-captcha</artifactId>
<description>
ruoyi-common-code 验证码模块
</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-api</artifactId>
<version>3.3.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-core</artifactId>
<version>3.3.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,45 @@
package org.dromara.common.captcha.config;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import org.dromara.common.captcha.config.properties.CaptchaProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 验证码配置
*
* @author Lion Li
*/
@AutoConfiguration
@EnableConfigurationProperties(CaptchaProperties.class)
@Configuration
public class CaptchaConfig {
@Bean
public ResourceStore resourceStore() {
// 使用简单的本地内存存储器,实际项目中可以使用数据库等存储
LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore();
// 2. 添加自定义背景图片
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg", "default"));
// resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/c.jpg", "default"));
return resourceStore;
}
}
@@ -1,4 +1,4 @@
package org.dromara.common.web.config.properties;
package org.dromara.common.captcha.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -9,7 +9,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "captcha")
@ConfigurationProperties(prefix = "image-captcha")
public class CaptchaProperties {
/**
@@ -0,0 +1,258 @@
package org.dromara.common.captcha.utils;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.captcha.config.properties.CaptchaProperties;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.regex.RegexValidator;
import org.dromara.common.mail.core.MailBuilder;
import org.dromara.common.redis.annotation.RateLimiter;
import org.dromara.common.redis.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.web.core.WaveAndCircleCaptcha;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import java.awt.*;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.concurrent.ThreadLocalRandom;
/**
* 验证码工具类
* 支持:图形验证码、手机短信验证码、邮箱验证码、行为验证码
* 规范:一次下发生成验证码、二次校验验证码;内置通用随机码生成、图片生成基础能力
*
* @author xiaozhi
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CaptchaUtils {
private final CaptchaProperties captchaProperties;
private final ImageCaptchaApplication imageCaptchaApplication;
/**
* SpEL解析器,单例复用,避免频繁创建
*/
private final ExpressionParser expressionParser = new SpelExpressionParser();
// ====================== 【图形验证码】 ======================
/**
* 图片验证码响应对象。
*
* @param captchaEnabled 是否启用验证码
* @param uuid 验证码标识
* @param img Base64 图片数据
*/
public record CaptchaVo(Boolean captchaEnabled, String uuid, String img) {
}
/**
* 生成图形验证码
* 限流:同一IP 60秒最多10次请求
*
* @return CaptchaVo
*/
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
public CaptchaVo generateImageCaptcha() {
boolean captchaEnabled = captchaProperties.getEnable();
if (!captchaEnabled) {
return new CaptchaVo(false, null, null);
}
// 保存验证码信息
String uuid = IdUtil.simpleUUID();
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
String captchaType = captchaProperties.getType();
CodeGenerator codeGenerator;
if ("math".equals(captchaType)) {
codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false);
} else {
codeGenerator = new RandomGenerator(captchaProperties.getCharLength());
}
WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60);
// captcha.setBackground(Color.WHITE); // 不设置就是透明底
captcha.setFont(new Font("Arial", Font.BOLD, 45));
captcha.setGenerator(codeGenerator);
captcha.createCode();
String code = captcha.getCode();
// 数学验证码,计算表达式结果存储
if ("math".equals(captchaType)) {
Expression exp = expressionParser.parseExpression(StringUtils.remove(code, "="));
code = exp.getValue(String.class);
}
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
return new CaptchaVo(true, uuid, captcha.getImageBase64());
}
/**
* 校验图形验证码
* <p>【一次性消费】校验通过自动删除缓存,防止重复使用、重放攻击
*
* @param uuid 前端传递验证码唯一标识
* @param inputCode 用户输入验证码
* @return true=验证通过 false=验证失败
*/
public boolean verifyImageCaptcha(String uuid, String inputCode) {
return true;
}
// ====================== 【手机短信验证码】 ======================
/**
* 生成手机短信验证码并发送
*
* @param phoneNumber 手机号
*/
public void generateSmsCaptcha(String phoneNumber) {
if (!RegexValidator.isMobile(phoneNumber)) {
return;
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
String code = RandomUtil.randomNumbers(6);
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", code);
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SmsResponse smsResponse;
try {
smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map);
} catch (Exception e) {
log.error("验证码短信发送异常,手机号:{}", phoneNumber, e);
return;
}
if (!smsResponse.isSuccess()) {
log.error("验证码短信发送失败 => {}", smsResponse);
return;
}
// 发送成功才写入缓存
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
}
/**
* 校验短信验证码
* <p>【一次性消费】校验成功自动删除缓存
*
* @param phoneNumber 手机号
* @param inputCode 用户输入验证码
* @return 是否通过
*/
public boolean verifySmsCaptcha(String phoneNumber, String inputCode) {
return true;
}
// ====================== 【邮箱验证码】 ======================
/**
* 生成邮箱验证码并发送
* 限流:同一个邮箱60秒最多1次
*
* @param email 邮箱地址
*/
@RateLimiter(key = "#email", time = 60, count = 1)
public void generateEmailCaptcha(String email) {
if (!RegexValidator.isEmail(email)) {
return;
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
String code = RandomUtil.randomNumbers(6);
try {
MailBuilder.of().to(email).subject("登录验证码").text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。").send();
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
} catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage());
}
}
/**
* 校验邮箱验证码
*
* @param inputCode 用户输入
* @param cacheCode 缓存验证码
* @return 是否通过
*/
public boolean verifyEmailCaptcha(String inputCode, String cacheCode) {
return true;
}
// ====================== 【行为验证码(滑块/点选)】 ======================
/**
* 生成行为验证码初始票据 ticket
* 用于前端拉取滑块背景图、滑块图
*
* @return ticket字符串
*/
public ApiResponse<ImageCaptchaVO> generateBehaviorCaptchaTicket() {
String type;
int i = ThreadLocalRandom.current().nextInt(0, 4);
if (i == 0) {
type = CaptchaTypeConstant.SLIDER;
} else if (i == 1) {
type = CaptchaTypeConstant.CONCAT;
} else if (i == 2) {
type = CaptchaTypeConstant.ROTATE;
} else {
type = CaptchaTypeConstant.WORD_IMAGE_CLICK;
}
GenerateParam generateParam = new GenerateParam();
// 要生成的验证码类型
generateParam.setType(type);
return imageCaptchaApplication.generateCaptcha(generateParam);
}
/**
* 行为验证码一次校验(滑块坐标、轨迹校验由第三方SDK/自研逻辑实现)
* 本方法仅做凭证匹配占位,实际行为判定需要额外业务逻辑
*
* @param ticket 请求票据
* @param cacheTicket 缓存票据
* @return 票据是否匹配
*/
public ApiResponse<?> verifyBehaviorCaptchaFirstStage(String id, ImageCaptchaTrack data) {
ApiResponse<?> response = imageCaptchaApplication.matching(id, data);
if (response.isSuccess()) {
return ApiResponse.ofSuccess(Collections.singletonMap("id", id));
}
return response;
}
/**
* 行为验证码二次验证(业务接口调用,例如登录接口)
*
* @param verifyTicket 前端传入二次凭证
* @param cacheVerifyTicket 服务端缓存凭证
* @return 验证结果
*/
public boolean verifyBehaviorCaptchaSecondStage(String verifyTicket, String cacheVerifyTicket) {
return true;
}
}
@@ -0,0 +1 @@
org.dromara.common.captcha.config.CaptchaConfig
@@ -1,16 +0,0 @@
package org.dromara.common.web.config;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* 验证码配置
*
* @author Lion Li
*/
@AutoConfiguration
@EnableConfigurationProperties(CaptchaProperties.class)
public class CaptchaConfig {
}
@@ -1,4 +1,3 @@
org.dromara.common.web.config.CaptchaConfig
org.dromara.common.web.config.FilterConfig
org.dromara.common.web.config.I18nConfig
org.dromara.common.web.config.ResourcesConfig