Compare commits
1 Commits
430478c040
...
6.1
| Author | SHA1 | Date | |
|---|---|---|---|
| ab23efc272 |
@@ -22,8 +22,7 @@
|
||||
|
||||
> 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
|
||||
|
||||
> 官方前端项目地址: 基于 Vue + ElementPlus 版本前端项目 [gitee](https://gitee.com/JavaLionLi/plus-ui) - [github](https://github.com/CrazyLionCat/plus-ui) - [gitcode](https://gitcode.com/dromara/plus-ui)<br>
|
||||
> 官方前端项目地址: 基于 React + Ant Design 版本前端项目 [plus-ui-react](https://gitee.com/JavaLionLi/plus-ui/tree/6.X-React/)
|
||||
> 官方前端项目地址: [gitee](https://gitee.com/JavaLionLi/plus-ui) - [github](https://github.com/CrazyLionCat/plus-ui) - [gitcode](https://gitcode.com/dromara/plus-ui)<br>
|
||||
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://github.com/imdap/ruoyi-plus-vben5)<br>
|
||||
> 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br>
|
||||
> 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br>
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
<fory.version>1.3.0</fory.version>
|
||||
|
||||
<!-- 三方集成与业务扩展相关依赖版本 -->
|
||||
<!-- tianai验证码 -->
|
||||
<tianai-captcha.version>1.5.5</tianai-captcha.version>
|
||||
<!-- 离线IP地址定位库 -->
|
||||
<ip2region.version>3.3.7</ip2region.version>
|
||||
<!-- OSS 配置 -->
|
||||
@@ -408,6 +410,13 @@
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 行为验证码 -->
|
||||
<dependency>
|
||||
<groupId>cloud.tianai.captcha</groupId>
|
||||
<artifactId>tianai-captcha-springboot-starter</artifactId>
|
||||
<version>${tianai-captcha.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
|
||||
@@ -117,6 +117,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-captcha</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- skywalking 整合 logback -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.apache.skywalking</groupId>-->
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
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 org.dromara.common.captcha.utils.CaptchaUtils;
|
||||
import org.dromara.common.captcha.utils.CaptchaUtils.CaptchaVo;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.regex.RegexValidator;
|
||||
import org.dromara.common.mail.config.properties.MailProperties;
|
||||
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.config.properties.CaptchaProperties;
|
||||
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.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.awt.*;
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 验证码操作处理
|
||||
*
|
||||
@@ -47,9 +30,10 @@ import java.util.LinkedHashMap;
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/resource/captcha")
|
||||
public class CaptchaController {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final CaptchaUtils captchaUtils;
|
||||
private final MailProperties mailProperties;
|
||||
|
||||
/**
|
||||
@@ -59,25 +43,9 @@ public class CaptchaController {
|
||||
* @return 操作结果
|
||||
*/
|
||||
@RateLimiter(key = "#phoneNumber", time = 60, count = 1)
|
||||
@GetMapping("/resource/sms/code")
|
||||
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) {
|
||||
if (!RegexValidator.isMobile(phoneNumber)) {
|
||||
return R.fail("请输入正确的手机号!");
|
||||
}
|
||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
|
||||
String code = RandomUtil.randomNumbers(4);
|
||||
// 验证码模板id 自行处理 (查数据库或写死均可)
|
||||
String templateId = "";
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", code);
|
||||
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map);
|
||||
if (!smsResponse.isSuccess()) {
|
||||
log.error("验证码短信发送异常 => {}", smsResponse);
|
||||
Object data = smsResponse.getData();
|
||||
return R.fail(data == null ? "验证码短信发送失败" : data.toString());
|
||||
}
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
@GetMapping("/sms")
|
||||
public R<Void> sendSmsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) {
|
||||
captchaUtils.generateSmsCaptcha(phoneNumber);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@@ -87,96 +55,60 @@ public class CaptchaController {
|
||||
* @param email 邮箱
|
||||
* @return 操作结果
|
||||
*/
|
||||
@GetMapping("/resource/email/code")
|
||||
public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
|
||||
@GetMapping("/email")
|
||||
public R<Void> sendEmailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
|
||||
if (!mailProperties.getEnabled()) {
|
||||
return R.fail("当前系统没有开启邮箱功能!");
|
||||
}
|
||||
if (!RegexValidator.isEmail(email)) {
|
||||
return R.fail("请输入正确的邮箱地址!");
|
||||
}
|
||||
SpringUtils.getAopProxy(this).emailCodeImpl(email);
|
||||
SpringUtils.getAopProxy(this).captchaUtils.generateEmailCaptcha(email);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码的实际执行方法,拆分出来避免开关关闭时仍触发限流。
|
||||
*
|
||||
* @param email 邮箱
|
||||
*/
|
||||
@RateLimiter(key = "#email", time = 60, count = 1)
|
||||
public void emailCodeImpl(String email) {
|
||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
|
||||
String code = RandomUtil.randomNumbers(4);
|
||||
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());
|
||||
throw new ServiceException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片验证码。
|
||||
*
|
||||
* @return 验证码信息
|
||||
*/
|
||||
@GetMapping("/auth/code")
|
||||
public R<CaptchaVo> getCode() {
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
if (!captchaEnabled) {
|
||||
return R.ok(new CaptchaVo(false, null, null));
|
||||
}
|
||||
return R.ok(SpringUtils.getAopProxy(this).getCodeImpl());
|
||||
@GetMapping("/image")
|
||||
public R<CaptchaVo> getImageCaptcha() {
|
||||
return R.ok(captchaUtils.generateImageCaptcha());
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际生成图片验证码并缓存结果。
|
||||
* 获取【行为滑块验证码】生成接口
|
||||
* <p>返回滑块背景图、缺口图、验证码key</p>
|
||||
*
|
||||
* @return 验证码信息
|
||||
* @return 行为验证码vo
|
||||
*/
|
||||
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
|
||||
public CaptchaVo getCodeImpl() {
|
||||
// 保存验证码信息
|
||||
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();
|
||||
// 如果是数学验证码,使用SpEL表达式处理验证码结果
|
||||
String code = captcha.getCode();
|
||||
if ("math".equals(captchaType)) {
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.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());
|
||||
@RequestMapping("/behavior/generate")
|
||||
public ApiResponse<ImageCaptchaVO> generateBehaviorCaptcha() {
|
||||
return captchaUtils.generateBehaviorCaptchaTicket();
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片验证码响应对象。
|
||||
* 校验【行为滑块验证码】
|
||||
* POST语义更合适,提交校验参数
|
||||
*
|
||||
* @param captchaEnabled 是否启用验证码
|
||||
* @param uuid 验证码标识
|
||||
* @param img Base64 图片数据
|
||||
* @param captchaKey 验证码缓存key
|
||||
* @param slideX 用户滑动偏移x坐标
|
||||
* @return 校验结果
|
||||
*/
|
||||
public record CaptchaVo(Boolean captchaEnabled, String uuid, String img) {
|
||||
@PostMapping("/behavior/verify")
|
||||
public ApiResponse<?> verifyBehaviorCaptcha(behaviorData data) {
|
||||
return captchaUtils.verifyBehaviorCaptchaFirstStage(data.id(), data.data());
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为验证码验证接口传回BO
|
||||
*
|
||||
* @param id 验证码唯一ID,请求登录接口时携带该标识,后端根据ID读取缓存验证码做校验
|
||||
* @param data 验证码业务数据,包含Base64图片、滑块滑动轨迹信息等,详见 {@link ImageCaptchaTrack}
|
||||
*/
|
||||
public record behaviorData(String id, ImageCaptchaTrack data) {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.log.event.LoginInfoEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.common.captcha.config.properties.CaptchaProperties;
|
||||
import org.dromara.system.api.model.RegisterBody;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.domain.bo.SysUserBo;
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.common.captcha.config.properties.CaptchaProperties;
|
||||
import org.dromara.system.api.model.LoginUser;
|
||||
import org.dromara.system.api.model.PasswordLoginBody;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
|
||||
@@ -155,7 +155,7 @@ spring.data:
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# redis 密码必须配置
|
||||
password: ruoyi123
|
||||
# password: ruoyi123
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
# 是否开启ssl
|
||||
|
||||
@@ -15,7 +15,7 @@ server:
|
||||
# 最大线程数
|
||||
max: 256
|
||||
|
||||
captcha:
|
||||
image-captcha:
|
||||
# 是否启用验证码校验
|
||||
enable: true
|
||||
# 验证码类型 math 数组计算 char 字符验证
|
||||
@@ -25,6 +25,39 @@ captcha:
|
||||
# 字符验证码长度
|
||||
charLength: 4
|
||||
|
||||
# 行为验证码配置, 详细请看 cloud.tianai.captcha.autoconfiguration.ImageCaptchaProperties 类
|
||||
captcha:
|
||||
# 如果项目中使用到了redis,滑块验证码会自动把验证码数据存到redis中, 这里配置redis的key的前缀,默认是captcha:slider
|
||||
prefix: captcha
|
||||
# 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整
|
||||
expire:
|
||||
# 默认缓存时间 2分钟
|
||||
default: 10000
|
||||
# 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些
|
||||
WORD_IMAGE_CLICK: 20000
|
||||
# 使用加载系统自带的资源, 默认是 false(这里系统的默认资源包含 滑动验证码模板/旋转验证码模板,如果想使用系统的模板,这里设置为true)
|
||||
init-default-resource: true
|
||||
# 缓存控制, 默认为false不开启
|
||||
local-cache-enabled: false
|
||||
# 缓存开启后,验证码会提前缓存一些生成好的验证数据, 默认是20
|
||||
local-cache-size: 20
|
||||
# 缓存开启后,缓存拉取失败后等待时间 默认是 5秒钟
|
||||
local-cache-wait-time: 5000
|
||||
# 缓存开启后,缓存检查间隔 默认是2秒钟
|
||||
local-cache-period: 2000
|
||||
# 缓存开启后,忽略的字段,默认是 ""(不忽略任何字段)
|
||||
local-cache-ignored-cache-fields: ""
|
||||
# 配置字体包,供文字点选验证码使用,可以配置多个,不配置使用默认的字体
|
||||
font-path:
|
||||
- classpath:font/SimHei.ttf
|
||||
secondary:
|
||||
# 二次验证, 默认false 不开启
|
||||
enabled: true
|
||||
# 二次验证过期时间, 默认 2分钟
|
||||
expire: 240000
|
||||
# 二次验证缓存key前缀,默认是 captcha:secondary
|
||||
keyPrefix: "captcha:secondary"
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
|
||||
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 142 KiB |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||