Compare commits

...

24 Commits

Author SHA1 Message Date
96a80985d4 release: v2.13.4 2025-07-26 23:23:13 +08:00
cff4f02d96 refactor(cache/redisson): 移除 RedisUtils 中的 Lock 相关工具方法(统一使用 RedisLockUtils) 2025-07-26 23:12:46 +08:00
书中自有颜如玉
48783db422 feat(cache/redisson): 新增 RedisLockUtils Redisson 分布式锁工具类 2025-07-26 15:00:48 +00:00
49c804ac9e fix(security/crypto): 修复配置错误 2025-07-26 22:41:55 +08:00
1fdb0291d2 fix(extension/crud): 修复配置错误 2025-07-26 22:35:42 +08:00
0ba365dabc feat(security/crypto): 新增密码编码器配置(由原 security/password 模块融合) 2025-07-25 21:43:13 +08:00
ecabda6aec feat(extension/crud): CRUD API 新增 DICT(字典列表(下拉选项等场景))、DICT_TREE(字典树列表(树型结构下拉选项等场景)) 2025-07-25 21:31:43 +08:00
ca33851fbd refactor(extension/crud): 优化 CRUD API 自动配置代码,EnableCrudRestController => EnableCrudApi 2025-07-25 21:15:57 +08:00
e5354b765d refactor(cache/redisson): 移除 RedisQueueUtils 类 2025-07-25 21:10:43 +08:00
KAI
8676f9b591 feat(cache/redisson): RedisUtils 新增 Hash 常用操作方法(hSet/hGet/hGetAll/hExists/hDel) 2025-07-23 13:49:38 +00:00
书中自有颜如玉
d0eddcb9f7 fix(security/crypto): 修复 构造默认加密上下文时缺失默认加密器 导致找不到加密器的问题。 2025-07-23 09:44:14 +00:00
0c606e681a release: v2.13.3 2025-07-22 23:14:38 +08:00
a2135374b2 fix(extension/crud): 修复树接口传参错误 2025-07-22 22:53:19 +08:00
38b6428662 feat(security/crypto): 新增支持密码编码器加密 2025-07-22 22:46:42 +08:00
58f9687c58 feat(security/password): 重构密码编码器,新增 PasswordEncoderUtil 2025-07-22 22:46:33 +08:00
9d39012f0b build(dependencies): spel-validator 0.5.1-beta => 0.5.2-beta 2025-07-22 22:41:12 +08:00
e64553e620 refactor(web): 拆分 default-web.yml 为 default-response.yml 和 default-server.yml 配置文件 2025-07-22 22:40:59 +08:00
a392fab782 feat(core): 新增 OrderedConstants 统一登记过滤器和拦截器相关顺序常量,并调整相关过滤器和拦截器顺序 2025-07-22 20:53:59 +08:00
3e822c0b84 feat(data): Query 注解新增多列查询逻辑关系支持(原来仅支持或者,现在也支持并且) 2025-07-22 20:44:46 +08:00
0a9027d91f refactor(extension/crud): 优化部分代码 2025-07-22 20:31:17 +08:00
1eb1c2d845 feat(core): ReflectUtils 新增 createMethodReference 方法(由 CRUD 模块迁移) 2025-07-22 20:24:42 +08:00
c76d777a2e refactor(core): TreeBuildUtils => TreeUtils 2025-07-22 20:21:44 +08:00
书中自有颜如玉
55660ba18b refactor(extension/crud): 重构查询树列表功能,增加重载方法,支持构建单个根节点或者多个根节点的树结构 2025-07-22 11:30:35 +00:00
书中自有颜如玉
36c30a20dd fix(security/crypto): 修复新版 API 未支持自定义加密器问题 2025-07-22 07:35:38 +00:00
59 changed files with 1242 additions and 601 deletions

View File

@@ -1,3 +1,47 @@
## [v2.13.4](https://github.com/continew-org/continew-starter/compare/v2.13.3...v2.13.4) (2025-07-26)
### ✨ 新特性
- 【cache/redisson】RedisUtils 新增 Hash 常用操作方法hSet/hGet/hGetAll/hExists/hDel(Gitee#77@kiki1373639299) ([8676f9b](https://github.com/continew-org/continew-starter/commit/8676f9b5912914f2bc1025d7695e2b11136bad20))
- 【extension/crud】CRUD API 新增 DICT字典列表下拉选项等场景、DICT_TREE字典树列表树型结构下拉选项等场景 ([ecabda6](https://github.com/continew-org/continew-starter/commit/ecabda6aecc70fdb49eebd2dcde5bddd63fe337b))
- 【security/crypto】新增密码编码器配置由原 security/password 模块融合) ([0ba365d](https://github.com/continew-org/continew-starter/commit/0ba365dabc3c18a7b07eaec05c17a8848642e78f)) ([49c804a](https://github.com/continew-org/continew-starter/commit/49c804ac9e577ccc4019319c812ebcdf20ef5ad6))
- 【cache/redisson】新增 RedisLockUtils Redisson 分布式锁工具类 (Gitee#78@lishuyanla) ([48783db](https://github.com/continew-org/continew-starter/commit/48783db422525548d7eec5caba788f8ab53d7bec))
### 💎 功能优化
- 【cache/redisson】移除 RedisQueueUtils 类 ([e5354b7](https://github.com/continew-org/continew-starter/commit/e5354b765d27f4ba853865fa6290f706f14a990c))
- 【extension/crud】优化 CRUD API 自动配置代码EnableCrudRestController => EnableCrudApi ([ca33851](https://github.com/continew-org/continew-starter/commit/ca33851fbd92f145229844c464a0cf1edbf7b9c7)) ([1fdb029](https://github.com/continew-org/continew-starter/commit/1fdb0291d20975e667232f866e3605712b29f8a9))
- 【cache/redisson】移除 RedisUtils 中的 Lock 相关工具方法(统一使用 RedisLockUtils ([cff4f02](https://github.com/continew-org/continew-starter/commit/cff4f02d966836fd291c268d0e44b0047e35d053))
### 🐛 问题修复
- 【security/crypto】修复 构造默认加密上下文时缺失默认加密器 导致找不到加密器的问题 (Gitee#76@lishuyanla) ([d0eddcb](https://github.com/continew-org/continew-starter/commit/d0eddcb9f73282578df6ebb6b8c3d41fe164abd0))
## [v2.13.3](https://github.com/continew-org/continew-starter/compare/v2.13.2...v2.13.3) (2025-07-22)
### ✨ 新特性
- 【core】ReflectUtils 新增 createMethodReference 方法(由 CRUD 模块迁移) ([1eb1c2d](https://github.com/continew-org/continew-starter/commit/1eb1c2d845ded85197a222e492b0afe5bd8da48d))
- 【data】Query 注解新增多列查询逻辑关系支持(原来仅支持或者,现在也支持并且) ([3e822c0](https://github.com/continew-org/continew-starter/commit/3e822c0b8442a5a00840a9ae67d7fa03cd5d33b0))
- 【core】新增 OrderedConstants 统一登记过滤器和拦截器相关顺序常量,并调整相关过滤器和拦截器顺序 ([a392fab](https://github.com/continew-org/continew-starter/commit/a392fab78222db8f05933e398d8b0541aed07651))
- 【security/password】重构密码编码器新增 PasswordEncoderUtil ([58f9687](https://github.com/continew-org/continew-starter/commit/58f9687c581c121d4688e2ab99678d94d262c60a))
- 【security/crypto】新增支持密码编码器加密 ([38b6428](https://github.com/continew-org/continew-starter/commit/38b6428662b909875df4ae8f36f180b0394accc1))
### 💎 功能优化
- 【extension/crud】重构查询树列表功能增加重载方法支持构建单个根节点或者多个根节点的树结构 (Gitee#75@lishuyanla) ([55660ba](https://github.com/continew-org/continew-starter/commit/55660ba18bb3b8b8cecc1c979aa71cde5b4b39d9)) ([a213537](https://github.com/continew-org/continew-starter/commit/a2135374b231ee410bafc8573e706443c6097353))
- 【core】TreeBuildUtils => TreeUtils ([c76d777](https://github.com/continew-org/continew-starter/commit/c76d777a2e3b20a0542ef606cb3a4c85068a25fe))
- 【extension/crud】优化部分代码 ([0a9027d](https://github.com/continew-org/continew-starter/commit/0a9027d91f3a2618f91e7b5417cbed5288e1e46b))
- 【web】拆分 default-web.yml 为 default-response.yml 和 default-server.yml 配置文件 ([e64553e](https://github.com/continew-org/continew-starter/commit/e64553e6205ca3473a656f60448304bf4c18ddca))
### 🐛 问题修复
- 【security/crypto】修复新版 API 未支持自定义加密器问题 (Gitee#74@lishuyanla) ([36c30a2](https://github.com/continew-org/continew-starter/commit/36c30a20ddff30832a31e7d6751d0140c45de3a7))
### 📦 依赖升级
- 【dependencies】spel-validator 0.5.1-beta => 0.5.2-beta ([9d39012](https://github.com/continew-org/continew-starter/commit/9d39012f0b53baa81040a863526048955cab6d11))
## [v2.13.2](https://github.com/continew-org/continew-starter/compare/v2.13.1...v2.13.2) (2025-07-21) ## [v2.13.2](https://github.com/continew-org/continew-starter/compare/v2.13.1...v2.13.2) (2025-07-21)
### ✨ 新特性 ### ✨ 新特性

View File

@@ -167,9 +167,9 @@ continew-starter
│ └─ continew-starter-data-mfMyBatis Flex │ └─ continew-starter-data-mfMyBatis Flex
├─ continew-starter-security安全模块 ├─ continew-starter-security安全模块
│ ├─ continew-starter-security-crypto加密字段加解密 │ ├─ continew-starter-security-crypto加密字段加解密
│ ├─ continew-starter-security-xssXSS 过滤)
│ ├─ continew-starter-security-mask脱敏JSON 数据脱敏) │ ├─ continew-starter-security-mask脱敏JSON 数据脱敏)
─ continew-starter-security-password密码编码器 ─ continew-starter-security-xssXSS 过滤
│ └─ continew-starter-security-sensitivewords敏感词
├─ continew-starter-ratelimiter限流模块 ├─ continew-starter-ratelimiter限流模块
├─ continew-starter-idempotent幂等模块 ├─ continew-starter-idempotent幂等模块
├─ continew-starter-trace链路追踪模块 ├─ continew-starter-trace链路追踪模块

View File

@@ -36,6 +36,7 @@ import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.auth.satoken.autoconfigure.dao.SaTokenDaoConfiguration; import top.continew.starter.auth.satoken.autoconfigure.dao.SaTokenDaoConfiguration;
import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.core.constant.PropertiesConstants; import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.GeneralPropertySourceFactory; import top.continew.starter.core.util.GeneralPropertySourceFactory;
@@ -61,7 +62,9 @@ public class SaTokenAutoConfiguration implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(SpringUtil.getBean(SaInterceptor.class)).addPathPatterns(StringConstants.PATH_PATTERN); registry.addInterceptor(SpringUtil.getBean(SaInterceptor.class))
.addPathPatterns(StringConstants.PATH_PATTERN)
.order(OrderedConstants.Interceptor.AUTH_INTERCEPTOR);
} }
/** /**

View File

@@ -13,7 +13,7 @@
<description>ContiNew Starter BOM</description> <description>ContiNew Starter BOM</description>
<properties> <properties>
<revision>2.13.2</revision> <revision>2.13.4</revision>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@@ -104,12 +104,6 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 安全模块 - 密码编码器 -->
<dependency>
<groupId>top.continew.starter</groupId>
<artifactId>continew-starter-security-password</artifactId>
<version>${revision}</version>
</dependency>
<!-- 安全模块 - 加密 --> <!-- 安全模块 - 加密 -->
<dependency> <dependency>
<groupId>top.continew.starter</groupId> <groupId>top.continew.starter</groupId>

View File

@@ -0,0 +1,192 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.cache.redisson.util;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.core.util.SpringUtils;
import java.util.concurrent.TimeUnit;
/**
* Redisson 分布式锁工具类
*
* @author lishuyan
* @since 2.13.4
*/
public class RedisLockUtils implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(RedisLockUtils.class);
/**
* 默认锁过期时间(毫秒)
*/
private static final long DEFAULT_EXPIRE_TIME = 10000L;
/**
* 默认获取锁超时时间(毫秒)
*/
private static final long DEFAULT_TIMEOUT = 5000L;
/**
* Redisson 客户端
*/
private static volatile RedissonClient CLIENT;
/**
* 锁实例
*/
private final RLock lock;
/**
* 是否成功获取锁
*/
private boolean isLocked;
/**
* 获取Redisson客户端实例
*
* @return RedissonClient实例
*/
private static RedissonClient getClient() {
if (CLIENT == null) {
synchronized (RedisLockUtils.class) {
if (CLIENT == null) {
CLIENT = SpringUtils.getBean(RedissonClient.class, false);
}
}
}
return CLIENT;
}
/**
* 私有构造函数,防止外部实例化
*/
private RedisLockUtils(RLock lock, long expireTime, long timeout, TimeUnit unit) {
this.lock = lock;
try {
this.isLocked = lock.tryLock(timeout, expireTime, unit);
if (isLocked) {
log.debug("获取锁成功key: {}", lock.getName());
} else {
log.debug("获取锁失败key: {}", lock.getName());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取锁过程中被中断key: {}", lock.getName(), e);
}
}
/**
* 尝试获取锁(启用看门狗自动续期机制)
*
* @param key 锁的键
* @param timeout 获取锁的超时时间
* @param unit 时间单位
* @return LockUtils 实例
*/
public static RedisLockUtils tryLockWithWatchdog(String key, long timeout, TimeUnit unit) {
RLock lock = getClient().getLock(key);
// 传入-1表示使用看门狗机制
return new RedisLockUtils(lock, -1, timeout, unit);
}
/**
* 尝试获取锁(启用看门狗自动续期机制,默认时间单位为毫秒)
*
* @param key 锁的键
* @param timeout 获取锁的超时时间(单位:毫秒)
* @return LockUtils 实例
*/
public static RedisLockUtils tryLockWithWatchdog(String key, long timeout) {
return tryLockWithWatchdog(key, timeout, TimeUnit.MILLISECONDS);
}
/**
* 尝试获取锁(启用看门狗自动续期机制,使用默认超时时间)
*
* @param key 锁的键
* @return LockUtils 实例
*/
public static RedisLockUtils tryLockWithWatchdog(String key) {
return tryLockWithWatchdog(key, DEFAULT_TIMEOUT);
}
/**
* 尝试获取锁
*
* @param key 锁的键
* @param expireTime 锁的过期时间
* @param timeout 获取锁的超时时间
* @param unit 时间单位
* @return LockUtils 实例
*/
public static RedisLockUtils tryLock(String key, long expireTime, long timeout, TimeUnit unit) {
RLock lock = getClient().getLock(key);
return new RedisLockUtils(lock, expireTime, timeout, unit);
}
/**
* 尝试获取锁(默认时间单位为毫秒)
*
* @param key 锁的键
* @param expireTime 锁的过期时间(单位:毫秒)
* @param timeout 获取锁的超时时间(单位:毫秒)
* @return LockUtils 实例
*/
public static RedisLockUtils tryLock(String key, long expireTime, long timeout) {
return tryLock(key, expireTime, timeout, TimeUnit.MILLISECONDS);
}
/**
* 尝试获取锁(使用默认过期时间和超时时间)
*
* @param key 锁的键
* @return LockUtils 实例
*/
public static RedisLockUtils tryLock(String key) {
return tryLock(key, DEFAULT_EXPIRE_TIME, DEFAULT_TIMEOUT);
}
/**
* 检查是否成功获取锁
*
* @return true成功false失败
*/
public boolean isLocked() {
return isLocked;
}
/**
* 释放锁
*/
@Override
public void close() {
if (isLocked && lock.isHeldByCurrentThread()) {
try {
lock.unlockAsync().get();
log.debug("释放锁成功key: {}", lock.getName());
} catch (Exception e) {
log.error("释放锁失败key: {}", lock.getName(), e);
}
} else {
log.debug("锁未被当前线程持有无需释放key: {}", lock.getName());
}
}
}

View File

@@ -1,146 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.cache.redisson.util;
import cn.hutool.extra.spring.SpringUtil;
import org.redisson.api.*;
import java.util.concurrent.TimeUnit;
/**
* Redis 队列工具类
*
* @author Charles7c
* @since 2.12.1
*/
public class RedisQueueUtils {
private static final RedissonClient CLIENT = SpringUtil.getBean(RedissonClient.class);
private RedisQueueUtils() {
}
/**
* 获取 Redisson 客户端实例
*
* @return Redisson 客户端实例
*/
public static RedissonClient getClient() {
return CLIENT;
}
/**
* 获取阻塞队列实例
*
* @param key 键
* @return 阻塞队列实例
*/
public static <T> RBlockingQueue<T> getBlockingQueue(String key) {
return CLIENT.getBlockingQueue(key);
}
/**
* 添加元素到阻塞队列尾部
* <p>
* 如果队列已满,则返回 false
* </p>
*
* @param key 键
* @param value 值
* @return true添加成功false添加失败
*/
public static <T> boolean addBlockingQueueData(String key, T value) {
RBlockingQueue<T> queue = getBlockingQueue(key);
return queue.offer(value);
}
/**
* 获取并移除阻塞队列头部元素
* <p>
* 如果队列为空,则返回 null
* </p>
*
* @param key 键
* @return 队列头部元素,如果队列为空,则返回 null
*/
public static <T> T getBlockingQueueData(String key) {
RBlockingQueue<T> queue = getBlockingQueue(key);
return queue.poll();
}
/**
* 删除阻塞队列中的指定元素
*
* @param key 键
* @param value 值
* @return true删除成功false删除失败
*/
public static <T> boolean removeBlockingQueueData(String key, T value) {
RBlockingQueue<T> queue = getBlockingQueue(key);
return queue.remove(value);
}
/**
* 获取延迟队列实例
*
* @param key 键
* @return 延迟队列实例
*/
public static <T> RDelayedQueue<T> getDelayedQueue(String key) {
RBlockingQueue<T> blockingQueue = getBlockingQueue(key);
return CLIENT.getDelayedQueue(blockingQueue);
}
/**
* 添加元素到延迟队列
*
* @param key 键
* @param value 值
* @param delay 延迟时间
* @param unit 时间单位
*/
public static <T> void addDelayedQueueData(String key, T value, long delay, TimeUnit unit) {
RDelayedQueue<T> delayedQueue = getDelayedQueue(key);
delayedQueue.offer(value, delay, unit);
}
/**
* 获取并移除延迟队列头部元素
* <p>
* 如果队列为空,则返回 null
* </p>
*
* @param key 键
* @return 队列头部元素,如果队列为空,则返回 null
*/
public static <T> T getDelayedQueueData(String key) {
RDelayedQueue<T> delayedQueue = getDelayedQueue(key);
return delayedQueue.poll();
}
/**
* 移除延迟队列中的指定元素
*
* @param key 键
* @param value 值
* @return true移除成功false移除失败
*/
public static <T> boolean removeDelayedQueueData(String key, T value) {
RDelayedQueue<T> delayedQueue = getDelayedQueue(key);
return delayedQueue.remove(value);
}
}

View File

@@ -25,8 +25,8 @@ import top.continew.starter.core.constant.StringConstants;
import java.time.Duration; import java.time.Duration;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.function.Consumer;
/** /**
* Redis 工具类 * Redis 工具类
@@ -262,6 +262,75 @@ public class RedisUtils {
return CLIENT.getKeys().getKeysStream(options).toList(); return CLIENT.getKeys().getKeysStream(options).toList();
} }
/**
* 设置 Hash 中指定字段的值
*
* @param key Hash 键
* @param field 字段
* @param value 值
* @author KAI
* @since 2.13.4
*/
public static <T> void hSet(String key, String field, T value) {
RMap<String, T> map = CLIENT.getMap(key);
map.put(field, value);
}
/**
* 获取 Hash 中指定字段的值
*
* @param key Hash 键
* @param field 字段
* @return 值
* @author KAI
* @since 2.13.4
*/
public static <T> T hGet(String key, String field) {
RMap<String, T> map = CLIENT.getMap(key);
return map.get(field);
}
/**
* 获取整个 Hash 的所有字段值
*
* @param key Hash 键
* @return Map
* @author KAI
* @since 2.13.4
*/
public static <T> Map<String, T> hGetAll(String key) {
RMap<String, T> map = CLIENT.getMap(key);
return map.readAllMap();
}
/**
* 判断 Hash 中是否存在指定字段
*
* @param key Hash 键
* @param field 字段
* @return true存在false不存在
* @author KAI
* @since 2.13.4
*/
public static boolean hExists(String key, String field) {
RMap<String, ?> map = CLIENT.getMap(key);
return map.containsKey(field);
}
/**
* 删除 Hash 中指定字段
*
* @param key Hash 键
* @param fields 字段数组
* @return 删除成功的字段数量
* @author KAI
* @since 2.13.4
*/
public static long hDel(String key, String... fields) {
RMap<String, ?> map = CLIENT.getMap(key);
return map.fastRemove(fields);
}
/** /**
* 添加元素到 ZSet 中 * 添加元素到 ZSet 中
* *
@@ -452,77 +521,45 @@ public class RedisUtils {
} }
/** /**
* 尝试获取锁 * 发布消息
* *
* @param key * @param name 主题名称
* @param expireTime 锁过期时间(单位:毫秒) * @param msg 发送数据
* @param timeout 获取锁超时时间(单位:毫秒) * @param consumer 自定义处理
* @return true成功false失败 * @author lishuyan
* @since 2.7.2 * @since 2.13.4
*/ */
public static boolean tryLock(String key, long expireTime, long timeout) { public static <T> void publish(String name, T msg, Consumer<T> consumer) {
return tryLock(key, expireTime, timeout, TimeUnit.MILLISECONDS); RTopic topic = CLIENT.getTopic(name);
topic.publish(msg);
consumer.accept(msg);
} }
/** /**
* 释放锁 * 发布消息
* *
* @param key 键 * @param name 主题名称
* @return true释放成功false释放失败 * @param msg 发送数据
* @since 2.7.2 * @author lishuyan
* @since 2.13.4
*/ */
public static boolean unlock(String key) { public static <T> void publish(String name, T msg) {
RLock lock = getLock(key); RTopic topic = CLIENT.getTopic(name);
return unlock(lock); topic.publish(msg);
} }
/** /**
* 尝试获取锁 * 订阅消息
* *
* @param key * @param name 主题名称
* @param expireTime 锁过期时间 * @param clazz 消息类型
* @param timeout 获取锁超时时间 * @param consumer 自定义处理
* @param unit 时间单位 * @author lishuyan
* @return true成功false失败 * @since 2.13.4
* @since 2.7.2
*/ */
public static boolean tryLock(String key, long expireTime, long timeout, TimeUnit unit) { public static <T> void subscribe(String name, Class<T> clazz, Consumer<T> consumer) {
RLock lock = getLock(key); RTopic topic = CLIENT.getTopic(name);
try { topic.addListener(clazz, (channel, msg) -> consumer.accept(msg));
return lock.tryLock(timeout, expireTime, unit);
} catch (InterruptedException e) {
return false;
}
}
/**
* 释放锁
*
* @param lock 锁实例
* @return true释放成功false释放失败
* @since 2.7.2
*/
public static boolean unlock(RLock lock) {
if (lock.isHeldByCurrentThread()) {
try {
lock.unlockAsync().get();
return true;
} catch (ExecutionException | InterruptedException e) {
return false;
}
}
return false;
}
/**
* 获取锁实例
*
* @param key 键
* @return 锁实例
* @since 2.7.2
*/
public static RLock getLock(String key) {
return CLIENT.getLock(key);
} }
/** /**

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.constant;
import org.springframework.core.Ordered;
/**
* 过滤器和拦截器相关顺序常量
*
* @author Charles7c
* @since 2.13.3
*/
public class OrderedConstants {
/**
* 过滤器顺序
*/
public static final class Filter {
/**
* 链路追踪过滤器顺序
*/
public static final int TRACE_FILTER = Ordered.HIGHEST_PRECEDENCE + 100;
/**
* XSS 过滤器顺序
*/
public static final int XSS_FILTER = Ordered.HIGHEST_PRECEDENCE + 200;
/**
* 日志过滤器顺序
*/
public static final int LOG_FILTER = Ordered.LOWEST_PRECEDENCE - 100;
private Filter() {
}
}
/**
* 拦截器顺序
*/
public static final class Interceptor {
/**
* 租户拦截器顺序
*/
public static final int TENANT_INTERCEPTOR = Ordered.HIGHEST_PRECEDENCE + 100;
/**
* 认证拦截器顺序
*/
public static final int AUTH_INTERCEPTOR = Ordered.HIGHEST_PRECEDENCE + 200;
/**
* 日志拦截器顺序
*/
public static final int LOG_INTERCEPTOR = Ordered.LOWEST_PRECEDENCE - 100;
private Interceptor() {
}
}
private OrderedConstants() {
}
}

View File

@@ -54,11 +54,6 @@ public class PropertiesConstants {
*/ */
public static final String SECURITY = CONTINEW_STARTER + StringConstants.DOT + "security"; public static final String SECURITY = CONTINEW_STARTER + StringConstants.DOT + "security";
/**
* 安全-密码编解码配置
*/
public static final String SECURITY_PASSWORD = SECURITY + StringConstants.DOT + "password";
/** /**
* 安全-加/解密配置 * 安全-加/解密配置
*/ */

View File

@@ -17,11 +17,17 @@
package top.continew.starter.core.util; package top.continew.starter.core.util;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BusinessException;
import java.lang.invoke.MethodHandleProxies;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -60,4 +66,27 @@ public class ReflectUtils {
Field[] fields = ReflectUtil.getFields(beanClass); Field[] fields = ReflectUtil.getFields(beanClass);
return Arrays.stream(fields).filter(f -> !Modifier.isStatic(f.getModifiers())).collect(Collectors.toList()); return Arrays.stream(fields).filter(f -> !Modifier.isStatic(f.getModifiers())).collect(Collectors.toList());
} }
/**
* 通过反射创建方法引用,支持在父类中查找方法
*
* @param clazz 实体类类型
* @param methodName 方法名
* @param <T> 实体类类型
* @param <K> 返回值类型
* @return Function<T, K> 方法引用
* @throws IllegalArgumentException 如果参数不合法
* @author lishuyan
* @since 2.13.2
*/
@SuppressWarnings("unchecked")
public static <T, K> Function<T, K> createMethodReference(Class<T> clazz, String methodName) {
try {
Method method = ReflectUtil.getMethodByName(clazz, methodName);
method.setAccessible(true);
return MethodHandleProxies.asInterfaceInstance(Function.class, MethodHandles.lookup().unreflect(method));
} catch (Exception e) {
throw new BusinessException("创建方法引用失败:" + clazz.getName() + StringConstants.DOT + methodName, e);
}
}
} }

View File

@@ -24,6 +24,7 @@ import cn.hutool.core.lang.tree.parser.NodeParser;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
@@ -31,14 +32,18 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
* 扩展 hutool TreeUtil 封装树构建 * 树工具类
* *
* @author Lion Li * <p>
* 扩展 Hutool TreeUtil 封装树构建
* </p>
*
* @author Lion Li<a href="https://gitee.com/dromara/RuoYi-Vue-Plus">RuoYi-Vue-Plus</a>
* @author lishuyan * @author lishuyan
*/ */
public class TreeBuildUtils extends TreeUtil { public class TreeUtils extends TreeUtil {
private TreeBuildUtils() { private TreeUtils() {
} }
/** /**
@@ -52,7 +57,7 @@ public class TreeBuildUtils extends TreeUtil {
*/ */
public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> nodeParser) { public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList(); return new ArrayList<>(0);
} }
K k = ReflectUtil.invoke(list.get(0), CharSequenceUtil.genGetter("parentId")); K k = ReflectUtil.invoke(list.get(0), CharSequenceUtil.genGetter("parentId"));
return TreeUtil.build(list, k, TreeNodeConfig.DEFAULT_CONFIG, nodeParser); return TreeUtil.build(list, k, TreeNodeConfig.DEFAULT_CONFIG, nodeParser);
@@ -70,7 +75,7 @@ public class TreeBuildUtils extends TreeUtil {
*/ */
public static <T, K> List<Tree<K>> build(List<T> list, K parentId, NodeParser<T, K> nodeParser) { public static <T, K> List<Tree<K>> build(List<T> list, K parentId, NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList(); return new ArrayList<>(0);
} }
return TreeUtil.build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser); return TreeUtil.build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser);
} }
@@ -91,7 +96,7 @@ public class TreeBuildUtils extends TreeUtil {
TreeNodeConfig treeNodeConfig, TreeNodeConfig treeNodeConfig,
NodeParser<T, K> nodeParser) { NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList(); return new ArrayList<>(0);
} }
return TreeUtil.build(list, parentId, treeNodeConfig, nodeParser); return TreeUtil.build(list, parentId, treeNodeConfig, nodeParser);
} }
@@ -112,7 +117,7 @@ public class TreeBuildUtils extends TreeUtil {
Function<T, K> getParentId, Function<T, K> getParentId,
NodeParser<T, K> parser) { NodeParser<T, K> parser) {
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList(); return new ArrayList<>(0);
} }
Set<K> rootParentIds = CollUtils.mapToSet(list, getParentId); Set<K> rootParentIds = CollUtils.mapToSet(list, getParentId);
rootParentIds.removeAll(CollUtils.mapToSet(list, getId)); rootParentIds.removeAll(CollUtils.mapToSet(list, getId));
@@ -140,7 +145,7 @@ public class TreeBuildUtils extends TreeUtil {
TreeNodeConfig treeNodeConfig, TreeNodeConfig treeNodeConfig,
NodeParser<T, K> parser) { NodeParser<T, K> parser) {
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList(); return new ArrayList<>(0);
} }
Set<K> rootParentIds = CollUtils.mapToSet(list, getParentId); Set<K> rootParentIds = CollUtils.mapToSet(list, getParentId);
rootParentIds.removeAll(CollUtils.mapToSet(list, getId)); rootParentIds.removeAll(CollUtils.mapToSet(list, getId));
@@ -159,9 +164,9 @@ public class TreeBuildUtils extends TreeUtil {
*/ */
public static <K> List<Tree<K>> getLeafNodes(List<Tree<K>> nodes) { public static <K> List<Tree<K>> getLeafNodes(List<Tree<K>> nodes) {
if (CollUtil.isEmpty(nodes)) { if (CollUtil.isEmpty(nodes)) {
return CollUtil.newArrayList(); return new ArrayList<>(0);
} }
return nodes.stream().flatMap(TreeBuildUtils::extractLeafNodes).collect(Collectors.toList()); return nodes.stream().flatMap(TreeUtils::extractLeafNodes).collect(Collectors.toList());
} }
/** /**
@@ -176,7 +181,7 @@ public class TreeBuildUtils extends TreeUtil {
return Stream.of(node); return Stream.of(node);
} else { } else {
// 递归调用获取所有子节点的叶子节点 // 递归调用获取所有子节点的叶子节点
return node.getChildren().stream().flatMap(TreeBuildUtils::extractLeafNodes); return node.getChildren().stream().flatMap(TreeUtils::extractLeafNodes);
} }
} }

View File

@@ -16,6 +16,7 @@
package top.continew.starter.data.annotation; package top.continew.starter.data.annotation;
import top.continew.starter.data.enums.LogicalRelation;
import top.continew.starter.data.enums.QueryType; import top.continew.starter.data.enums.QueryType;
import java.lang.annotation.*; import java.lang.annotation.*;
@@ -46,4 +47,9 @@ public @interface Query {
* 查询类型(等值查询、模糊查询、范围查询等) * 查询类型(等值查询、模糊查询、范围查询等)
*/ */
QueryType type() default QueryType.EQ; QueryType type() default QueryType.EQ;
/**
* 多列查询时的逻辑关系(仅当 columns 长度大于 1 时生效)
*/
LogicalRelation logicalRelation() default LogicalRelation.OR;
} }

View File

@@ -14,23 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
package top.continew.starter.security.password.constant; package top.continew.starter.data.enums;
import java.util.regex.Pattern;
/** /**
* 密码编码器相关常量 * 逻辑关系枚举
* *
* @author Charles7c * @author Charles7c
* @since 2.12.0 * @since 2.13.3
*/ */
public class PasswordEncoderConstants { public enum LogicalRelation {
/** /**
* BCrypt 正则表达式 * 并且关系
*/ */
public static final Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}"); AND,
private PasswordEncoderConstants() { /**
} * 或者关系
*/
OR
} }

View File

@@ -32,6 +32,7 @@ import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.data.annotation.Query; import top.continew.starter.data.annotation.Query;
import top.continew.starter.data.annotation.QueryIgnore; import top.continew.starter.data.annotation.QueryIgnore;
import top.continew.starter.data.enums.QueryType; import top.continew.starter.data.enums.QueryType;
import top.continew.starter.data.enums.LogicalRelation;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
@@ -174,8 +175,21 @@ public class QueryWrapperHelper {
return consumers; return consumers;
} }
// 解析多列查询 // 解析多列查询
LogicalRelation logicalRelation = queryAnnotation.logicalRelation();
List<Consumer<QueryWrapper<R>>> columnConsumers = new ArrayList<>();
for (String column : columns) { for (String column : columns) {
parse(queryType, column, fieldValue, consumers); parse(queryType, column, fieldValue, columnConsumers);
}
if (logicalRelation == LogicalRelation.AND) {
if (!columnConsumers.isEmpty()) {
consumers.add(q -> {
columnConsumers.get(0).accept(q);
columnConsumers.subList(1, columnConsumers.size()).forEach(q::and);
});
}
} else {
consumers.addAll(columnConsumers);
} }
return consumers; return consumers;
} catch (BadRequestException e) { } catch (BadRequestException e) {

View File

@@ -14,7 +14,7 @@
<properties> <properties>
<!-- Project Version --> <!-- Project Version -->
<revision>2.13.2</revision> <revision>2.13.4</revision>
<!-- Core Framework Versions --> <!-- Core Framework Versions -->
<spring-boot.version>3.3.12</spring-boot.version> <spring-boot.version>3.3.12</spring-boot.version>
@@ -58,7 +58,7 @@
<!-- Validation and Response Processing Versions --> <!-- Validation and Response Processing Versions -->
<graceful-response.version>5.0.5-boot3</graceful-response.version> <graceful-response.version>5.0.5-boot3</graceful-response.version>
<spel-validator.version>0.5.1-beta</spel-validator.version> <spel-validator.version>0.5.2-beta</spel-validator.version>
<crane4j.version>2.9.0</crane4j.version> <crane4j.version>2.9.0</crane4j.version>
<!-- API Documentation Versions --> <!-- API Documentation Versions -->

View File

@@ -39,5 +39,5 @@ public @interface CrudRequestMapping {
/** /**
* API 列表 * API 列表
*/ */
Api[] api() default {Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE, Api.EXPORT}; Api[] api() default {Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.BATCH_DELETE, Api.EXPORT, Api.DICT};
} }

View File

@@ -17,13 +17,13 @@
package top.continew.starter.extension.crud.annotation; package top.continew.starter.extension.crud.annotation;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import top.continew.starter.extension.crud.autoconfigure.CrudApiAutoConfiguration;
import top.continew.starter.extension.crud.autoconfigure.CrudRequestMappingAutoConfiguration; import top.continew.starter.extension.crud.autoconfigure.CrudRequestMappingAutoConfiguration;
import top.continew.starter.extension.crud.autoconfigure.CrudRestControllerAutoConfiguration;
import java.lang.annotation.*; import java.lang.annotation.*;
/** /**
* CRUD REST Controller 启用注解 * CRUD API 启用注解
* *
* @author Charles7c * @author Charles7c
* @since 1.2.0 * @since 1.2.0
@@ -31,5 +31,5 @@ import java.lang.annotation.*;
@Target({ElementType.TYPE}) @Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
@Import({CrudRequestMappingAutoConfiguration.class, CrudRestControllerAutoConfiguration.class}) @Import({CrudRequestMappingAutoConfiguration.class, CrudApiAutoConfiguration.class})
public @interface EnableCrudRestController {} public @interface EnableCrudApi {}

View File

@@ -28,16 +28,16 @@ import top.continew.starter.extension.crud.aop.CrudApiAnnotationAdvisor;
import top.continew.starter.extension.crud.aop.CrudApiAnnotationInterceptor; import top.continew.starter.extension.crud.aop.CrudApiAnnotationInterceptor;
/** /**
* CRUD REST Controller 自动配置 * CRUD API 自动配置
* *
* @author Charles7c * @author Charles7c
* @since 2.7.5 * @since 2.7.5
*/ */
@AutoConfiguration @AutoConfiguration
@EnableConfigurationProperties(CrudProperties.class) @EnableConfigurationProperties(CrudProperties.class)
public class CrudRestControllerAutoConfiguration { public class CrudApiAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(CrudRestControllerAutoConfiguration.class); private static final Logger log = LoggerFactory.getLogger(CrudApiAutoConfiguration.class);
/** /**
* CRUD API 注解通知 * CRUD API 注解通知
@@ -59,6 +59,6 @@ public class CrudRestControllerAutoConfiguration {
@PostConstruct @PostConstruct
public void postConstruct() { public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Extension-CRUD REST Controller' completed initialization."); log.debug("[ContiNew Starter] - Auto Configuration 'Extension-CRUD API' completed initialization.");
} }
} }

View File

@@ -17,7 +17,6 @@
package top.continew.starter.extension.crud.autoconfigure; package top.continew.starter.extension.crud.autoconfigure;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
@@ -34,7 +33,6 @@ import org.springframework.web.servlet.resource.ResourceUrlProvider;
* @since 1.0.0 * @since 1.0.0
*/ */
@Configuration @Configuration
@EnableConfigurationProperties(CrudProperties.class)
public class CrudRequestMappingAutoConfiguration extends DelegatingWebMvcConfiguration { public class CrudRequestMappingAutoConfiguration extends DelegatingWebMvcConfiguration {
/** /**

View File

@@ -34,6 +34,7 @@ import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.req.IdsReq; import top.continew.starter.extension.crud.model.req.IdsReq;
import top.continew.starter.extension.crud.model.resp.IdResp; import top.continew.starter.extension.crud.model.resp.IdResp;
import top.continew.starter.extension.crud.model.resp.BasePageResp; import top.continew.starter.extension.crud.model.resp.BasePageResp;
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.extension.crud.service.CrudService; import top.continew.starter.extension.crud.service.CrudService;
import top.continew.starter.extension.crud.validation.CrudValidationGroup; import top.continew.starter.extension.crud.validation.CrudValidationGroup;
@@ -187,4 +188,32 @@ public abstract class AbstractCrudController<S extends CrudService<L, D, Q, C>,
public void export(@Valid Q query, @Valid SortQuery sortQuery, HttpServletResponse response) { public void export(@Valid Q query, @Valid SortQuery sortQuery, HttpServletResponse response) {
baseService.export(query, sortQuery, response); baseService.export(query, sortQuery, response);
} }
/**
* 查询字典列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 字典列表信息
*/
@CrudApi(Api.DICT)
@Operation(summary = "查询字典列表", description = "查询字典列表(下拉选项等场景)")
@GetMapping("/dict")
public List<LabelValueResp> dict(@Valid Q query, @Valid SortQuery sortQuery) {
return baseService.dict(query, sortQuery);
}
/**
* 查询字典树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 字典树列表信息
*/
@CrudApi(Api.DICT_TREE)
@Operation(summary = "查询字典树列表", description = "查询树型结构字典列表(树型结构下拉选项等场景)")
@GetMapping("/dict/tree")
public List<Tree<Long>> dictTree(@Valid Q query, @Valid SortQuery sortQuery) {
return baseService.tree(query, sortQuery, true);
}
} }

View File

@@ -68,4 +68,14 @@ public enum Api {
* 批量删除 * 批量删除
*/ */
BATCH_DELETE, BATCH_DELETE,
/**
* 字典列表(下拉选项等场景)
*/
DICT,
/**
* 字典树列表(树型结构下拉选项等场景)
*/
DICT_TREE
} }

View File

@@ -21,6 +21,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.model.query.PageQuery; import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery; import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.BasePageResp; import top.continew.starter.extension.crud.model.resp.BasePageResp;
@@ -59,15 +60,30 @@ public interface CrudService<L, D, Q, C> {
*/ */
List<L> list(@Valid Q query, @Valid SortQuery sortQuery); List<L> list(@Valid Q query, @Valid SortQuery sortQuery);
/**
* 查询树列表(多个根节点)
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段,简单树(下拉列表)使用全局配置结构,复杂树(表格)使用 @TreeField 局部配置)
* @return 树列表信息
* @see TreeField
*/
List<Tree<Long>> tree(@Valid Q query, @Valid SortQuery sortQuery, boolean isSimple);
/** /**
* 查询树列表 * 查询树列表
* *
* @param query 查询条件 * @param query 查询条件
* @param sortQuery 排序查询条件 * @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段,简单树(下拉列表)使用全局配置结构,复杂树(表格)使用 @DictField 局部配置) * @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段,简单树(下拉列表)使用全局配置结构,复杂树(表格)使用 @TreeField 局部配置)
* @param isSingleRoot 是否为单个根节点
* @return 树列表信息 * @return 树列表信息
* @author lishuyan
* @since 2.13.3
* @see TreeField
*/ */
List<Tree<Long>> tree(@Valid Q query, @Valid SortQuery sortQuery, boolean isSimple); List<Tree<Long>> tree(@Valid Q query, @Valid SortQuery sortQuery, boolean isSimple, boolean isSingleRoot);
/** /**
* 查询详情 * 查询详情
@@ -77,16 +93,6 @@ public interface CrudService<L, D, Q, C> {
*/ */
D get(Long id); D get(Long id);
/**
* 查询字典列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 字典列表信息
* @since 2.1.0
*/
List<LabelValueResp> listDict(@Valid Q query, @Valid SortQuery sortQuery);
/** /**
* 创建 * 创建
* *
@@ -120,4 +126,15 @@ public interface CrudService<L, D, Q, C> {
* @param response 响应对象 * @param response 响应对象
*/ */
void export(@Valid Q query, @Valid SortQuery sortQuery, HttpServletResponse response); void export(@Valid Q query, @Valid SortQuery sortQuery, HttpServletResponse response);
/**
* 查询字典列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 字典列表信息
* @since 2.1.0
* @see top.continew.starter.extension.crud.annotation.DictModel
*/
List<LabelValueResp> dict(@Valid Q query, @Valid SortQuery sortQuery);
} }

View File

@@ -19,9 +19,11 @@ package top.continew.starter.extension.crud.service;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig; import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil; import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
@@ -32,10 +34,14 @@ import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ReflectUtils; import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.TreeUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.ValidationUtils; import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.data.base.BaseMapper; import top.continew.starter.data.base.BaseMapper;
import top.continew.starter.data.service.impl.ServiceImpl; import top.continew.starter.data.service.impl.ServiceImpl;
import top.continew.starter.data.util.QueryWrapperHelper; import top.continew.starter.data.util.QueryWrapperHelper;
import top.continew.starter.excel.util.ExcelUtils;
import top.continew.starter.extension.crud.annotation.DictModel;
import top.continew.starter.extension.crud.annotation.TreeField; import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.autoconfigure.CrudProperties; import top.continew.starter.extension.crud.autoconfigure.CrudProperties;
import top.continew.starter.extension.crud.autoconfigure.CrudTreeProperties; import top.continew.starter.extension.crud.autoconfigure.CrudTreeProperties;
@@ -44,12 +50,10 @@ import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery; import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp; import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp; import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.excel.util.ExcelUtils;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.function.Function;
import java.util.Optional;
/** /**
* CRUD 业务实现基类 * CRUD 业务实现基类
@@ -89,9 +93,14 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
@Override @Override
public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) { public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) {
return this.tree(query, sortQuery, isSimple, false);
}
@Override
public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple, boolean isSingleRoot) {
List<L> list = this.list(query, sortQuery); List<L> list = this.list(query, sortQuery);
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0); return CollUtil.newArrayList();
} }
CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class); CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class);
CrudTreeProperties treeProperties = crudProperties.getTree(); CrudTreeProperties treeProperties = crudProperties.getTree();
@@ -106,21 +115,19 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
treeNodeConfig = treeProperties.genTreeNodeConfig(treeField); treeNodeConfig = treeProperties.genTreeNodeConfig(treeField);
rootId = treeField.rootId(); rootId = treeField.rootId();
} }
// 构建树 if (isSingleRoot) {
return TreeUtil.build(list, rootId, treeNodeConfig, (node, tree) -> { // 构建单根节点树
tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value()))); return TreeUtil.build(list, rootId, treeNodeConfig, (node,
tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey()))); tree) -> buildTreeField(isSimple, node, tree, treeField));
tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey()))); } else {
tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey()))); Function<L, Long> getId = ReflectUtils.createMethodReference(listClass, CharSequenceUtil.genGetter(treeField
// 如果构建简单树结构,则不包含扩展字段 .value()));
if (!isSimple) { Function<L, Long> getParentId = ReflectUtils.createMethodReference(listClass, CharSequenceUtil
List<Field> fieldList = ReflectUtils.getNonStaticFields(listClass); .genGetter(treeField.parentIdKey()));
fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField // 构建多根节点树
.parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField.childrenKey())); return TreeUtils.buildMultiRoot(list, getId, getParentId, treeNodeConfig, (node,
fieldList.forEach(f -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil.genGetter(f tree) -> buildTreeField(isSimple, node, tree, treeField));
.getName()))));
} }
});
} }
@Override @Override
@@ -131,11 +138,6 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
return detail; return detail;
} }
@Override
public List<LabelValueResp> listDict(Q query, SortQuery sortQuery) {
return List.of();
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Long create(C req) { public Long create(C req) {
@@ -171,6 +173,44 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
ExcelUtils.export(list, "导出数据", detailClass, response); ExcelUtils.export(list, "导出数据", detailClass, response);
} }
@Override
public List<LabelValueResp> dict(Q query, SortQuery sortQuery) {
DictModel dictModel = entityClass.getDeclaredAnnotation(DictModel.class);
CheckUtils.throwIfNull(dictModel, "请添加并配置 @DictModel 字典结构信息");
List<L> list = this.list(query, sortQuery);
// 解析映射
List<LabelValueResp> respList = new ArrayList<>(list.size());
String labelKey = dictModel.labelKey().contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(dictModel.labelKey(), StringConstants.DOT, true)
: dictModel.labelKey();
String valueKey = dictModel.valueKey().contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(dictModel.valueKey(), StringConstants.DOT, true)
: dictModel.valueKey();
List<String> extraFieldNames = Arrays.stream(dictModel.extraKeys())
.map(extraKey -> extraKey.contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(extraKey, StringConstants.DOT, true)
: extraKey)
.map(CharSequenceUtil::toCamelCase)
.toList();
for (L entity : list) {
LabelValueResp<Object> labelValueResp = new LabelValueResp<>();
labelValueResp.setLabel(Convert.toStr(ReflectUtil.getFieldValue(entity, CharSequenceUtil
.toCamelCase(labelKey))));
labelValueResp.setValue(ReflectUtil.getFieldValue(entity, CharSequenceUtil.toCamelCase(valueKey)));
respList.add(labelValueResp);
if (CollUtil.isEmpty(extraFieldNames)) {
continue;
}
// 额外数据
Map<String, Object> extraMap = MapUtil.newHashMap(dictModel.extraKeys().length);
for (String extraFieldName : extraFieldNames) {
extraMap.put(extraFieldName, ReflectUtil.getFieldValue(entity, extraFieldName));
}
labelValueResp.setExtra(extraMap);
}
return respList;
}
/** /**
* 查询列表 * 查询列表
* *
@@ -323,4 +363,26 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
return (Class<Q>)this.typeArguments[4]; return (Class<Q>)this.typeArguments[4];
} }
/**
* 构建树字段
*
* @param isSimple 是否简单树结构
* @param node 节点
* @param tree 树
* @param treeField 树字段
*/
private void buildTreeField(boolean isSimple, L node, Tree<Long> tree, TreeField treeField) {
tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value())));
tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey())));
tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey())));
tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey())));
// 如果构建简单树结构,则不包含扩展字段
if (!isSimple) {
List<Field> fieldList = ReflectUtils.getNonStaticFields(listClass);
fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField
.parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField.childrenKey()));
fieldList.forEach(f -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil.genGetter(f
.getName()))));
}
}
} }

View File

@@ -22,6 +22,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig; import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
@@ -35,7 +36,7 @@ import org.springframework.transaction.annotation.Transactional;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ClassUtils; import top.continew.starter.core.util.ClassUtils;
import top.continew.starter.core.util.ReflectUtils; import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.TreeBuildUtils; import top.continew.starter.core.util.TreeUtils;
import top.continew.starter.core.util.validation.CheckUtils; import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.ValidationUtils; import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.data.mapper.BaseMapper; import top.continew.starter.data.mapper.BaseMapper;
@@ -52,10 +53,7 @@ import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp; import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp; import top.continew.starter.extension.crud.model.resp.PageResp;
import java.lang.invoke.MethodHandleProxies;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
@@ -97,55 +95,40 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
@Override @Override
public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) { public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) {
return this.tree(query, sortQuery, isSimple, false);
}
@Override
public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple, boolean isSingleRoot) {
List<L> list = this.list(query, sortQuery); List<L> list = this.list(query, sortQuery);
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0); return CollUtil.newArrayList();
} }
CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class); CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class);
CrudTreeProperties treeProperties = crudProperties.getTree(); CrudTreeProperties treeProperties = crudProperties.getTree();
TreeField treeField = listClass.getDeclaredAnnotation(TreeField.class); TreeField treeField = listClass.getDeclaredAnnotation(TreeField.class);
TreeNodeConfig treeNodeConfig;
Long rootId;
// 简单树(下拉列表)使用全局配置结构,复杂树(表格)使用局部配置 // 简单树(下拉列表)使用全局配置结构,复杂树(表格)使用局部配置
TreeNodeConfig treeNodeConfig = isSimple if (isSimple) {
? treeProperties.genTreeNodeConfig() treeNodeConfig = treeProperties.genTreeNodeConfig();
: treeProperties.genTreeNodeConfig(treeField); rootId = treeProperties.getRootId();
String valueGetter = CharSequenceUtil.genGetter(treeField.value()); } else {
String parentIdKeyGetter = CharSequenceUtil.genGetter(treeField.parentIdKey()); treeNodeConfig = treeProperties.genTreeNodeConfig(treeField);
Function<L, Long> getId = createMethodReference(listClass, valueGetter); rootId = treeField.rootId();
Function<L, Long> getParentId = createMethodReference(listClass, parentIdKeyGetter);
// 构建树
return TreeBuildUtils.buildMultiRoot(list, getId, getParentId, treeNodeConfig, (node, tree) -> {
tree.setId(ReflectUtil.invoke(node, valueGetter));
tree.setParentId(ReflectUtil.invoke(node, parentIdKeyGetter));
tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey())));
tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey())));
// 如果构建简单树结构,则不包含扩展字段
if (!isSimple) {
List<Field> fieldList = ReflectUtils.getNonStaticFields(listClass);
fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField
.parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField.childrenKey()));
fieldList.forEach(f -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil.genGetter(f
.getName()))));
} }
}); if (isSingleRoot) {
} // 构建单根节点树
return TreeUtil.build(list, rootId, treeNodeConfig, (node,
/** tree) -> buildTreeField(isSimple, node, tree, treeField));
* 通过反射创建方法引用 } else {
* Function<L, Long> getId = ReflectUtils.createMethodReference(listClass, CharSequenceUtil.genGetter(treeField
* @param clazz 实体类类型 .value()));
* @param methodName 方法名 Function<L, Long> getParentId = ReflectUtils.createMethodReference(listClass, CharSequenceUtil
* @param <T> 实体类类型 .genGetter(treeField.parentIdKey()));
* @param <K> 返回值类型 // 构建多根节点树
* @return Function<T, K> 方法引用 return TreeUtils.buildMultiRoot(list, getId, getParentId, treeNodeConfig, (node,
*/ tree) -> buildTreeField(isSimple, node, tree, treeField));
@SuppressWarnings("unchecked")
public static <T, K> Function<T, K> createMethodReference(Class<T> clazz, String methodName) {
try {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
return MethodHandleProxies.asInterfaceInstance(Function.class, MethodHandles.lookup().unreflect(method));
} catch (Exception e) {
throw new RuntimeException("Failed to create method reference for " + methodName, e);
} }
} }
@@ -157,44 +140,6 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
return detail; return detail;
} }
@Override
public List<LabelValueResp> listDict(Q query, SortQuery sortQuery) {
DictModel dictModel = super.getEntityClass().getDeclaredAnnotation(DictModel.class);
CheckUtils.throwIfNull(dictModel, "请添加并配置 @DictModel 字典结构信息");
List<L> list = this.list(query, sortQuery);
// 解析映射
List<LabelValueResp> respList = new ArrayList<>(list.size());
String labelKey = dictModel.labelKey().contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(dictModel.labelKey(), StringConstants.DOT, true)
: dictModel.labelKey();
String valueKey = dictModel.valueKey().contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(dictModel.valueKey(), StringConstants.DOT, true)
: dictModel.valueKey();
List<String> extraFieldNames = Arrays.stream(dictModel.extraKeys())
.map(extraKey -> extraKey.contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(extraKey, StringConstants.DOT, true)
: extraKey)
.map(CharSequenceUtil::toCamelCase)
.toList();
for (L entity : list) {
LabelValueResp<Object> labelValueResp = new LabelValueResp<>();
labelValueResp.setLabel(Convert.toStr(ReflectUtil.getFieldValue(entity, CharSequenceUtil
.toCamelCase(labelKey))));
labelValueResp.setValue(ReflectUtil.getFieldValue(entity, CharSequenceUtil.toCamelCase(valueKey)));
respList.add(labelValueResp);
if (CollUtil.isEmpty(extraFieldNames)) {
continue;
}
// 额外数据
Map<String, Object> extraMap = MapUtil.newHashMap(dictModel.extraKeys().length);
for (String extraFieldName : extraFieldNames) {
extraMap.put(extraFieldName, ReflectUtil.getFieldValue(entity, extraFieldName));
}
labelValueResp.setExtra(extraMap);
}
return respList;
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Long create(C req) { public Long create(C req) {
@@ -230,6 +175,44 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
ExcelUtils.export(list, "导出数据", this.getDetailClass(), response); ExcelUtils.export(list, "导出数据", this.getDetailClass(), response);
} }
@Override
public List<LabelValueResp> dict(Q query, SortQuery sortQuery) {
DictModel dictModel = super.getEntityClass().getDeclaredAnnotation(DictModel.class);
CheckUtils.throwIfNull(dictModel, "请添加并配置 @DictModel 字典结构信息");
List<L> list = this.list(query, sortQuery);
// 解析映射
List<LabelValueResp> respList = new ArrayList<>(list.size());
String labelKey = dictModel.labelKey().contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(dictModel.labelKey(), StringConstants.DOT, true)
: dictModel.labelKey();
String valueKey = dictModel.valueKey().contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(dictModel.valueKey(), StringConstants.DOT, true)
: dictModel.valueKey();
List<String> extraFieldNames = Arrays.stream(dictModel.extraKeys())
.map(extraKey -> extraKey.contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(extraKey, StringConstants.DOT, true)
: extraKey)
.map(CharSequenceUtil::toCamelCase)
.toList();
for (L entity : list) {
LabelValueResp<Object> labelValueResp = new LabelValueResp<>();
labelValueResp.setLabel(Convert.toStr(ReflectUtil.getFieldValue(entity, CharSequenceUtil
.toCamelCase(labelKey))));
labelValueResp.setValue(ReflectUtil.getFieldValue(entity, CharSequenceUtil.toCamelCase(valueKey)));
respList.add(labelValueResp);
if (CollUtil.isEmpty(extraFieldNames)) {
continue;
}
// 额外数据
Map<String, Object> extraMap = MapUtil.newHashMap(dictModel.extraKeys().length);
for (String extraFieldName : extraFieldNames) {
extraMap.put(extraFieldName, ReflectUtil.getFieldValue(entity, extraFieldName));
}
labelValueResp.setExtra(extraMap);
}
return respList;
}
/** /**
* 获取当前列表信息类型 * 获取当前列表信息类型
* *
@@ -401,4 +384,27 @@ public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D,
protected void afterDelete(List<Long> ids) { protected void afterDelete(List<Long> ids) {
/* 删除后置处理 */ /* 删除后置处理 */
} }
/**
* 构建树字段
*
* @param isSimple 是否简单树结构
* @param node 节点
* @param tree 树
* @param treeField 树字段
*/
private void buildTreeField(boolean isSimple, L node, Tree<Long> tree, TreeField treeField) {
tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value())));
tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey())));
tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey())));
tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey())));
// 如果构建简单树结构,则不包含扩展字段
if (!isSimple) {
List<Field> fieldList = ReflectUtils.getNonStaticFields(listClass);
fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField
.parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField.childrenKey()));
fieldList.forEach(f -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil.genGetter(f
.getName()))));
}
}
} }

View File

@@ -19,9 +19,9 @@ package top.continew.starter.extension.tenant.autoconfigure;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.extension.tenant.annotation.ConditionalOnEnabledTenant; import top.continew.starter.extension.tenant.annotation.ConditionalOnEnabledTenant;
import top.continew.starter.extension.tenant.config.TenantProvider; import top.continew.starter.extension.tenant.config.TenantProvider;
import top.continew.starter.extension.tenant.interceptor.TenantInterceptor; import top.continew.starter.extension.tenant.interceptor.TenantInterceptor;
@@ -49,6 +49,6 @@ public class TenantWebMvcAutoConfiguration implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantInterceptor(tenantProperties, tenantProvider)) registry.addInterceptor(new TenantInterceptor(tenantProperties, tenantProvider))
.order(Ordered.HIGHEST_PRECEDENCE); .order(OrderedConstants.Interceptor.TENANT_INTERCEPTOR);
} }
} }

View File

@@ -22,8 +22,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.log.annotation.ConditionalOnEnabledLog; import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
import top.continew.starter.log.aspect.AccessLogAspect; import top.continew.starter.log.aspect.AccessLogAspect;
import top.continew.starter.log.aspect.LogAspect; import top.continew.starter.log.aspect.LogAspect;
@@ -61,8 +63,11 @@ public class LogAutoConfiguration {
*/ */
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public LogFilter logFilter() { public FilterRegistrationBean<LogFilter> logFilter() {
return new LogFilter(logProperties); FilterRegistrationBean<LogFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LogFilter(logProperties));
registrationBean.setOrder(OrderedConstants.Filter.LOG_FILTER);
return registrationBean;
} }
/** /**

View File

@@ -22,7 +22,6 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import top.continew.starter.core.wrapper.RepeatReadRequestWrapper; import top.continew.starter.core.wrapper.RepeatReadRequestWrapper;
@@ -40,7 +39,7 @@ import java.net.URISyntaxException;
* @author echo * @author echo
* @since 1.1.0 * @since 1.1.0
*/ */
public class LogFilter extends OncePerRequestFilter implements Ordered { public class LogFilter extends OncePerRequestFilter {
private final LogProperties logProperties; private final LogProperties logProperties;
@@ -48,11 +47,6 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
this.logProperties = logProperties; this.logProperties = logProperties;
} }
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
@Override @Override
protected void doFilterInternal(@NonNull HttpServletRequest request, protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response, @NonNull HttpServletResponse response,

View File

@@ -22,10 +22,12 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.log.annotation.ConditionalOnEnabledLog; import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
import top.continew.starter.log.dao.LogDao; import top.continew.starter.log.dao.LogDao;
@@ -59,7 +61,8 @@ public class LogAutoConfiguration implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor(logProperties, logHandler(), logDao())) registry.addInterceptor(new LogInterceptor(logProperties, logHandler(), logDao()))
.addPathPatterns(StringConstants.PATH_PATTERN) .addPathPatterns(StringConstants.PATH_PATTERN)
.excludePathPatterns(logProperties.getExcludePatterns()); .excludePathPatterns(logProperties.getExcludePatterns())
.order(OrderedConstants.Interceptor.LOG_INTERCEPTOR);
} }
/** /**
@@ -67,8 +70,11 @@ public class LogAutoConfiguration implements WebMvcConfigurer {
*/ */
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public LogFilter logFilter() { public FilterRegistrationBean<LogFilter> logFilter() {
return new LogFilter(logProperties); FilterRegistrationBean<LogFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LogFilter(logProperties));
registrationBean.setOrder(OrderedConstants.Filter.LOG_FILTER);
return registrationBean;
} }
/** /**

View File

@@ -22,6 +22,12 @@
<artifactId>hutool-crypto</artifactId> <artifactId>hutool-crypto</artifactId>
</dependency> </dependency>
<!-- Spring Security 附带的一个密码加密库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- MyBatis PlusMyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) --> <!-- MyBatis PlusMyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) -->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>

View File

@@ -24,10 +24,21 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import top.continew.starter.core.constant.PropertiesConstants; import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.crypto.core.MyBatisDecryptInterceptor; import top.continew.starter.core.util.GeneralPropertySourceFactory;
import top.continew.starter.security.crypto.core.MyBatisEncryptInterceptor; import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.security.crypto.utils.EncryptHelper; import top.continew.starter.security.crypto.enums.PasswordEncoderAlgorithm;
import top.continew.starter.security.crypto.mybatis.MyBatisDecryptInterceptor;
import top.continew.starter.security.crypto.mybatis.MyBatisEncryptInterceptor;
import top.continew.starter.security.crypto.util.EncryptHelper;
import top.continew.starter.security.crypto.util.PasswordEncoderUtil;
import java.util.HashMap;
import java.util.Map;
/** /**
* 加/解密自动配置 * 加/解密自动配置
@@ -39,6 +50,7 @@ import top.continew.starter.security.crypto.utils.EncryptHelper;
@AutoConfiguration @AutoConfiguration
@EnableConfigurationProperties(CryptoProperties.class) @EnableConfigurationProperties(CryptoProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_CRYPTO, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true) @ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_CRYPTO, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@PropertySource(value = "classpath:default-crypto.yml", factory = GeneralPropertySourceFactory.class)
public class CryptoAutoConfiguration { public class CryptoAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(CryptoAutoConfiguration.class); private static final Logger log = LoggerFactory.getLogger(CryptoAutoConfiguration.class);
@@ -66,6 +78,31 @@ public class CryptoAutoConfiguration {
return new MyBatisDecryptInterceptor(); return new MyBatisDecryptInterceptor();
} }
/**
* 密码编码器配置
*
* @see DelegatingPasswordEncoder
* @see PasswordEncoderFactories
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_CRYPTO + ".password-encoder", name = PropertiesConstants.ENABLED, havingValue = "true")
public PasswordEncoder passwordEncoder() {
PasswordEncoderProperties passwordEncoderProperties = properties.getPasswordEncoder();
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(PasswordEncoderAlgorithm.BCRYPT.name().toLowerCase(), PasswordEncoderUtil
.getEncoder(PasswordEncoderAlgorithm.BCRYPT));
encoders.put(PasswordEncoderAlgorithm.SCRYPT.name().toLowerCase(), PasswordEncoderUtil
.getEncoder(PasswordEncoderAlgorithm.SCRYPT));
encoders.put(PasswordEncoderAlgorithm.PBKDF2.name().toLowerCase(), PasswordEncoderUtil
.getEncoder(PasswordEncoderAlgorithm.PBKDF2));
encoders.put(PasswordEncoderAlgorithm.ARGON2.name().toLowerCase(), PasswordEncoderUtil
.getEncoder(PasswordEncoderAlgorithm.ARGON2));
PasswordEncoderAlgorithm algorithm = passwordEncoderProperties.getAlgorithm();
CheckUtils.throwIf(PasswordEncoderUtil.getEncoder(algorithm) == null, "不支持的加密算法: {}", algorithm);
return new DelegatingPasswordEncoder(algorithm.name().toLowerCase(), encoders);
}
@PostConstruct @PostConstruct
public void postConstruct() { public void postConstruct() {
EncryptHelper.init(properties); EncryptHelper.init(properties);

View File

@@ -14,8 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package top.continew.starter.security.crypto.encryptor; package top.continew.starter.security.crypto.autoconfigure;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm; import top.continew.starter.security.crypto.enums.Algorithm;
import java.util.Objects; import java.util.Objects;
@@ -33,6 +34,14 @@ public class CryptoContext {
*/ */
private Algorithm algorithm; private Algorithm algorithm;
/**
* 加密/解密处理器
* <p>
* 优先级高于加密/解密算法
* </p>
*/
Class<? extends IEncryptor> encryptor;
/** /**
* 对称加密算法密钥 * 对称加密算法密钥
*/ */
@@ -56,6 +65,14 @@ public class CryptoContext {
this.algorithm = algorithm; this.algorithm = algorithm;
} }
public Class<? extends IEncryptor> getEncryptor() {
return encryptor;
}
public void setEncryptor(Class<? extends IEncryptor> encryptor) {
this.encryptor = encryptor;
}
public String getPassword() { public String getPassword() {
return password; return password;
} }
@@ -89,12 +106,13 @@ public class CryptoContext {
return false; return false;
} }
CryptoContext that = (CryptoContext)o; CryptoContext that = (CryptoContext)o;
return algorithm == that.algorithm && Objects.equals(password, that.password) && Objects return algorithm == that.algorithm && Objects.equals(encryptor, that.encryptor) && Objects
.equals(publicKey, that.publicKey) && Objects.equals(privateKey, that.privateKey); .equals(password, that.password) && Objects.equals(publicKey, that.publicKey) && Objects
.equals(privateKey, that.privateKey);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(algorithm, password, publicKey, privateKey); return Objects.hash(algorithm, encryptor, password, publicKey, privateKey);
} }
} }

View File

@@ -17,6 +17,7 @@
package top.continew.starter.security.crypto.autoconfigure; package top.continew.starter.security.crypto.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import top.continew.starter.core.constant.PropertiesConstants; import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.crypto.enums.Algorithm; import top.continew.starter.security.crypto.enums.Algorithm;
@@ -55,6 +56,12 @@ public class CryptoProperties {
*/ */
private String privateKey; private String privateKey;
/**
* 密码编码器配置
*/
@NestedConfigurationProperty
private PasswordEncoderProperties passwordEncoder;
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
} }
@@ -94,4 +101,12 @@ public class CryptoProperties {
public void setPrivateKey(String privateKey) { public void setPrivateKey(String privateKey) {
this.privateKey = privateKey; this.privateKey = privateKey;
} }
public PasswordEncoderProperties getPasswordEncoder() {
return passwordEncoder;
}
public void setPasswordEncoder(PasswordEncoderProperties passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
} }

View File

@@ -14,18 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
package top.continew.starter.security.password.autoconfigure; package top.continew.starter.security.crypto.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties; import top.continew.starter.security.crypto.enums.PasswordEncoderAlgorithm;
import top.continew.starter.core.constant.PropertiesConstants;
/** /**
* 密码编码配置属性 * 密码编码配置属性
* *
* @author Jasmine * @author Jasmine
* @since 1.3.0 * @since 1.3.0
*/ */
@ConfigurationProperties(PropertiesConstants.SECURITY_PASSWORD)
public class PasswordEncoderProperties { public class PasswordEncoderProperties {
/** /**
@@ -34,9 +32,9 @@ public class PasswordEncoderProperties {
private boolean enabled = true; private boolean enabled = true;
/** /**
* 默认启用的编码器 ID默认BCryptPasswordEncoder * 默认启用的编码器算法默认BCrypt 加密算法
*/ */
private String encodingId = "bcrypt"; private PasswordEncoderAlgorithm algorithm = PasswordEncoderAlgorithm.BCRYPT;
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
@@ -46,11 +44,11 @@ public class PasswordEncoderProperties {
this.enabled = enabled; this.enabled = enabled;
} }
public String getEncodingId() { public PasswordEncoderAlgorithm getAlgorithm() {
return encodingId; return algorithm;
} }
public void setEncodingId(String encodingId) { public void setAlgorithm(PasswordEncoderAlgorithm algorithm) {
this.encodingId = encodingId; this.algorithm = algorithm;
} }
} }

View File

@@ -16,6 +16,8 @@
package top.continew.starter.security.crypto.encryptor; package top.continew.starter.security.crypto.encryptor;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/** /**
* 加密器基类 * 加密器基类
* *

View File

@@ -20,6 +20,7 @@ import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm; import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto; import cn.hutool.crypto.symmetric.SymmetricCrypto;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;

View File

@@ -17,6 +17,7 @@
package top.continew.starter.security.crypto.encryptor; package top.continew.starter.security.crypto.encryptor;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm; import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/** /**
* AESAdvanced Encryption Standard 加/解密处理器 * AESAdvanced Encryption Standard 加/解密处理器

View File

@@ -17,6 +17,7 @@
package top.continew.starter.security.crypto.encryptor; package top.continew.starter.security.crypto.encryptor;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm; import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/** /**
* DESData Encryption Standard 加/解密处理器 * DESData Encryption Standard 加/解密处理器

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
import top.continew.starter.security.crypto.autoconfigure.PasswordEncoderProperties;
/**
* 密码编码器加/解密处理器
*
* <p>
* 使用前必须注入 {@link PasswordEncoder},此加密方式不可逆,适合于密码场景
* </p>
*
* @see PasswordEncoder
* @see PasswordEncoderProperties
*
* @author Charles7c
* @since 2.13.3
*/
public class PasswordEncoderEncryptor extends AbstractEncryptor {
private final PasswordEncoder passwordEncoder = SpringUtil.getBean(PasswordEncoder.class);
private final CryptoProperties properties = SpringUtil.getBean(CryptoProperties.class);
public PasswordEncoderEncryptor(CryptoContext context) {
super(context);
}
@Override
public String encrypt(String plaintext) {
// 如果已经是加密格式,直接返回
if (properties.getPasswordEncoder().getAlgorithm().getPattern().matcher(plaintext).matches()) {
return plaintext;
}
return passwordEncoder.encode(plaintext);
}
@Override
public String decrypt(String ciphertext) {
return ciphertext;
}
}

View File

@@ -17,6 +17,7 @@
package top.continew.starter.security.crypto.encryptor; package top.continew.starter.security.crypto.encryptor;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm; import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/** /**
* PBEWithMD5AndDESPassword Based Encryption With MD5 And DES 加/解密处理器 * PBEWithMD5AndDESPassword Based Encryption With MD5 And DES 加/解密处理器

View File

@@ -19,6 +19,7 @@ package top.continew.starter.security.crypto.encryptor;
import cn.hutool.core.codec.Base64; import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType; import cn.hutool.crypto.asymmetric.KeyType;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/** /**
* RSA 加/解密处理器 * RSA 加/解密处理器

View File

@@ -55,7 +55,12 @@ public enum Algorithm {
/** /**
* Base64 * Base64
*/ */
BASE64(Base64Encryptor.class),; BASE64(Base64Encryptor.class),
/**
* 密码编码器支持算法BCrypt、SCRYPT、PBKDF2、ARGON2
*/
PASSWORD_ENCODER(PasswordEncoderEncryptor.class);
/** /**
* 加密/解密处理器 * 加密/解密处理器

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.enums;
import java.util.regex.Pattern;
/**
* 密码编码器加密算法枚举
*
* @author Charles7c
* @since 2.13.3
*/
public enum PasswordEncoderAlgorithm {
/** BCrypt加密算法 */
BCRYPT(Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}")),
/** SCrypt加密算法 */
SCRYPT(Pattern.compile("\\A\\$s0\\$[0-9a-f]+\\$[0-9a-f]+\\$[0-9a-f]+")),
/** PBKDF2加密算法 */
PBKDF2(Pattern.compile("\\A\\$pbkdf2-sha256\\$\\d+\\$[0-9a-f]+\\$[0-9a-f]+")),
/** Argon2加密算法 */
ARGON2(Pattern.compile("\\A\\$argon2(id|i|d)\\$v=\\d+\\$m=\\d+,t=\\d+,p=\\d+\\$[0-9a-zA-Z+/]+\\$[0-9a-zA-Z+/]+"));
/** 正则匹配 */
private final Pattern pattern;
PasswordEncoderAlgorithm(Pattern pattern) {
this.pattern = pattern;
}
public Pattern getPattern() {
return pattern;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.exception;
import top.continew.starter.core.exception.BaseException;
import java.io.Serial;
/**
* 密码编码异常
*
* @author Charles7c
* @since 2.13.3
*/
public class PasswordEncodeException extends BaseException {
@Serial
private static final long serialVersionUID = 1L;
public PasswordEncodeException() {
}
public PasswordEncodeException(String message) {
super(message);
}
public PasswordEncodeException(Throwable cause) {
super(cause);
}
public PasswordEncodeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package top.continew.starter.security.crypto.core; package top.continew.starter.security.crypto.mybatis;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package top.continew.starter.security.crypto.core; package top.continew.starter.security.crypto.mybatis;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
@@ -27,7 +27,7 @@ import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.type.SimpleTypeRegistry; import org.apache.ibatis.type.SimpleTypeRegistry;
import top.continew.starter.security.crypto.annotation.FieldEncrypt; import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.utils.EncryptHelper; import top.continew.starter.security.crypto.util.EncryptHelper;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.sql.Statement; import java.sql.Statement;

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package top.continew.starter.security.crypto.core; package top.continew.starter.security.crypto.mybatis;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ClassUtil;
@@ -30,7 +30,7 @@ import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.RowBounds;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.security.crypto.annotation.FieldEncrypt; import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.utils.EncryptHelper; import top.continew.starter.security.crypto.util.EncryptHelper;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Arrays; import java.util.Arrays;

View File

@@ -14,15 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
package top.continew.starter.security.crypto.utils; package top.continew.starter.security.crypto.util;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import top.continew.starter.security.crypto.annotation.FieldEncrypt; import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties; import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
import top.continew.starter.security.crypto.encryptor.CryptoContext;
import top.continew.starter.security.crypto.encryptor.IEncryptor; import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm; import top.continew.starter.security.crypto.enums.Algorithm;
@@ -68,13 +68,14 @@ public class EncryptHelper {
* 计算 CryptoContext 对象的hashCode作为缓存中的key通过hashCode查询缓存中存在则直接返回不存在则创建并缓存 * 计算 CryptoContext 对象的hashCode作为缓存中的key通过hashCode查询缓存中存在则直接返回不存在则创建并缓存
* </p> * </p>
* *
* @param encryptContext 加密执行者需要的相关配置参数 * @param cryptoContext 加密执行者需要的相关配置参数
* @return 加密执行者 * @return 加密执行者
*/ */
public static IEncryptor registerAndGetEncryptor(CryptoContext encryptContext) { public static IEncryptor registerAndGetEncryptor(CryptoContext cryptoContext) {
int key = encryptContext.hashCode(); int key = cryptoContext.hashCode();
return ENCRYPTOR_CACHE.computeIfAbsent(key, k -> ReflectUtil.newInstance(encryptContext.getAlgorithm() return ENCRYPTOR_CACHE.computeIfAbsent(key, k -> cryptoContext.getEncryptor().equals(IEncryptor.class)
.getEncryptor(), encryptContext)); ? ReflectUtil.newInstance(cryptoContext.getAlgorithm().getEncryptor(), cryptoContext)
: ReflectUtil.newInstance(cryptoContext.getEncryptor(), cryptoContext));
} }
/** /**
@@ -103,8 +104,8 @@ public class EncryptHelper {
} }
String ciphertext = value; String ciphertext = value;
try { try {
CryptoContext encryptContext = buildEncryptContext(fieldEncrypt); CryptoContext cryptoContext = buildCryptoContext(fieldEncrypt);
IEncryptor encryptor = registerAndGetEncryptor(encryptContext); IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
ciphertext = encryptor.encrypt(ciphertext); ciphertext = encryptor.encrypt(ciphertext);
} catch (Exception e) { } catch (Exception e) {
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e); log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
@@ -124,8 +125,8 @@ public class EncryptHelper {
} }
String ciphertext = value; String ciphertext = value;
try { try {
CryptoContext encryptContext = buildEncryptContext(); CryptoContext cryptoContext = buildCryptoContext();
IEncryptor encryptor = registerAndGetEncryptor(encryptContext); IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
ciphertext = encryptor.encrypt(ciphertext); ciphertext = encryptor.encrypt(ciphertext);
} catch (Exception e) { } catch (Exception e) {
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e); log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
@@ -146,8 +147,8 @@ public class EncryptHelper {
} }
String plaintext = value; String plaintext = value;
try { try {
CryptoContext encryptContext = buildEncryptContext(fieldEncrypt); CryptoContext cryptoContext = buildCryptoContext(fieldEncrypt);
IEncryptor encryptor = registerAndGetEncryptor(encryptContext); IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
plaintext = encryptor.decrypt(plaintext); plaintext = encryptor.decrypt(plaintext);
} catch (Exception e) { } catch (Exception e) {
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e); log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
@@ -167,8 +168,8 @@ public class EncryptHelper {
} }
String plaintext = value; String plaintext = value;
try { try {
CryptoContext encryptContext = buildEncryptContext(); CryptoContext cryptoContext = buildCryptoContext();
IEncryptor encryptor = registerAndGetEncryptor(encryptContext); IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
plaintext = encryptor.decrypt(plaintext); plaintext = encryptor.decrypt(plaintext);
} catch (Exception e) { } catch (Exception e) {
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e); log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
@@ -182,21 +183,24 @@ public class EncryptHelper {
* @param fieldEncrypt 字段加密注解 * @param fieldEncrypt 字段加密注解
* @return 加密上下文 * @return 加密上下文
*/ */
private static CryptoContext buildEncryptContext(FieldEncrypt fieldEncrypt) { private static CryptoContext buildCryptoContext(FieldEncrypt fieldEncrypt) {
CryptoContext encryptContext = new CryptoContext(); CryptoContext cryptoContext = new CryptoContext();
encryptContext.setAlgorithm(fieldEncrypt.value() == Algorithm.DEFAULT cryptoContext.setAlgorithm(fieldEncrypt.value() == Algorithm.DEFAULT
? defaultProperties.getAlgorithm() ? defaultProperties.getAlgorithm()
: fieldEncrypt.value()); : fieldEncrypt.value());
encryptContext.setPassword(fieldEncrypt.password().isEmpty() cryptoContext.setEncryptor(fieldEncrypt.encryptor().equals(IEncryptor.class)
? IEncryptor.class
: fieldEncrypt.encryptor());
cryptoContext.setPassword(fieldEncrypt.password().isEmpty()
? defaultProperties.getPassword() ? defaultProperties.getPassword()
: fieldEncrypt.password()); : fieldEncrypt.password());
encryptContext.setPrivateKey(fieldEncrypt.privateKey().isEmpty() cryptoContext.setPrivateKey(fieldEncrypt.privateKey().isEmpty()
? defaultProperties.getPrivateKey() ? defaultProperties.getPrivateKey()
: fieldEncrypt.privateKey()); : fieldEncrypt.privateKey());
encryptContext.setPublicKey(fieldEncrypt.publicKey().isEmpty() cryptoContext.setPublicKey(fieldEncrypt.publicKey().isEmpty()
? defaultProperties.getPublicKey() ? defaultProperties.getPublicKey()
: fieldEncrypt.publicKey()); : fieldEncrypt.publicKey());
return encryptContext; return cryptoContext;
} }
/** /**
@@ -204,12 +208,13 @@ public class EncryptHelper {
* *
* @return 加密上下文 * @return 加密上下文
*/ */
private static CryptoContext buildEncryptContext() { private static CryptoContext buildCryptoContext() {
CryptoContext encryptContext = new CryptoContext(); CryptoContext cryptoContext = new CryptoContext();
encryptContext.setAlgorithm(defaultProperties.getAlgorithm()); cryptoContext.setAlgorithm(defaultProperties.getAlgorithm());
encryptContext.setPassword(defaultProperties.getPassword()); cryptoContext.setEncryptor(IEncryptor.class);
encryptContext.setPrivateKey(defaultProperties.getPrivateKey()); cryptoContext.setPassword(defaultProperties.getPassword());
encryptContext.setPublicKey(defaultProperties.getPublicKey()); cryptoContext.setPrivateKey(defaultProperties.getPrivateKey());
return encryptContext; cryptoContext.setPublicKey(defaultProperties.getPublicKey());
return cryptoContext;
} }
} }

View File

@@ -0,0 +1,127 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.util;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import top.continew.starter.security.crypto.enums.PasswordEncoderAlgorithm;
import top.continew.starter.security.crypto.exception.PasswordEncodeException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 密码加密工具类
* <p>
* 支持多种加密算法可通过编码ID动态选择加密方式
* </p>
*
* @author Charles7c
* @since 2.13.3
*/
public final class PasswordEncoderUtil {
private static final Map<PasswordEncoderAlgorithm, PasswordEncoder> ENCODER_CACHE = new ConcurrentHashMap<>();
static {
// 初始化默认的加密算法实例
ENCODER_CACHE.put(PasswordEncoderAlgorithm.BCRYPT, new BCryptPasswordEncoder());
ENCODER_CACHE.put(PasswordEncoderAlgorithm.SCRYPT, SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
ENCODER_CACHE.put(PasswordEncoderAlgorithm.PBKDF2, Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
ENCODER_CACHE.put(PasswordEncoderAlgorithm.ARGON2, Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
}
private PasswordEncoderUtil() {
}
/**
* 使用指定的加密算法加密密码
*
* @param algorithm 加密算法
* @param rawPassword 原始密码
* @return 加密后的密码
* @throws IllegalArgumentException 如果不支持指定的加密算法
*/
public static String encode(PasswordEncoderAlgorithm algorithm, String rawPassword) {
// 参数校验
if (algorithm == null) {
throw new IllegalArgumentException("加密算法不能为空");
}
if (rawPassword == null) {
throw new IllegalArgumentException("原始密码不能为空");
}
// 获取对应的密码编码器
PasswordEncoder encoder = ENCODER_CACHE.get(algorithm);
if (encoder == null) {
throw new IllegalArgumentException("不支持的加密算法: " + algorithm);
}
try {
return encoder.encode(rawPassword);
} catch (Exception e) {
throw new PasswordEncodeException("密码加密失败: " + e.getMessage(), e);
}
}
/**
* 验证密码是否匹配
*
* @param algorithm 加密算法
* @param rawPassword 原始密码
* @param encodedPassword 加密后的密码
* @return 是否匹配
* @throws IllegalArgumentException 如果不支持指定的加密算法
*/
public static boolean matches(PasswordEncoderAlgorithm algorithm, String rawPassword, String encodedPassword) {
// 参数校验
if (algorithm == null) {
throw new IllegalArgumentException("加密算法不能为空");
}
if (rawPassword == null || encodedPassword == null) {
return false;
}
// 获取对应的密码编码器
PasswordEncoder encoder = ENCODER_CACHE.get(algorithm);
if (encoder == null) {
throw new IllegalArgumentException("不支持的加密算法: " + algorithm);
}
try {
return encoder.matches(rawPassword, encodedPassword);
} catch (Exception e) {
return false;
}
}
/**
* 获取指定算法的密码编码器
*
* @param algorithm 加密算法
* @return 密码编码器实例不存在则返回null
*/
public static PasswordEncoder getEncoder(PasswordEncoderAlgorithm algorithm) {
if (algorithm == null) {
return null;
}
return ENCODER_CACHE.get(algorithm);
}
}

View File

@@ -0,0 +1,6 @@
--- ### 安全配置:字段加/解密配置
continew-starter.security:
crypto:
enabled: true
# 默认算法,即 @FieldEncrypt 默认采用的算法默认AES 对称加密算法)
algorithm: AES

View File

@@ -1,25 +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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew.starter</groupId>
<artifactId>continew-starter-security</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-security-password</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 安全模块 - 密码编码器</description>
<dependencies>
<!-- Spring Security 附带的一个密码加密库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,98 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.password.autoconfigure;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.util.validation.CheckUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 密码编码器自动配置
*
* <p>
* 密码配置类,默认编解码器使用的是 BCryptPasswordEncoder <br />
* 编码后的密码是遵循一定规则的 {idForEncode}encodePassword前缀 {} 包含了编码的方式再拼接上该方式编码后的密码串。<br />
* 可以添加自定义的编解码,也可以修改默认的编解码器,只需修改默认的 encodingId。<br />
* 优点:如果有一天我们对密码编码规则进行替换或者轮转,现有的用户不会受到影响,只要修改 DelegatingPasswordEncoder 的 idForEncode 即可。
* </p>
*
* @author Jasmine
* @author Charles7c
* @since 1.3.0
*/
@AutoConfiguration
@EnableConfigurationProperties(PasswordEncoderProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_PASSWORD, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
public class PasswordEncoderAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(PasswordEncoderAutoConfiguration.class);
private final PasswordEncoderProperties properties;
public PasswordEncoderAutoConfiguration(PasswordEncoderProperties properties) {
this.properties = properties;
}
/**
* 密码编码器
*
* @see DelegatingPasswordEncoder
* @see PasswordEncoderFactories
*/
@Bean
public PasswordEncoder passwordEncoder(List<PasswordEncoder> passwordEncoderList) {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
// 添加自定义的密码编解码器
if (CollUtil.isNotEmpty(passwordEncoderList)) {
passwordEncoderList.forEach(passwordEncoder -> {
String simpleName = passwordEncoder.getClass().getSimpleName();
encoders.put(CharSequenceUtil.removeSuffix(simpleName, "PasswordEncoder")
.toLowerCase(), passwordEncoder);
});
}
String encodingId = properties.getEncodingId();
CheckUtils.throwIf(!encoders.containsKey(encodingId), "{} is not found in idToPasswordEncoder.", encodingId);
return new DelegatingPasswordEncoder(encodingId, encoders);
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Security-PasswordEncoder' completed initialization.");
}
}

View File

@@ -1 +0,0 @@
top.continew.starter.security.password.autoconfigure.PasswordEncoderAutoConfiguration

View File

@@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.core.constant.PropertiesConstants; import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.xss.filter.XssFilter; import top.continew.starter.security.xss.filter.XssFilter;
@@ -43,12 +44,13 @@ public class XssAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(XssAutoConfiguration.class); private static final Logger log = LoggerFactory.getLogger(XssAutoConfiguration.class);
/** /**
* XSS 过滤器配置 * XSS 过滤器
*/ */
@Bean @Bean
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties xssProperties) { public FilterRegistrationBean<XssFilter> xssFilter(XssProperties xssProperties) {
FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>(); FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new XssFilter(xssProperties)); registrationBean.setFilter(new XssFilter(xssProperties));
registrationBean.setOrder(OrderedConstants.Filter.XSS_FILTER);
return registrationBean; return registrationBean;
} }

View File

@@ -16,11 +16,10 @@
<description>ContiNew Starter 安全模块</description> <description>ContiNew Starter 安全模块</description>
<modules> <modules>
<module>continew-starter-security-password</module>
<module>continew-starter-security-mask</module>
<module>continew-starter-security-crypto</module> <module>continew-starter-security-crypto</module>
<module>continew-starter-security-sensitivewords</module> <module>continew-starter-security-mask</module>
<module>continew-starter-security-xss</module> <module>continew-starter-security-xss</module>
<module>continew-starter-security-sensitivewords</module>
</modules> </modules>
<dependencies> <dependencies>

View File

@@ -30,7 +30,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered; import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.core.constant.PropertiesConstants; import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.trace.filter.TLogServletFilter; import top.continew.starter.trace.filter.TLogServletFilter;
import top.continew.starter.trace.handler.TraceIdGenerator; import top.continew.starter.trace.handler.TraceIdGenerator;
@@ -70,14 +70,14 @@ public class TraceAutoConfiguration {
} }
/** /**
* TLog 过滤器配置 * TLog 过滤器
*/ */
@Bean @Bean
public FilterRegistrationBean<TLogServletFilter> tLogServletFilter() { public FilterRegistrationBean<TLogServletFilter> tLogServletFilter() {
FilterRegistrationBean<TLogServletFilter> registration = new FilterRegistrationBean<>(); FilterRegistrationBean<TLogServletFilter> registrationBean = new FilterRegistrationBean<>();
registration.setFilter(new TLogServletFilter(traceProperties)); registrationBean.setFilter(new TLogServletFilter(traceProperties));
registration.setOrder(Ordered.HIGHEST_PRECEDENCE); registrationBean.setOrder(OrderedConstants.Filter.TRACE_FILTER);
return registration; return registrationBean;
} }
/** /**

View File

@@ -54,7 +54,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GlobalResponseProperties.class) @EnableConfigurationProperties(GlobalResponseProperties.class)
@PropertySource(value = "classpath:default-web.yml", factory = GeneralPropertySourceFactory.class) @PropertySource(value = "classpath:default-response.yml", factory = GeneralPropertySourceFactory.class)
public class GlobalResponseAutoConfiguration { public class GlobalResponseAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(GlobalResponseAutoConfiguration.class); private static final Logger log = LoggerFactory.getLogger(GlobalResponseAutoConfiguration.class);

View File

@@ -30,8 +30,10 @@ import org.springframework.context.annotation.Bean;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.server.handlers.DisallowedMethodsHandler; import io.undertow.server.handlers.DisallowedMethodsHandler;
import io.undertow.util.HttpString; import io.undertow.util.HttpString;
import org.springframework.context.annotation.PropertySource;
import top.continew.starter.core.constant.PropertiesConstants; import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.util.CollUtils; import top.continew.starter.core.util.CollUtils;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
/** /**
* Undertow 自动配置 * Undertow 自动配置
@@ -44,6 +46,7 @@ import top.continew.starter.core.util.CollUtils;
@ConditionalOnWebApplication @ConditionalOnWebApplication
@ConditionalOnClass(Undertow.class) @ConditionalOnClass(Undertow.class)
@EnableConfigurationProperties(ServerExtensionProperties.class) @EnableConfigurationProperties(ServerExtensionProperties.class)
@PropertySource(value = "classpath:default-server.yml", factory = GeneralPropertySourceFactory.class)
@ConditionalOnProperty(prefix = "server.extension", name = PropertiesConstants.ENABLED, havingValue = "true") @ConditionalOnProperty(prefix = "server.extension", name = PropertiesConstants.ENABLED, havingValue = "true")
public class UndertowAutoConfiguration { public class UndertowAutoConfiguration {

View File

@@ -23,20 +23,3 @@ continew-starter.web.response:
- io.swagger.** - io.swagger.**
- org.springdoc.** - org.springdoc.**
- org.springframework.boot.actuate.* - org.springframework.boot.actuate.*
--- ### 服务器配置
server:
## Undertow 服务器配置
undertow:
# HTTP POST 请求内容的大小上限(默认 -1不限制
max-http-post-size: -1
# 以下的配置会影响 buffer这些 buffer 会用于服务器连接的 IO 操作,有点类似 Netty 的池化内存管理
# 每块 buffer的空间大小越小的空间被利用越充分不要设置太大以免影响其他应用合适即可
buffer-size: 512
# 是否分配的直接内存NIO 直接分配的堆外内存)
direct-buffers: true
threads:
# 设置 IO 线程数,它主要执行非阻塞的任务,它们会负责多个连接(默认每个 CPU 核心一个线程)
io: 8
# 阻塞任务线程池,当执行类似 Servlet 请求阻塞操作Undertow 会从这个线程池中取得线程(它的值设置取决于系统的负载)
worker: 256

View File

@@ -0,0 +1,16 @@
--- ### 服务器配置
server:
## Undertow 服务器配置
undertow:
# HTTP POST 请求内容的大小上限(默认 -1不限制
max-http-post-size: -1
# 以下的配置会影响 buffer这些 buffer 会用于服务器连接的 IO 操作,有点类似 Netty 的池化内存管理
# 每块 buffer的空间大小越小的空间被利用越充分不要设置太大以免影响其他应用合适即可
buffer-size: 512
# 是否分配的直接内存NIO 直接分配的堆外内存)
direct-buffers: true
threads:
# 设置 IO 线程数,它主要执行非阻塞的任务,它们会负责多个连接(默认每个 CPU 核心一个线程)
io: 8
# 阻塞任务线程池,当执行类似 Servlet 请求阻塞操作Undertow 会从这个线程池中取得线程(它的值设置取决于系统的负载)
worker: 256