!246 集成aj-captcha

pull/10/head
YunaiV 2022-09-04 17:06:12 +08:00
parent e99138493b
commit 6b4bc47d08
80 changed files with 339 additions and 743 deletions

View File

@ -47,14 +47,14 @@
<jedis-mock.version>0.1.16</jedis-mock.version> <jedis-mock.version>0.1.16</jedis-mock.version>
<mockito-inline.version>4.0.0</mockito-inline.version> <mockito-inline.version>4.0.0</mockito-inline.version>
<!-- Bpm 工作流相关 --> <!-- Bpm 工作流相关 -->
<flowable.version>6.7.0</flowable.version> <flowable.version>6.7.2</flowable.version>
<!-- 工具类相关 --> <!-- 工具类相关 -->
<jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version> <jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version>
<lombok.version>1.18.20</lombok.version> <lombok.version>1.18.20</lombok.version>
<mapstruct.version>1.4.1.Final</mapstruct.version> <mapstruct.version>1.4.1.Final</mapstruct.version>
<hutool.version>5.7.22</hutool.version> <hutool.version>5.8.5</hutool.version>
<easyexcel.verion>2.2.7</easyexcel.verion> <easyexcel.verion>2.2.7</easyexcel.verion>
<velocity.version>2.2</velocity.version> <velocity.version>2.3</velocity.version>
<screw.version>1.0.5</screw.version> <screw.version>1.0.5</screw.version>
<fastjson.version>2.0.5</fastjson.version> <fastjson.version>2.0.5</fastjson.version>
<guava.version>30.1.1-jre</guava.version> <guava.version>30.1.1-jre</guava.version>
@ -63,11 +63,12 @@
<commons-net.version>3.8.0</commons-net.version> <commons-net.version>3.8.0</commons-net.version>
<jsch.version>0.1.55</jsch.version> <jsch.version>0.1.55</jsch.version>
<tika-core.version>2.4.1</tika-core.version> <tika-core.version>2.4.1</tika-core.version>
<aj-captcha.version>1.3.0</aj-captcha.version>
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<minio.version>8.2.2</minio.version> <minio.version>8.2.2</minio.version>
<aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version> <aliyun-java-sdk-core.version>4.6.0</aliyun-java-sdk-core.version>
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version> <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
<tencentcloud-sdk-java.version>3.1.471</tencentcloud-sdk-java.version> <tencentcloud-sdk-java.version>3.1.561</tencentcloud-sdk-java.version>
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version> <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
<justauth.version>1.4.0</justauth.version> <justauth.version>1.4.0</justauth.version>
</properties> </properties>
@ -148,6 +149,11 @@
<artifactId>yudao-spring-boot-starter-biz-error-code</artifactId> <artifactId>yudao-spring-boot-starter-biz-error-code</artifactId>
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-captcha</artifactId>
<version>${revision}</version>
</dependency>
<!-- Spring 核心 --> <!-- Spring 核心 -->
<dependency> <dependency>
@ -494,6 +500,12 @@
<version>${tika-core.version}</version> <version>${tika-core.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha</artifactId>
<version>${aj-captcha.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.velocity</groupId> <groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId> <artifactId>velocity-engine-core</artifactId>

View File

@ -40,8 +40,8 @@
<module>yudao-spring-boot-starter-biz-data-permission</module> <module>yudao-spring-boot-starter-biz-data-permission</module>
<module>yudao-spring-boot-starter-biz-error-code</module> <module>yudao-spring-boot-starter-biz-error-code</module>
<module>yudao-spring-boot-starter-activiti</module>
<module>yudao-spring-boot-starter-flowable</module> <module>yudao-spring-boot-starter-flowable</module>
<module>yudao-spring-boot-starter-captcha</module>
</modules> </modules>
<artifactId>yudao-framework</artifactId> <artifactId>yudao-framework</artifactId>

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.common.util.collection; package cn.iocoder.yudao.framework.common.util.collection;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import java.util.Collection; import java.util.Collection;
@ -44,7 +45,7 @@ public class ArrayUtils {
if (CollectionUtil.isEmpty(from)) { if (CollectionUtil.isEmpty(from)) {
return (T[]) (new Object[0]); return (T[]) (new Object[0]);
} }
return ArrayUtil.toArray(from, (Class<T>) CollectionUtil.getElementType(from.iterator())); return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator()));
} }
public static <T> T get(T[] array, int index) { public static <T> T get(T[] array, int index) {

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.framework.common.util.validation; package cn.iocoder.yudao.framework.common.util.validation;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolation;
@ -17,16 +16,15 @@ import java.util.regex.Pattern;
*/ */
public class ValidationUtils { public class ValidationUtils {
private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[189]))\\d{8}$");
private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*"); private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
public static boolean isMobile(String mobile) { public static boolean isMobile(String mobile) {
if (StrUtil.length(mobile) != 11) { return StringUtils.hasText(mobile)
return false; && PATTERN_MOBILE.matcher(mobile).matches();
}
// TODO 芋艿,后面完善手机校验
return true;
} }
public static boolean isURL(String url) { public static boolean isURL(String url) {

View File

@ -1,48 +0,0 @@
<?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>cn.iocoder.cloud</groupId>
<artifactId>yudao-framework</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-activiti</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>Activiti 拓展</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<!-- 工作流相关 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-image-generator</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,45 +0,0 @@
package cn.iocoder.yudao.framework.activiti.config;
import cn.iocoder.yudao.framework.activiti.core.web.ActivitiWebFilter;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import org.activiti.image.ProcessDiagramGenerator;
import org.activiti.image.impl.DefaultProcessDiagramGenerator;
import org.activiti.spring.SpringProcessEngineConfiguration;
import org.activiti.spring.boot.ProcessEngineConfigurationConfigurer;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.transaction.TransactionFactory;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class YudaoActivitiConfiguration {
/**
* Activiti svg
*/
@Bean
public ProcessDiagramGenerator processDiagramGenerator() {
return new DefaultProcessDiagramGenerator();
}
@Bean
public FilterRegistrationBean<ActivitiWebFilter> activitiWebFilter() {
FilterRegistrationBean<ActivitiWebFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ActivitiWebFilter());
registrationBean.setOrder(WebFilterOrderEnum.ACTIVITI_FILTER);
return registrationBean;
}
/**
* ProcessEngineConfigurationConfigurer ACT_
*/
@Bean
public ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer(
PlatformTransactionManager platformTransactionManager) {
return processEngineConfiguration -> processEngineConfiguration.setTransactionManager(platformTransactionManager);
}
}

View File

@ -1,109 +0,0 @@
package cn.iocoder.yudao.framework.activiti.core.util;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.Process;
import org.activiti.engine.impl.identity.Authentication;
import org.activiti.engine.impl.util.io.BytesStreamSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Activiti
*
* @author
*/
public class ActivitiUtils {
static {
setAuthenticationThreadLocal();
}
// ========== Authentication 相关 ==========
/**
* Authentication authenticatedUserIdThreadLocal 使 TTL 线
* @Async
*/
private static void setAuthenticationThreadLocal() {
ReflectUtil.setFieldValue(Authentication.class, "authenticatedUserIdThreadLocal",
new TransmittableThreadLocal<String>());
}
public static void setAuthenticatedUserId(Long userId) {
Authentication.setAuthenticatedUserId(String.valueOf(userId));
}
public static void clearAuthenticatedUserId() {
Authentication.setAuthenticatedUserId(null);
}
public static boolean equals(String userIdStr, Long userId) {
return Objects.equals(userId, NumberUtils.parseLong(userIdStr));
}
// ========== BPMN XML 相关 ==========
/**
* BPMN Model
*
* @param bpmnBytes BPMN XML
* @return BPMN Model
*/
public static BpmnModel buildBpmnModel(byte[] bpmnBytes) {
// 转换成 BpmnModel 对象
BpmnXMLConverter converter = new BpmnXMLConverter();
return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true);
}
/**
* BPMN
*
* @param model
* @param clazz {@link org.activiti.bpmn.model.UserTask}{@link org.activiti.bpmn.model.Gateway}
* @return
*/
public static <T extends FlowElement> List<T> getBpmnModelElements(BpmnModel model, Class<T> clazz) {
List<T> result = new ArrayList<>();
model.getProcesses().forEach(process -> {
process.getFlowElements().forEach(flowElement -> {
if (flowElement.getClass().isAssignableFrom(clazz)) {
result.add((T) flowElement);
}
});
});
return result;
}
public static String getBpmnXml(BpmnModel model) {
if (model == null) {
return null;
}
return StrUtil.utf8Str(getBpmnBytes(model));
}
public static byte[] getBpmnBytes(BpmnModel model) {
if (model == null) {
return new byte[0];
}
BpmnXMLConverter converter = new BpmnXMLConverter();
return converter.convertToXML(model);
}
public static boolean equals(BpmnModel oldModel, BpmnModel newModel) {
// 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较
return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel));
}
}

View File

@ -1,37 +0,0 @@
package cn.iocoder.yudao.framework.activiti.core.web;
import cn.iocoder.yudao.framework.activiti.core.util.ActivitiUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Activiti Web userId {@link org.activiti.engine.impl.identity.Authentication}
*
* @author
*/
public class ActivitiWebFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
// 设置工作流的用户
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (userId != null) {
ActivitiUtils.setAuthenticatedUserId(userId);
}
// 过滤
chain.doFilter(request, response);
} finally {
// 清理
ActivitiUtils.clearAuthenticatedUserId();
}
}
}

View File

@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.activiti.config.YudaoActivitiConfiguration

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.dict.core.util;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.cache.CacheUtils; import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
import cn.iocoder.yudao.module.system.api.dict.DictDataApi; import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO; import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
@ -28,7 +27,7 @@ public class DictFrameworkUtils {
/** /**
* {@link #getDictDataLabel(String, String)} * {@link #getDictDataLabel(String, String)}
*/ */
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> getDictDataCache = CacheUtils.buildAsyncReloadingCache( private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟 Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() { new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
@ -43,7 +42,7 @@ public class DictFrameworkUtils {
/** /**
* {@link #parseDictDataValue(String, String)} * {@link #parseDictDataValue(String, String)}
*/ */
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> parseDictDataCache = CacheUtils.buildAsyncReloadingCache( private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟 Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() { new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
@ -62,12 +61,12 @@ public class DictFrameworkUtils {
@SneakyThrows @SneakyThrows
public static String getDictDataLabel(String dictType, String value) { public static String getDictDataLabel(String dictType, String value) {
return getDictDataCache.get(new KeyValue<>(dictType, value)).getLabel(); return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
} }
@SneakyThrows @SneakyThrows
public static String parseDictDataValue(String dictType, String label) { public static String parseDictDataValue(String dictType, String label) {
return parseDictDataCache.get(new KeyValue<>(dictType, label)).getValue(); return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();
} }
} }

View File

@ -52,12 +52,18 @@
<dependency> <dependency>
<groupId>com.alipay.sdk</groupId> <groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId> <artifactId>alipay-sdk-java</artifactId>
<version>4.17.9.ALL</version> <version>4.31.72.ALL</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.binarywang</groupId> <groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId> <artifactId>weixin-java-pay</artifactId>
<version>4.1.9.B</version> <version>4.3.8.B</version>
</dependency> </dependency>
<!-- TODO 芋艿:清理 --> <!-- TODO 芋艿:清理 -->

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.framework.pay.core.client.impl; package cn.iocoder.yudao.framework.pay.core.client.impl;
import cn.hutool.extra.validation.ValidationUtil;
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
import cn.iocoder.yudao.framework.pay.core.client.PayClient; import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
@ -10,6 +9,8 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.validation.Validation;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/** /**
@ -79,7 +80,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
@Override @Override
public final PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { public final PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
ValidationUtil.validate(reqDTO); Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO);
// 执行短信发送 // 执行短信发送
PayCommonResult<?> result; PayCommonResult<?> result;
try { try {

View File

@ -53,7 +53,6 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
"lrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZ" + "lrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZ" +
"ikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); "ikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
// TODO @tina= 前后要有空格哈
@InjectMocks @InjectMocks
AlipayQrPayClient client = new AlipayQrPayClient(10L, config); AlipayQrPayClient client = new AlipayQrPayClient(10L, config);

View File

@ -35,7 +35,7 @@
<groupId>com.github.binarywang</groupId> <groupId>com.github.binarywang</groupId>
<!-- <artifactId>weixin-java-mp</artifactId>--> <!-- <artifactId>weixin-java-mp</artifactId>-->
<artifactId>wx-java-mp-spring-boot-starter</artifactId> <artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.1.9.B</version> <version>4.3.8.B</version>
</dependency> </dependency>
<!-- TODO 芋艿:清理 --> <!-- TODO 芋艿:清理 -->
</dependencies> </dependencies>

View File

@ -0,0 +1,39 @@
<?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>cn.iocoder.cloud</groupId>
<artifactId>yudao-framework</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-captcha</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>验证码拓展
1. 基于 aj-captcha 实现滑块验证码文档https://ajcaptcha.beliefteam.cn/captcha-doc/
</description>
<dependencies>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
<!-- 验证码相关 -->
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.framework.captcha.config;
import cn.hutool.core.util.ClassUtil;
import cn.iocoder.yudao.framework.captcha.core.enums.CaptchaRedisKeyConstants;
import cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl;
import com.anji.captcha.service.CaptchaCacheService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class YudaoCaptchaConfiguration {
static {
// 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到
// 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举
ClassUtil.loadClass(CaptchaRedisKeyConstants.class.getName());
}
@Bean
public CaptchaCacheService captchaCacheService(StringRedisTemplate stringRedisTemplate) {
return new RedisCaptchaServiceImpl(stringRedisTemplate);
}
}

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.framework.captcha.core.enums;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import com.anji.captcha.model.vo.PointVO;
import java.time.Duration;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
/**
* Redis Key
*
* @author
*/
public interface CaptchaRedisKeyConstants {
RedisKeyDefine AJ_CAPTCHA_REQ_LIMIT = new RedisKeyDefine("验证码的请求限流",
"AJ.CAPTCHA.REQ.LIMIT-%s-%s",
STRING, Integer.class, Duration.ofSeconds(60)); // 例如说:验证失败 5 次get 接口锁定
RedisKeyDefine AJ_CAPTCHA_RUNNING = new RedisKeyDefine("验证码的坐标",
"RUNNING:CAPTCHA:%s", // AbstractCaptchaService.REDIS_CAPTCHA_KEY
STRING, PointVO.class, Duration.ofSeconds(120)); // {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5}
}

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.framework.captcha.core.service;
import com.anji.captcha.service.CaptchaCacheService;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* Redis
*
* @author
*/
@NoArgsConstructor // 保证 aj-captcha 的 SPI 创建
@AllArgsConstructor
public class RedisCaptchaServiceImpl implements CaptchaCacheService {
@Resource // 保证 aj-captcha 的 SPI 创建时的注入
private StringRedisTemplate stringRedisTemplate;
@Override
public String type() {
return "redis";
}
@Override
public void set(String key, String value, long expiresInSeconds) {
stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
}
@Override
public boolean exists(String key) {
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
}
@Override
public void delete(String key) {
stringRedisTemplate.delete(key);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public Long increment(String key, long val) {
return stringRedisTemplate.opsForValue().increment(key,val);
}
}

View File

@ -0,0 +1,7 @@
/**
*
* 1. aj-captcha https://ajcaptcha.beliefteam.cn/captcha-doc/
*
* @author
*/
package cn.iocoder.yudao.framework.captcha;

View File

@ -0,0 +1 @@
cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -16,11 +16,11 @@ import java.util.Set;
*/ */
public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler<Object> { public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler<Object> {
private static final TypeReference<Set<Long>> typeReference = new TypeReference<Set<Long>>(){}; private static final TypeReference<Set<Long>> TYPE_REFERENCE = new TypeReference<Set<Long>>(){};
@Override @Override
protected Object parse(String json) { protected Object parse(String json) {
return JsonUtils.parseObject(json, typeReference); return JsonUtils.parseObject(json, TYPE_REFERENCE);
} }
@Override @Override

View File

@ -11,18 +11,18 @@ public class RedisKeyRegistry {
/** /**
* Redis RedisKeyDefine * Redis RedisKeyDefine
*/ */
private static final List<RedisKeyDefine> defines = new ArrayList<>(); private static final List<RedisKeyDefine> DEFINES = new ArrayList<>();
public static void add(RedisKeyDefine define) { public static void add(RedisKeyDefine define) {
defines.add(define); DEFINES.add(define);
} }
public static List<RedisKeyDefine> list() { public static List<RedisKeyDefine> list() {
return defines; return DEFINES;
} }
public static int size() { public static int size() {
return defines.size(); return DEFINES.size();
} }
} }

View File

@ -17,19 +17,19 @@ public class TransmittableThreadLocalSecurityContextHolderStrategy implements Se
/** /**
* 使 TransmittableThreadLocal * 使 TransmittableThreadLocal
*/ */
private static final ThreadLocal<SecurityContext> contextHolder = new TransmittableThreadLocal<>(); private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>();
@Override @Override
public void clearContext() { public void clearContext() {
contextHolder.remove(); CONTEXT_HOLDER.remove();
} }
@Override @Override
public SecurityContext getContext() { public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get(); SecurityContext ctx = CONTEXT_HOLDER.get();
if (ctx == null) { if (ctx == null) {
ctx = createEmptyContext(); ctx = createEmptyContext();
contextHolder.set(ctx); CONTEXT_HOLDER.set(ctx);
} }
return ctx; return ctx;
} }
@ -37,7 +37,7 @@ public class TransmittableThreadLocalSecurityContextHolderStrategy implements Se
@Override @Override
public void setContext(SecurityContext context) { public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context); CONTEXT_HOLDER.set(context);
} }
@Override @Override

View File

@ -57,7 +57,7 @@ public class BpmMessageServiceImpl implements BpmMessageService {
templateParams.put("taskName", reqDTO.getTaskName()); templateParams.put("taskName", reqDTO.getTaskName());
templateParams.put("startUserNickname", reqDTO.getStartUserNickname()); templateParams.put("startUserNickname", reqDTO.getStartUserNickname());
templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()));
smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(), smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(),
BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams)); BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams));
} }

View File

@ -31,7 +31,7 @@ public class CodegenBuilder {
* {@link CodegenColumnListConditionEnum} * {@link CodegenColumnListConditionEnum}
* *
*/ */
private static final Map<String, CodegenColumnListConditionEnum> columnListOperationConditionMappings = private static final Map<String, CodegenColumnListConditionEnum> COLUMN_LIST_OPERATION_CONDITION_MAPPINGS =
MapUtil.<String, CodegenColumnListConditionEnum>builder() MapUtil.<String, CodegenColumnListConditionEnum>builder()
.put("name", CodegenColumnListConditionEnum.LIKE) .put("name", CodegenColumnListConditionEnum.LIKE)
.put("time", CodegenColumnListConditionEnum.BETWEEN) .put("time", CodegenColumnListConditionEnum.BETWEEN)
@ -42,7 +42,7 @@ public class CodegenBuilder {
* {@link CodegenColumnHtmlTypeEnum} * {@link CodegenColumnHtmlTypeEnum}
* *
*/ */
private static final Map<String, CodegenColumnHtmlTypeEnum> columnHtmlTypeMappings = private static final Map<String, CodegenColumnHtmlTypeEnum> COLUMN_HTML_TYPE_MAPPINGS =
MapUtil.<String, CodegenColumnHtmlTypeEnum>builder() MapUtil.<String, CodegenColumnHtmlTypeEnum>builder()
.put("status", CodegenColumnHtmlTypeEnum.RADIO) .put("status", CodegenColumnHtmlTypeEnum.RADIO)
.put("sex", CodegenColumnHtmlTypeEnum.RADIO) .put("sex", CodegenColumnHtmlTypeEnum.RADIO)
@ -143,7 +143,7 @@ public class CodegenBuilder {
column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField())
&& !column.getPrimaryKey()); // 对于主键,列表过滤不需要传递 && !column.getPrimaryKey()); // 对于主键,列表过滤不需要传递
// 处理 listOperationCondition 字段 // 处理 listOperationCondition 字段
columnListOperationConditionMappings.entrySet().stream() COLUMN_LIST_OPERATION_CONDITION_MAPPINGS.entrySet().stream()
.filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey()))
.findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition())); .findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition()));
if (column.getListOperationCondition() == null) { if (column.getListOperationCondition() == null) {
@ -155,7 +155,7 @@ public class CodegenBuilder {
private void processColumnUI(CodegenColumnDO column) { private void processColumnUI(CodegenColumnDO column) {
// 基于后缀进行匹配 // 基于后缀进行匹配
columnHtmlTypeMappings.entrySet().stream() COLUMN_HTML_TYPE_MAPPINGS.entrySet().stream()
.filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey()))
.findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType())); .findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType()));
// 如果是 Boolean 类型时,设置为 radio 类型. // 如果是 Boolean 类型时,设置为 radio 类型.

View File

@ -12,8 +12,7 @@ public interface ErrorCodeConstants {
// ========== AUTH 模块 1002000000 ========== // ========== AUTH 模块 1002000000 ==========
ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确"); ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确");
ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用"); ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用");
ErrorCode AUTH_LOGIN_CAPTCHA_NOT_FOUND = new ErrorCode(1002000003, "验证码不存在"); ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确,原因:{}");
ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确");
ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定"); ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定");
ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1002000006, "Token 已经过期"); ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1002000006, "Token 已经过期");
ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1002000007, "手机号不存在"); ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1002000007, "手机号不存在");

View File

@ -141,6 +141,11 @@
<artifactId>yudao-spring-boot-starter-excel</artifactId> <artifactId>yudao-spring-boot-starter-excel</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-captcha</artifactId>
</dependency>
<!-- 监控相关 --> <!-- 监控相关 -->
<dependency> <dependency>
<groupId>cn.iocoder.cloud</groupId> <groupId>cn.iocoder.cloud</groupId>

View File

@ -35,13 +35,11 @@ public class AuthLoginReqVO {
// ========== 图片验证码相关 ========== // ========== 图片验证码相关 ==========
@ApiModelProperty(value = "验证码", required = true, example = "1024", notes = "验证码开启时,需要传递") @ApiModelProperty(value = "验证码", required = true,
example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==",
notes = "验证码开启时,需要传递")
@NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class)
private String code; private String captchaVerification;
@ApiModelProperty(value = "验证码的唯一标识", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62", notes = "验证码开启时,需要传递")
@NotEmpty(message = "唯一标识不能为空", groups = CodeEnableGroup.class)
private String uuid;
// ========== 绑定社交登录时,需要传递如下参数 ========== // ========== 绑定社交登录时,需要传递如下参数 ==========

View File

@ -1,3 +0,0 @@
### 请求 /captcha/get-image 接口 => 成功
GET {{baseUrl}}/system/captcha/get-image
tenant-id: {{adminTenentId}}

View File

@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.system.controller.admin.common;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO;
import cn.iocoder.yudao.module.system.service.common.CaptchaService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Api(tags = "管理后台 - 验证码")
@RestController
@RequestMapping("/system/captcha")
public class CaptchaController {
@Resource
private CaptchaService captchaService;
@GetMapping("/get-image")
@PermitAll
@ApiOperation("生成图片验证码")
public CommonResult<CaptchaImageRespVO> getCaptchaImage() {
return success(captchaService.getCaptchaImage());
}
}

View File

@ -1,27 +0,0 @@
package cn.iocoder.yudao.module.system.controller.admin.common.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@ApiModel("管理后台 - 验证码图片 Response VO")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CaptchaImageRespVO {
@ApiModelProperty(value = "是否开启", required = true, example = "true", notes = "如果为 false则关闭验证码功能")
private Boolean enable;
@ApiModelProperty(value = "uuid", example = "1b3b7d00-83a8-4638-9e37-d67011855968",
notes = "enable = true 时,非空!通过该 uuid 作为该验证码的标识")
private String uuid;
@ApiModelProperty(value = "图片", notes = "enable = true 时,非空!验证码的图片内容,使用 Base64 编码")
private String img;
}

View File

@ -1,17 +0,0 @@
package cn.iocoder.yudao.module.system.convert.common;
import cn.hutool.captcha.AbstractCaptcha;
import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface CaptchaConvert {
CaptchaConvert INSTANCE = Mappers.getMapper(CaptchaConvert.class);
default CaptchaImageRespVO convert(String uuid, AbstractCaptcha captcha) {
return CaptchaImageRespVO.builder().uuid(uuid).img(captcha.getImageBase64()).build();
}
}

View File

@ -14,10 +14,6 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.S
*/ */
public interface RedisKeyConstants { public interface RedisKeyConstants {
RedisKeyDefine CAPTCHA_CODE = new RedisKeyDefine("验证码的缓存",
"captcha_code:%s", // 参数为 uuid
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
RedisKeyDefine OAUTH2_ACCESS_TOKEN = new RedisKeyDefine("访问令牌的缓存", RedisKeyDefine OAUTH2_ACCESS_TOKEN = new RedisKeyDefine("访问令牌的缓存",
"oauth2_access_token:%s", // 参数为访问令牌 token "oauth2_access_token:%s", // 参数为访问令牌 token
STRING, OAuth2AccessTokenDO.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); STRING, OAuth2AccessTokenDO.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);

View File

@ -1,41 +0,0 @@
package cn.iocoder.yudao.module.system.dal.redis.common;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.time.Duration;
import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.CAPTCHA_CODE;
/**
* Redis DAO
*
* @author
*/
@Repository
public class CaptchaRedisDAO {
@Resource
private StringRedisTemplate stringRedisTemplate;
public String get(String uuid) {
String redisKey = formatKey(uuid);
return stringRedisTemplate.opsForValue().get(redisKey);
}
public void set(String uuid, String code, Duration timeout) {
String redisKey = formatKey(uuid);
stringRedisTemplate.opsForValue().set(redisKey, code, timeout);
}
public void delete(String uuid) {
String redisKey = formatKey(uuid);
stringRedisTemplate.delete(redisKey);
}
private static String formatKey(String uuid) {
return String.format(CAPTCHA_CODE.getKeyTemplate(), uuid);
}
}

View File

@ -1,9 +0,0 @@
package cn.iocoder.yudao.module.system.framework.captcha.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(CaptchaProperties.class)
public class CaptchaConfig {
}

View File

@ -1,38 +0,0 @@
package cn.iocoder.yudao.module.system.framework.captcha.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
import java.time.Duration;
@ConfigurationProperties(prefix = "yudao.captcha")
@Validated
@Data
public class CaptchaProperties {
private static final Boolean ENABLE_DEFAULT = true;
/**
*
* Server
*/
private Boolean enable = ENABLE_DEFAULT;
/**
*
*/
@NotNull(message = "验证码的过期时间不为空")
private Duration timeout;
/**
*
*/
@NotNull(message = "验证码的高度不能为空")
private Integer height;
/**
*
*/
@NotNull(message = "验证码的宽度不能为空")
private Integer width;
}

View File

@ -1,4 +0,0 @@
/**
* Hutool captcha
*/
package cn.iocoder.yudao.module.system.framework.captcha;

View File

@ -17,14 +17,17 @@ import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants; import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import cn.iocoder.yudao.module.system.service.common.CaptchaService;
import cn.iocoder.yudao.module.system.service.logger.LoginLogService; import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
import cn.iocoder.yudao.module.system.service.member.MemberService; import cn.iocoder.yudao.module.system.service.member.MemberService;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
import cn.iocoder.yudao.module.system.service.social.SocialUserService; import cn.iocoder.yudao.module.system.service.social.SocialUserService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -47,8 +50,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Resource @Resource
private AdminUserService userService; private AdminUserService userService;
@Resource @Resource
private CaptchaService captchaService;
@Resource
private LoginLogService loginLogService; private LoginLogService loginLogService;
@Resource @Resource
private OAuth2TokenService oauth2TokenService; private OAuth2TokenService oauth2TokenService;
@ -60,9 +61,17 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Resource @Resource
private Validator validator; private Validator validator;
@Resource
private CaptchaService captchaService;
@Resource @Resource
private SmsCodeApi smsCodeApi; private SmsCodeApi smsCodeApi;
/**
* true
*/
@Value("${yudao.captcha.enable:true}")
private Boolean captchaEnable;
@Override @Override
public AdminUserDO authenticate(String username, String password) { public AdminUserDO authenticate(String username, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
@ -130,27 +139,20 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@VisibleForTesting @VisibleForTesting
void verifyCaptcha(AuthLoginReqVO reqVO) { void verifyCaptcha(AuthLoginReqVO reqVO) {
// 如果验证码关闭,则不进行校验 // 如果验证码关闭,则不进行校验
if (!captchaService.isCaptchaEnable()) { if (!captchaEnable) {
return; return;
} }
// 校验验证码 // 校验验证码
ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class); ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class);
// 验证码不存在 CaptchaVO captchaVO = new CaptchaVO();
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
String code = captchaService.getCaptchaCode(reqVO.getUuid()); ResponseModel response = captchaService.verification(captchaVO);
if (code == null) { // 验证不通过
// 创建登录失败日志(验证码不存在) if (!response.isSuccess()) {
createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_NOT_FOUND);
throw exception(AUTH_LOGIN_CAPTCHA_NOT_FOUND);
}
// 验证码不正确
if (!code.equals(reqVO.getCode())) {
// 创建登录失败日志(验证码不正确) // 创建登录失败日志(验证码不正确)
createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_CODE_ERROR); createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR);
throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR); throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg());
} }
// 正确,所以要删除下验证码
captchaService.deleteCaptchaCode(reqVO.getUuid());
} }
private void createLoginLog(Long userId, String username, private void createLoginLog(Long userId, String username,

View File

@ -1,39 +0,0 @@
package cn.iocoder.yudao.module.system.service.common;
import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO;
/**
* Service
*/
public interface CaptchaService {
/**
*
*
* @return
*/
CaptchaImageRespVO getCaptchaImage();
/**
*
*
* @return
*/
Boolean isCaptchaEnable();
/**
* uuid
*
* @param uuid
* @return
*/
String getCaptchaCode(String uuid);
/**
* uuid
*
* @param uuid
*/
void deleteCaptchaCode(String uuid);
}

View File

@ -1,65 +0,0 @@
package cn.iocoder.yudao.module.system.service.common;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.system.convert.common.CaptchaConvert;
import cn.iocoder.yudao.module.system.framework.captcha.config.CaptchaProperties;
import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO;
import cn.iocoder.yudao.module.system.dal.redis.common.CaptchaRedisDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* Service
*/
@Service
public class CaptchaServiceImpl implements CaptchaService {
@Resource
private CaptchaProperties captchaProperties;
/**
*
*
* {@link CaptchaProperties#getEnable()} Apollo Spring Boot @ConfigurationProperties
* ~
*/
@Value("${yudao.captcha.enable}")
private Boolean enable;
@Resource
private CaptchaRedisDAO captchaRedisDAO;
@Override
public CaptchaImageRespVO getCaptchaImage() {
if (!Boolean.TRUE.equals(enable)) {
return CaptchaImageRespVO.builder().enable(enable).build();
}
// 生成验证码
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(captchaProperties.getWidth(), captchaProperties.getHeight());
// 缓存到 Redis 中
String uuid = IdUtil.fastSimpleUUID();
captchaRedisDAO.set(uuid, captcha.getCode(), captchaProperties.getTimeout());
// 返回
return CaptchaConvert.INSTANCE.convert(uuid, captcha).setEnable(enable);
}
@Override
public Boolean isCaptchaEnable() {
return enable;
}
@Override
public String getCaptchaCode(String uuid) {
return captchaRedisDAO.get(uuid);
}
@Override
public void deleteCaptchaCode(String uuid) {
captchaRedisDAO.delete(uuid);
}
}

View File

@ -91,6 +91,25 @@ xxl:
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径 logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
accessToken: default_token # 执行器通讯TOKEN accessToken: default_token # 执行器通讯TOKEN
--- #################### 验证码相关配置 ####################
aj:
captcha:
jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
cache-type: redis # 缓存 local/redis...
cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存
timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行
type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选
water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 UnicodeLinux 可能需要转 unicode
interference-options: 2 # 滑动干扰项(0/1/2)
req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false
req-get-lock-limit: 5 # 验证失败5次get接口锁定
req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔
req-get-minute-limit: 30 # get 接口一分钟内请求数限制
req-check-minute-limit: 60 # check 接口一分钟内请求数限制
req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制
--- #################### 芋道相关配置 #################### --- #################### 芋道相关配置 ####################
yudao: yudao:
@ -106,9 +125,7 @@ yudao:
version: ${yudao.info.version} version: ${yudao.info.version}
base-package: ${yudao.info.base-package} base-package: ${yudao.info.base-package}
captcha: captcha:
timeout: 5m enable: true # 验证码的开关,默认为 true注意优先读取数据库 infra_config 的 yudao.captcha.enable所以请从数据库修改可能需要重启项目
width: 160
height: 60
error-code: # 错误码相关配置项 error-code: # 错误码相关配置项
constants-class-list: constants-class-list:
- cn.iocoder.yudao.module.system.enums.ErrorCodeConstants - cn.iocoder.yudao.module.system.enums.ErrorCodeConstants

View File

@ -5,19 +5,16 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.framework.test.core.util.AssertUtils; import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginReqVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.service.common.CaptchaService;
import cn.iocoder.yudao.module.system.service.logger.LoginLogService; import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
import cn.iocoder.yudao.module.system.service.member.MemberService; import cn.iocoder.yudao.module.system.service.member.MemberService;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
import cn.iocoder.yudao.module.system.service.social.SocialUserService; import cn.iocoder.yudao.module.system.service.social.SocialUserService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import org.junit.jupiter.api.BeforeEach; import com.anji.captcha.service.CaptchaService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -26,10 +23,10 @@ import javax.annotation.Resource;
import javax.validation.Validator; import javax.validation.Validator;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.AUTH_LOGIN_BAD_CREDENTIALS;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.AUTH_LOGIN_USER_DISABLED;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -57,11 +54,6 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
@MockBean @MockBean
private Validator validator; private Validator validator;
@BeforeEach
public void setUp() {
when(captchaService.isCaptchaEnable()).thenReturn(true);
}
@Test @Test
public void testAuthenticate_success() { public void testAuthenticate_success() {
// 准备参数 // 准备参数
@ -138,82 +130,82 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
); );
} }
@Test // @Test
public void testCaptcha_success() { // public void testCaptcha_success() {
// 准备参数 // // 准备参数
AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); // AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
//
// // mock 验证码正确
// when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
//
// // 调用
// authService.verifyCaptcha(reqVO);
// // 断言
// verify(captchaService).deleteCaptchaCode(reqVO.getUuid());
// }
//
// @Test
// public void testCaptcha_notFound() {
// // 准备参数
// AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
//
// // 调用, 并断言异常
// assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND);
// // 校验调用参数
// verify(loginLogService, times(1)).createLoginLog(
// argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
// && o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult()))
// );
// }
// mock 验证码正确 // @Test
when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); // public void testCaptcha_codeError() {
// // 准备参数
// AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
//
// // mock 验证码不正确
// String code = randomString();
// when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code);
//
// // 调用, 并断言异常
// assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR);
// // 校验调用参数
// verify(loginLogService).createLoginLog(
// argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
// && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult()))
// );
// }
// 调用 // @Test
authService.verifyCaptcha(reqVO); // public void testLogin_success() {
// 断言 // // 准备参数
verify(captchaService).deleteCaptchaCode(reqVO.getUuid()); // AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o ->
} // o.setUsername("test_username").setPassword("test_password"));
//
@Test // // mock 验证码正确
public void testCaptcha_notFound() { // when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
// 准备参数 // // mock user 数据
AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); // AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username")
// .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus()));
// 调用, 并断言异常 // when(userService.getUserByUsername(eq("test_username"))).thenReturn(user);
assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND); // // mock password 匹配
// 校验调用参数 // when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true);
verify(loginLogService, times(1)).createLoginLog( // // mock 缓存登录用户到 Redis
argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) // OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
&& o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult())) // .setUserType(UserTypeEnum.ADMIN.getValue()));
); // when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull()))
} // .thenReturn(accessTokenDO);
//
@Test // // 调用, 并断言异常
public void testCaptcha_codeError() { // AuthLoginRespVO loginRespVO = authService.login(reqVO);
// 准备参数 // assertPojoEquals(accessTokenDO, loginRespVO);
AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); // // 校验调用参数
// verify(loginLogService).createLoginLog(
// mock 验证码不正确 // argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
String code = randomString(); // && o.getResult().equals(LoginResultEnum.SUCCESS.getResult())
when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code); // && o.getUserId().equals(user.getId()))
// );
// 调用, 并断言异常 // }
assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR);
// 校验调用参数
verify(loginLogService).createLoginLog(
argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
&& o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult()))
);
}
@Test
public void testLogin_success() {
// 准备参数
AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o ->
o.setUsername("test_username").setPassword("test_password"));
// mock 验证码正确
when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
// mock user 数据
AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username")
.setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(userService.getUserByUsername(eq("test_username"))).thenReturn(user);
// mock password 匹配
when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true);
// mock 缓存登录用户到 Redis
OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull()))
.thenReturn(accessTokenDO);
// 调用, 并断言异常
AuthLoginRespVO loginRespVO = authService.login(reqVO);
assertPojoEquals(accessTokenDO, loginRespVO);
// 校验调用参数
verify(loginLogService).createLoginLog(
argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
&& o.getResult().equals(LoginResultEnum.SUCCESS.getResult())
&& o.getUserId().equals(user.getId()))
);
}
@Test @Test
public void testLogout_success() { public void testLogout_success() {

View File

@ -1,65 +0,0 @@
package cn.iocoder.yudao.module.system.service.common;
import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO;
import cn.iocoder.yudao.module.system.dal.redis.common.CaptchaRedisDAO;
import cn.iocoder.yudao.module.system.framework.captcha.config.CaptchaProperties;
import cn.iocoder.yudao.framework.test.core.ut.BaseRedisUnitTest;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
@Import({CaptchaServiceImpl.class, CaptchaProperties.class, CaptchaRedisDAO.class})
public class CaptchaServiceTest extends BaseRedisUnitTest {
@Resource
private CaptchaServiceImpl captchaService;
@Resource
private CaptchaRedisDAO captchaRedisDAO;
@Resource
private CaptchaProperties captchaProperties;
@Test
public void testGetCaptchaImage() {
// 调用
CaptchaImageRespVO respVO = captchaService.getCaptchaImage();
// 断言
assertNotNull(respVO.getUuid());
assertNotNull(respVO.getImg());
String captchaCode = captchaRedisDAO.get(respVO.getUuid());
assertNotNull(captchaCode);
}
@Test
public void testGetCaptchaCode() {
// 准备参数
String uuid = randomString();
String code = randomString();
// mock 数据
captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout());
// 调用
String resultCode = captchaService.getCaptchaCode(uuid);
// 断言
assertEquals(code, resultCode);
}
@Test
public void testDeleteCaptchaCode() {
// 准备参数
String uuid = randomString();
String code = randomString();
// mock 数据
captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout());
// 调用
captchaService.deleteCaptchaCode(uuid);
// 断言
assertNull(captchaRedisDAO.get(uuid));
}
}