Compare commits

..

30 Commits

Author SHA1 Message Date
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
b7646cd87b release: v2.13.2 2025-07-21 21:26:54 +08:00
601c071505 build(dependencies): spel-validator 0.5.0-beta => 0.5.1-beta 2025-07-21 21:11:44 +08:00
47165f80a1 chore: 解决部分 sonar 问题 2025-07-21 20:55:48 +08:00
43d1489f1a refactor(json/jackson): 重构 JSON 工具类
- 修改了 JSONUtils 类的多个方法名称,使其更加清晰
- 添加了新的方法来处理 JSON 数组转换
- 优化了异常处理,引入了专门的 JSONException 类
- 调整了部分方法的参数和返回类型,提高灵活性
2025-07-21 20:25:01 +08:00
书中自有颜如玉
5d10a28aa1 refactor(security/crypto):重构加/解密模块业务逻辑,封装 EncryptHelper 工具类,提供统一的加/解密方法,方便使用者灵活处理加/解密 2025-07-21 10:14:25 +00:00
c8e5191dc0 chore(dependencies): 优化注释 2025-07-20 18:49:54 +08:00
3011e64375 Merge remote-tracking branch 'origin/dev' into dev 2025-07-20 18:45:59 +08:00
5dd6808bea refactor(extension/datapermission): 优化数据权限模块代码
- 新增 DataPermissionConstants 和 DataPermissionException 类
- 重构 DataPermissionUserDataProvider 接口位置
- 优化 RoleData 和 UserData 类,使用 Long 类型替代 String 类型
- 重构 DefaultDataPermissionHandler 类,改进数据权限处理逻辑
2025-07-20 15:29:40 +08:00
43ba770971 fix(tenant): 将 TenantUtils.executeIgnore 方法改为静态方法 2025-07-20 14:11:30 +08:00
lishuyan
586322a180 refactor(crud):♻️ 重构树形结构构建逻辑,支持了使用查询条件,缺失根节点时,树节点不丢失。
- 移除了冗余的代码和不必要的注释
- 使用三元运算符简化了 treeNodeConfig 的赋值
- 新增 createMethodReference 方法,通过反射创建方法引用
- 使用 TreeBuildUtils.buildMultiRoot 方法替代 TreeUtil.build,支持多根节点树的构建
2025-07-20 14:11:05 +08:00
lishuyan
90c11f60f9 feat(core): 新增 扩展 hutool TreeUtil 封装树构建的 TreeBuildUtils 工具类,其中包括扩展的(构建树形结构、构建多根节点的树结构(支持多个顶级节点))等方法。 2025-07-20 12:57:09 +08:00
12746d6261 fix(validation): 修复字符串值仅进行了 null 判空错误 2025-07-20 08:47:53 +08:00
ddd4e38dca refactor: 使用 CharSequenceUtil 替换部分 StrUtil 使用以解决 Sonar 问题 2025-07-20 08:45:24 +08:00
35e79620e4 refactor(tenant): 优化租户忽略逻辑 2025-07-20 08:37:09 +08:00
d8c4224030 refactor(extension/tenant): 设置租户拦截器的优先级为最高 2025-07-20 00:01:36 +08:00
a778e3182a refactor(extension/tenant): 移除超级租户 ID 配置属性
- 删除了 DefaultTenantLineHandler 中关于超级租户的判断逻辑
- 移除了 TenantProperties 中的 superTenantId 相关配置项
2025-07-20 00:01:04 +08:00
e4d0c98838 build: 更新项目版本号至2.13.2-SNAPSHOT 2025-07-20 00:00:31 +08:00
76 changed files with 1825 additions and 506 deletions

View File

@@ -1,3 +1,54 @@
## [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)
### ✨ 新特性
- 【core】新增 扩展 hutool TreeUtil 封装树构建的 TreeBuildUtils 工具类,其中包括扩展的(构建树形结构、构建多根节点的树结构(支持多个顶级节点))等方法。(Gitee#72@lishuyanla) ([90c11f6](https://github.com/continew-org/continew-starter/commit/90c11f60f9ba313acbfd76de66f3b4022bc8b270))
- 【security/crypto】重构加/解密模块业务逻辑,封装 EncryptHelper 工具类,提供统一的加/解密方法,方便使用者灵活处理加/解密 (Gitee#73@lishuyanla) ([5d10a28](https://github.com/continew-org/continew-starter/commit/5d10a28aa1c4ade0a51235e302c46143b90f7bb5))
### 💎 功能优化
- 【extension/tenant】移除超级租户 ID 配置属性 ([a778e31](https://github.com/continew-org/continew-starter/commit/a778e3182a8163e9e3ea5bbc677090da2efe0a31))
- 【extension/tenant】设置租户拦截器的优先级为最高 ([d8c4224](https://github.com/continew-org/continew-starter/commit/d8c4224030d6d2eb6eea3554e689165315924bf6))
- 【extension/tenant】优化租户忽略逻辑 ([35e7962](https://github.com/continew-org/continew-starter/commit/35e79620e40d8d4f121a24ec720dcd8968ce9104))
- 【extension/crud】 ([586322a](https://github.com/continew-org/continew-starter/commit/586322a180f2bce9faf1acbacb65ec09df921815))
- 【extension/datapermission】优化数据权限模块代码 ([5dd6808](https://github.com/continew-org/continew-starter/commit/5dd6808bea4483a7e69884b69bac4928cb95bd89))
- 【json/jackson】重构 JSON 工具类 ([43d1489](https://github.com/continew-org/continew-starter/commit/43d1489f1a850731b4fc27a2ae0cbab24a72025c))
- 解决部分 sonar 问题 ([ddd4e38](https://github.com/continew-org/continew-starter/commit/ddd4e38dca4c5f64b9fc999d57a13d827d29d474)) ([47165f8](https://github.com/continew-org/continew-starter/commit/47165f80a15cf7da346fbbb931894284b0cd7124))
### 🐛 问题修复
- 【validation】修复字符串值仅进行了 null 判空错误 ([12746d6](https://github.com/continew-org/continew-starter/commit/12746d62613f3e9d8cce4b4aea71d6466f345e0a))
- 【extension/tenant】将 TenantUtils.executeIgnore 方法改为静态方法 ([43ba770](https://github.com/continew-org/continew-starter/commit/43ba770971e5fb124272ed6d4fadef36be9c8fb8))
### 📦 依赖升级
- 【dependencies】spel-validator 0.5.0-beta => 0.5.1-beta ([601c071](https://github.com/continew-org/continew-starter/commit/601c0715052106f4cae3419fda0f276231cb3b13))
## [v2.13.1](https://github.com/continew-org/continew-starter/compare/v2.13.0...v2.13.1) (2025-07-17)
### ✨ 新特性

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.WebMvcConfigurer;
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.StringConstants;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
@@ -61,7 +62,9 @@ public class SaTokenAutoConfiguration implements WebMvcConfigurer {
@Override
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>
<properties>
<revision>2.13.1</revision>
<revision>2.13.3</revision>
</properties>
<dependencyManagement>

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

@@ -17,11 +17,17 @@
package top.continew.starter.core.util;
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.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -60,4 +66,27 @@ public class ReflectUtils {
Field[] fields = ReflectUtil.getFields(beanClass);
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

@@ -346,8 +346,8 @@ public class ServletUtils extends JakartaServletUtil {
* @since 2.13.1
* @see #write(HttpServletResponse, String, String)
*/
public static void writeJSON(HttpServletResponse response, Object data) {
write(response, JSONUtil.toJsonStr(data), MediaType.APPLICATION_JSON_VALUE);
public static void writeJSON(HttpServletResponse response, String data) {
write(response, data, MediaType.APPLICATION_JSON_VALUE);
}
/**

View File

@@ -0,0 +1,188 @@
/*
* 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.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.lang.tree.parser.NodeParser;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 树工具类
*
* <p>
* 扩展 Hutool TreeUtil 封装树构建
* </p>
*
* @author Lion Li<a href="https://gitee.com/dromara/RuoYi-Vue-Plus">RuoYi-Vue-Plus</a>
* @author lishuyan
*/
public class TreeUtils extends TreeUtil {
private TreeUtils() {
}
/**
* 构建树形结构
*
* @param <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param list 节点列表,其中包含了要构建树形结构的所有节点
* @param nodeParser 解析器,用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
}
K k = ReflectUtil.invoke(list.get(0), CharSequenceUtil.genGetter("parentId"));
return TreeUtil.build(list, k, TreeNodeConfig.DEFAULT_CONFIG, nodeParser);
}
/**
* 构建树形结构
*
* @param <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param parentId 顶级节点
* @param list 节点列表,其中包含了要构建树形结构的所有节点
* @param nodeParser 解析器,用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list, K parentId, NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
}
return TreeUtil.build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser);
}
/**
* 构建树形结构
*
* @param <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param list 节点列表,其中包含了要构建树形结构的所有节点
* @param parentId 顶级节点
* @param treeNodeConfig 树节点配置
* @param nodeParser 解析器,用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list,
K parentId,
TreeNodeConfig treeNodeConfig,
NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
}
return TreeUtil.build(list, parentId, treeNodeConfig, nodeParser);
}
/**
* 构建多根节点的树结构(支持多个顶级节点)
*
* @param list 原始数据列表
* @param getId 获取节点 ID 的方法引用例如node -> node.getId()
* @param getParentId 获取节点父级 ID 的方法引用例如node -> node.getParentId()
* @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点
* @param <T> 原始数据类型如实体类、DTO 等)
* @param <K> 节点 ID 类型(如 Long、String
* @return 构建完成的树形结构(可能包含多个顶级根节点)
*/
public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list,
Function<T, K> getId,
Function<T, K> getParentId,
NodeParser<T, K> parser) {
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
}
Set<K> rootParentIds = CollUtils.mapToSet(list, getParentId);
rootParentIds.removeAll(CollUtils.mapToSet(list, getId));
// 构建每一个根 parentId 下的树,并合并成最终结果列表
return rootParentIds.stream()
.flatMap(rootParentId -> TreeUtil.build(list, rootParentId, parser).stream())
.collect(Collectors.toList());
}
/**
* 构建多根节点的树结构(支持多个顶级节点)
*
* @param <T> 原始数据类型如实体类、DTO 等)
* @param <K> 节点 ID 类型(如 Long、String
* @param list 原始数据列表
* @param getId 获取节点 ID 的方法引用例如node -> node.getId()
* @param getParentId 获取节点父级 ID 的方法引用例如node -> node.getParentId()
* @param treeNodeConfig 树节点配置
* @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点
* @return 构建完成的树形结构(可能包含多个顶级根节点)
*/
public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list,
Function<T, K> getId,
Function<T, K> getParentId,
TreeNodeConfig treeNodeConfig,
NodeParser<T, K> parser) {
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
}
Set<K> rootParentIds = CollUtils.mapToSet(list, getParentId);
rootParentIds.removeAll(CollUtils.mapToSet(list, getId));
// 构建每一个根 parentId 下的树,并合并成最终结果列表
return rootParentIds.stream()
.flatMap(rootParentId -> TreeUtil.build(list, rootParentId, treeNodeConfig, parser).stream())
.collect(Collectors.toList());
}
/**
* 获取节点列表中所有节点的叶子节点
*
* @param <K> 节点ID的类型
* @param nodes 节点列表
* @return 包含所有叶子节点的列表
*/
public static <K> List<Tree<K>> getLeafNodes(List<Tree<K>> nodes) {
if (CollUtil.isEmpty(nodes)) {
return new ArrayList<>(0);
}
return nodes.stream().flatMap(TreeUtils::extractLeafNodes).collect(Collectors.toList());
}
/**
* 获取指定节点下的所有叶子节点
*
* @param <K> 节点ID的类型
* @param node 要查找叶子节点的根节点
* @return 包含所有叶子节点的列表
*/
private static <K> Stream<Tree<K>> extractLeafNodes(Tree<K> node) {
if (!node.hasChild()) {
return Stream.of(node);
} else {
// 递归调用,获取所有子节点的叶子节点
return node.getChildren().stream().flatMap(TreeUtils::extractLeafNodes);
}
}
}

View File

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

View File

@@ -14,23 +14,23 @@
* limitations under the License.
*/
package top.continew.starter.security.password.constant;
import java.util.regex.Pattern;
package top.continew.starter.data.enums;
/**
* 密码编码器相关常量
* 逻辑关系枚举
*
* @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.QueryIgnore;
import top.continew.starter.data.enums.QueryType;
import top.continew.starter.data.enums.LogicalRelation;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -174,8 +175,21 @@ public class QueryWrapperHelper {
return consumers;
}
// 解析多列查询
LogicalRelation logicalRelation = queryAnnotation.logicalRelation();
List<Consumer<QueryWrapper<R>>> columnConsumers = new ArrayList<>();
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;
} catch (BadRequestException e) {

View File

@@ -13,47 +13,77 @@
<description>ContiNew Starter 依赖模块</description>
<properties>
<!-- 项目版本号 -->
<revision>2.13.1</revision>
<!-- Project Version -->
<revision>2.13.3</revision>
<!-- Core Framework Versions -->
<spring-boot.version>3.3.12</spring-boot.version>
<spring-cloud.version>2023.0.5</spring-cloud.version>
<!-- Cache and Storage Versions -->
<redisson.version>3.49.0</redisson.version>
<jetcache.version>2.7.8</jetcache.version>
<cosid.version>2.13.0</cosid.version>
<!-- Security and Authentication Versions -->
<sa-token.version>1.44.0</sa-token.version>
<just-auth.version>1.16.7</just-auth.version>
<!-- Database and ORM Versions -->
<mybatis-plus.version>3.5.12</mybatis-plus.version>
<mybatis-flex.version>1.10.9</mybatis-flex.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<p6spy.version>3.9.1</p6spy.version>
<!-- ID Generator and Job Scheduler Versions -->
<cosid.version>2.13.0</cosid.version>
<snail-job.version>1.5.0</snail-job.version>
<!-- Messaging and Notification Versions -->
<sms4j.version>3.3.5</sms4j.version>
<!-- Captcha Versions -->
<aj-captcha.version>1.4.0</aj-captcha.version>
<easy-captcha.version>1.6.2</easy-captcha.version>
<nashorn.version>15.6</nashorn.version>
<!-- Excel Processing Versions -->
<fastexcel.version>1.2.0</fastexcel.version>
<poi.version>5.4.1</poi.version>
<!-- File Storage Versions -->
<x-file-storage.version>2.2.1</x-file-storage.version>
<aws-s3-v1.version>1.12.783</aws-s3-v1.version>
<aws-sdk.version>2.31.63</aws-sdk.version>
<aws-crt.version>0.38.5</aws-crt.version>
<thumbnails.version>0.4.20</thumbnails.version>
<!-- Validation and Response Processing Versions -->
<graceful-response.version>5.0.5-boot3</graceful-response.version>
<spel-validator.version>0.5.0-beta</spel-validator.version>
<spel-validator.version>0.5.2-beta</spel-validator.version>
<crane4j.version>2.9.0</crane4j.version>
<!-- API Documentation Versions -->
<knife4j.version>4.5.0</knife4j.version>
<!-- Tracing and Logging Versions -->
<tlog.version>1.5.2</tlog.version>
<!-- License and Compression Versions -->
<truelicense.version>1.33</truelicense.version>
<zip4j.version>2.11.5</zip4j.version>
<!-- HTTP Client and Utilities Versions -->
<okhttp.version>4.12.0</okhttp.version>
<ttl.version>2.14.5</ttl.version>
<ip2region.version>3.3.6</ip2region.version>
<hutool.version>5.8.38</hutool.version>
<snakeyaml.version>2.4</snakeyaml.version>
<!-- 解决部分传递依赖漏洞问题 -->
<nashorn.version>15.6</nashorn.version>
<!-- Security Vulnerability Fix Versions -->
<commons-beanutils.version>1.11.0</commons-beanutils.version>
<commons-io.version>2.17.0</commons-io.version>
<commons-compress.version>1.26.0</commons-compress.version>
<!-- Maven Plugin Versions -->
<flatten.version>1.7.0</flatten.version>
<spotless.version>2.44.3</spotless.version>

View File

@@ -21,6 +21,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
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.SortQuery;
import top.continew.starter.extension.crud.model.resp.BasePageResp;
@@ -60,19 +61,30 @@ public interface CrudService<L, D, Q, C> {
List<L> list(@Valid Q query, @Valid SortQuery sortQuery);
/**
* 查询树列表
* <p>
* 虽然提供了查询条件,但不建议使用,容易因缺失根节点导致树节点丢失。
* 建议在前端进行查询过滤,如需使用建议重写方法。
* </p>
* 查询树列表(多个根节点)
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段,简单树(下拉列表)使用全局配置结构,复杂树(表格)使用 @DictField 局部配置)
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段,简单树(下拉列表)使用全局配置结构,复杂树(表格)使用 @TreeField 局部配置)
* @return 树列表信息
* @see TreeField
*/
List<Tree<Long>> tree(@Valid Q query, @Valid SortQuery sortQuery, boolean isSimple);
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段,简单树(下拉列表)使用全局配置结构,复杂树(表格)使用 @TreeField 局部配置)
* @param isSingleRoot 是否为单个根节点
* @return 树列表信息
* @author lishuyan
* @since 2.13.3
* @see TreeField
*/
List<Tree<Long>> tree(@Valid Q query, @Valid SortQuery sortQuery, boolean isSimple, boolean isSingleRoot);
/**
* 查询详情
*
@@ -88,6 +100,7 @@ public interface CrudService<L, D, Q, C> {
* @param sortQuery 排序查询条件
* @return 字典列表信息
* @since 2.1.0
* @see top.continew.starter.extension.crud.annotation.DictModel
*/
List<LabelValueResp> listDict(@Valid Q query, @Valid SortQuery sortQuery);

View File

@@ -32,23 +32,25 @@ import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.TreeUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.data.base.BaseMapper;
import top.continew.starter.data.service.impl.ServiceImpl;
import top.continew.starter.data.util.QueryWrapperHelper;
import top.continew.starter.excel.util.ExcelUtils;
import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.autoconfigure.CrudProperties;
import top.continew.starter.extension.crud.autoconfigure.CrudTreeProperties;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
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.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.excel.util.ExcelUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
/**
* CRUD 业务实现基类
@@ -62,7 +64,7 @@ import java.util.Optional;
* @author Charles7c
* @since 1.0.0
*/
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements CrudService<L, D, Q, C> {
public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements CrudService<L, D, Q, C> {
protected final Class<L> listClass = this.currentListClass();
protected final Class<D> detailClass = this.currentDetailClass();
@@ -88,9 +90,14 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
@Override
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);
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
return CollUtil.newArrayList();
}
CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class);
CrudTreeProperties treeProperties = crudProperties.getTree();
@@ -105,21 +112,19 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
treeNodeConfig = treeProperties.genTreeNodeConfig(treeField);
rootId = treeField.rootId();
}
// 构建树
return TreeUtil.build(list, rootId, treeNodeConfig, (node, tree) -> {
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()))));
}
});
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
.value()));
Function<L, Long> getParentId = ReflectUtils.createMethodReference(listClass, CharSequenceUtil
.genGetter(treeField.parentIdKey()));
// 构建多根节点树
return TreeUtils.buildMultiRoot(list, getId, getParentId, treeNodeConfig, (node,
tree) -> buildTreeField(isSimple, node, tree, treeField));
}
}
@Override
@@ -130,6 +135,11 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
return detail;
}
@Override
public List<LabelValueResp> listDict(Q query, SortQuery sortQuery) {
return List.of();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long create(C req) {
@@ -317,4 +327,26 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
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

@@ -36,9 +36,13 @@ import org.springframework.transaction.annotation.Transactional;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ClassUtils;
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.data.mapper.BaseMapper;
import top.continew.starter.data.service.impl.ServiceImpl;
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.autoconfigure.CrudProperties;
@@ -48,12 +52,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.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.excel.util.ExcelUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Function;
/**
* CRUD 业务实现基类
@@ -67,7 +69,7 @@ import java.util.*;
* @author Charles7c
* @since 1.0.0
*/
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements CrudService<L, D, Q, C> {
public class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements CrudService<L, D, Q, C> {
private Class<L> listClass;
private Class<D> detailClass;
@@ -93,9 +95,14 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
@Override
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);
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
return CollUtil.newArrayList();
}
CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class);
CrudTreeProperties treeProperties = crudProperties.getTree();
@@ -110,21 +117,19 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
treeNodeConfig = treeProperties.genTreeNodeConfig(treeField);
rootId = treeField.rootId();
}
// 构建树
return TreeUtil.build(list, rootId, treeNodeConfig, (node, tree) -> {
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()))));
}
});
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
.value()));
Function<L, Long> getParentId = ReflectUtils.createMethodReference(listClass, CharSequenceUtil
.genGetter(treeField.parentIdKey()));
// 构建多根节点树
return TreeUtils.buildMultiRoot(list, getId, getParentId, treeNodeConfig, (node,
tree) -> buildTreeField(isSimple, node, tree, treeField));
}
}
@Override
@@ -379,4 +384,27 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
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

@@ -0,0 +1,39 @@
/*
* 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.extension.datapermission.constant;
/**
* 数据权限常量
*
* @author Charles7c
* @since 2.13.2
*/
public final class DataPermissionConstants {
/**
* 数据库字段:祖先节点
*/
public static final String ANCESTORS_COLUMN = "ancestors";
/**
* 方法名后缀COUNT
*/
public static final String COUNT_METHOD_SUFFIX = "_COUNT";
private DataPermissionConstants() {
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.extension.datapermission.exception;
import top.continew.starter.core.exception.BaseException;
/**
* 数据权限异常
*
* @author Charles7c
* @since 2.13.2
*/
public class DataPermissionException extends BaseException {
public DataPermissionException(String message) {
super(message);
}
public DataPermissionException(String message, Throwable cause) {
super(message, cause);
}
public static DataPermissionException unsupportedDataScope(String dataScope) {
return new DataPermissionException("Unsupported data scope: " + dataScope);
}
public static DataPermissionException unsupportedDatabase(String database) {
return new DataPermissionException("Unsupported database for data permission: " + database);
}
public static DataPermissionException invalidUserData(String message) {
return new DataPermissionException("Invalid user data: " + message);
}
public static DataPermissionException methodNotFound(String mappedStatementId) {
return new DataPermissionException("Method not found for data permission: " + mappedStatementId);
}
}

View File

@@ -29,7 +29,7 @@ public class RoleData {
/**
* 角色 ID
*/
private String roleId;
private Long roleId;
/**
* 数据权限
@@ -39,16 +39,16 @@ public class RoleData {
public RoleData() {
}
public RoleData(String roleId, DataScope dataScope) {
public RoleData(Long roleId, DataScope dataScope) {
this.roleId = roleId;
this.dataScope = dataScope;
}
public String getRoleId() {
public Long getRoleId() {
return roleId;
}
public void setRoleId(String roleId) {
public void setRoleId(Long roleId) {
this.roleId = roleId;
}

View File

@@ -16,6 +16,7 @@
package top.continew.starter.extension.datapermission.model;
import java.util.Collections;
import java.util.Set;
/**
@@ -29,23 +30,32 @@ public class UserData {
/**
* 用户 ID
*/
private String userId;
private Long userId;
/**
* 角色列表
*/
private Set<RoleData> roles;
private Set<RoleData> roles = Collections.emptySet();
/**
* 部门 ID
*/
private String deptId;
private Long deptId;
public String getUserId() {
public UserData() {
}
public UserData(Long userId, Long deptId, Set<RoleData> roles) {
this.userId = userId;
this.deptId = deptId;
this.roles = roles != null ? roles : Collections.emptySet();
}
public Long getUserId() {
return userId;
}
public void setUserId(String userId) {
public void setUserId(Long userId) {
this.userId = userId;
}
@@ -54,14 +64,23 @@ public class UserData {
}
public void setRoles(Set<RoleData> roles) {
this.roles = roles;
this.roles = roles != null ? roles : Collections.emptySet();
}
public String getDeptId() {
public Long getDeptId() {
return deptId;
}
public void setDeptId(String deptId) {
public void setDeptId(Long deptId) {
this.deptId = deptId;
}
/**
* 检查用户数据是否有效
*
* @return 是否有效
*/
public boolean isValid() {
return userId != null && deptId != null && !roles.isEmpty();
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.extension.datapermission.filter.DataPermissionUserDataProvider;
import top.continew.starter.extension.datapermission.provider.DataPermissionUserDataProvider;
import top.continew.starter.extension.datapermission.handler.DefaultDataPermissionHandler;
/**

View File

@@ -46,8 +46,10 @@ import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.data.enums.DatabaseType;
import top.continew.starter.data.util.MetaUtils;
import top.continew.starter.extension.datapermission.annotation.DataPermission;
import top.continew.starter.extension.datapermission.constant.DataPermissionConstants;
import top.continew.starter.extension.datapermission.enums.DataScope;
import top.continew.starter.extension.datapermission.filter.DataPermissionUserDataProvider;
import top.continew.starter.extension.datapermission.exception.DataPermissionException;
import top.continew.starter.extension.datapermission.provider.DataPermissionUserDataProvider;
import top.continew.starter.extension.datapermission.model.RoleData;
import top.continew.starter.extension.datapermission.model.UserData;
@@ -62,7 +64,6 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
private static final Logger log = LoggerFactory.getLogger(DefaultDataPermissionHandler.class);
private final DataPermissionUserDataProvider dataPermissionUserDataProvider;
private static final DataSource dataSource = SpringUtil.getBean(DataSource.class);
public DefaultDataPermissionHandler(DataPermissionUserDataProvider dataPermissionUserDataProvider) {
this.dataPermissionUserDataProvider = dataPermissionUserDataProvider;
@@ -71,26 +72,47 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
try {
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId
.lastIndexOf(StringConstants.DOT)));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringConstants.DOT) + 1);
Method[] methodArr = clazz.getMethods();
for (Method method : methodArr) {
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
String name = method.getName();
if (dataPermission == null || !CharSequenceUtil.equalsAny(methodName, name, name + "_COUNT")) {
continue;
}
if (dataPermissionUserDataProvider.isFilter()) {
return buildDataScopeFilter(dataPermission, where);
}
DataPermission dataPermission = findDataPermissionAnnotation(mappedStatementId);
if (dataPermission != null && dataPermissionUserDataProvider.isFilter()) {
return buildDataScopeFilter(dataPermission, where);
}
} catch (ClassNotFoundException e) {
} catch (Exception e) {
log.error("Data permission handler build data scope filter occurred an error: {}.", e.getMessage(), e);
}
return where;
}
/**
* 查找数据权限注解
*
* @param mappedStatementId Mapper 方法 ID
* @return 数据权限注解
*/
private DataPermission findDataPermissionAnnotation(String mappedStatementId) {
try {
int lastDotIndex = mappedStatementId.lastIndexOf(StringConstants.DOT);
if (lastDotIndex == -1) {
return null;
}
String className = mappedStatementId.substring(0, lastDotIndex);
String methodName = mappedStatementId.substring(lastDotIndex + 1);
Class<?> clazz = Class.forName(className);
Method[] methods = clazz.getMethods();
for (Method method : methods) {
String name = method.getName();
if (CharSequenceUtil.equalsAny(methodName, name, name + DataPermissionConstants.COUNT_METHOD_SUFFIX)) {
return method.getAnnotation(DataPermission.class);
}
}
} catch (ClassNotFoundException e) {
throw DataPermissionException.methodNotFound(mappedStatementId);
}
return null;
}
/**
* 构建数据范围过滤条件
*
@@ -99,23 +121,29 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
* @return 构建后查询条件
*/
private Expression buildDataScopeFilter(DataPermission dataPermission, Expression where) {
Expression expression = null;
UserData userData = dataPermissionUserDataProvider.getUserData();
if (userData == null || !userData.isValid()) {
throw DataPermissionException.invalidUserData("User data is null or invalid");
}
Expression expression = null;
Set<RoleData> roles = userData.getRoles();
for (RoleData roleData : roles) {
DataScope dataScope = roleData.getDataScope();
if (DataScope.ALL.equals(dataScope)) {
return where;
}
switch (dataScope) {
case DEPT_AND_CHILD -> expression = this
.buildDeptAndChildExpression(dataPermission, userData, expression);
case DEPT -> expression = this.buildDeptExpression(dataPermission, userData, expression);
case SELF -> expression = this.buildSelfExpression(dataPermission, userData, expression);
case CUSTOM -> expression = this.buildCustomExpression(dataPermission, roleData, expression);
default -> throw new IllegalArgumentException("暂不支持 [%s] 数据权限".formatted(dataScope));
}
expression = switch (dataScope) {
case DEPT_AND_CHILD -> buildDeptAndChildExpression(dataPermission, userData, expression);
case DEPT -> buildDeptExpression(dataPermission, userData, expression);
case SELF -> buildSelfExpression(dataPermission, userData, expression);
case CUSTOM -> buildCustomExpression(dataPermission, roleData, expression);
default -> throw DataPermissionException.unsupportedDataScope(dataScope.toString());
};
}
return where != null ? new AndExpression(where, new ParenthesedExpressionList<>(expression)) : expression;
}
@@ -144,18 +172,19 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
equalsTo.setLeftExpression(new Column(dataPermission.id()));
equalsTo.setRightExpression(new LongValue(userData.getDeptId()));
DatabaseType databaseType = MetaUtils.getDatabaseType(dataSource);
DatabaseType databaseType = MetaUtils.getDatabaseType(SpringUtil.getBean(DataSource.class));
Expression inSetExpression;
if (DatabaseType.MYSQL.getDatabase().equalsIgnoreCase(databaseType.getDatabase())) {
Function findInSetFunction = new Function();
findInSetFunction.setName("find_in_set");
findInSetFunction.setParameters(new ExpressionList(new LongValue(userData
.getDeptId()), new Column("ancestors")));
.getDeptId()), new Column(DataPermissionConstants.ANCESTORS_COLUMN)));
inSetExpression = findInSetFunction;
} else if (DatabaseType.POSTGRE_SQL.getDatabase().equalsIgnoreCase(databaseType.getDatabase())) {
// 构建 concat 函数
Function concatFunction = new Function("concat");
concatFunction.setParameters(new ExpressionList<>(new Column("ancestors"), new StringValue(",")));
concatFunction
.setParameters(new ExpressionList<>(new Column(DataPermissionConstants.ANCESTORS_COLUMN), new StringValue(",")));
// 创建 LIKE 函数
LikeExpression likeExpression = new LikeExpression();
@@ -163,7 +192,7 @@ public class DefaultDataPermissionHandler implements DataPermissionHandler {
likeExpression.setRightExpression(new StringValue("%," + userData.getDeptId() + ",%"));
inSetExpression = likeExpression;
} else {
throw new IllegalArgumentException("暂不支持 [%s] 数据权限".formatted(""));
throw DataPermissionException.unsupportedDatabase(databaseType.getDatabase());
}
select.setWhere(new OrExpression(equalsTo, inSetExpression));

View File

@@ -41,11 +41,14 @@ public class TenantIgnoreAspect {
@Around("@annotation(tenantIgnore)")
public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
boolean oldIgnore = TenantContextHolder.isIgnore();
if (oldIgnore) {
return joinPoint.proceed();
}
try {
TenantContextHolder.setIgnore(true);
return joinPoint.proceed();
} finally {
TenantContextHolder.setIgnore(oldIgnore);
TenantContextHolder.setIgnore(false);
}
}
}

View File

@@ -51,11 +51,6 @@ public class TenantProperties {
*/
private String tenantIdHeader = "X-Tenant-Id";
/**
* 超级/默认租户 ID超管用户所在租户
*/
private Long superTenantId = 0L;
/**
* 忽略表(忽略拼接租户条件)
*/
@@ -93,14 +88,6 @@ public class TenantProperties {
this.tenantIdHeader = tenantIdHeader;
}
public Long getSuperTenantId() {
return superTenantId;
}
public void setSuperTenantId(Long superTenantId) {
this.superTenantId = superTenantId;
}
public List<String> getIgnoreTables() {
return ignoreTables;
}

View File

@@ -17,11 +17,12 @@
package top.continew.starter.extension.tenant.autoconfigure;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.extension.tenant.annotation.ConditionalOnEnabledTenant;
import top.continew.starter.extension.tenant.config.TenantProvider;
import top.continew.starter.extension.tenant.interceptor.TenantInterceptor;
@@ -32,8 +33,9 @@ import top.continew.starter.extension.tenant.interceptor.TenantInterceptor;
* @since 2.7.0
*/
@AutoConfiguration
@ConditionalOnEnabledTenant
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = PropertiesConstants.TENANT, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(TenantProperties.class)
public class TenantWebMvcAutoConfiguration implements WebMvcConfigurer {
private final TenantProperties tenantProperties;
@@ -46,6 +48,7 @@ public class TenantWebMvcAutoConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantInterceptor(tenantProperties, tenantProvider));
registry.addInterceptor(new TenantInterceptor(tenantProperties, tenantProvider))
.order(OrderedConstants.Interceptor.TENANT_INTERCEPTOR);
}
}

View File

@@ -35,7 +35,7 @@ public class TenantContext {
/**
* 隔离级别
*/
private TenantIsolationLevel isolationLevel;
private TenantIsolationLevel isolationLevel = TenantIsolationLevel.LINE;
/**
* 数据源信息

View File

@@ -19,7 +19,6 @@ package top.continew.starter.extension.tenant.interceptor;
import cn.hutool.core.annotation.AnnotationUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import top.continew.starter.extension.tenant.annotation.TenantIgnore;
@@ -33,7 +32,7 @@ import top.continew.starter.extension.tenant.context.TenantContextHolder;
* @author Charles7c
* @since 2.7.0
*/
public class TenantInterceptor implements HandlerInterceptor, Ordered {
public class TenantInterceptor implements HandlerInterceptor {
private final TenantProperties tenantProperties;
private final TenantProvider tenantProvider;
@@ -45,21 +44,11 @@ public class TenantInterceptor implements HandlerInterceptor, Ordered {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 忽略租户拦截
if (handler instanceof HandlerMethod handlerMethod) {
TenantIgnore methodAnnotation = handlerMethod.getMethodAnnotation(TenantIgnore.class);
if (methodAnnotation != null) {
return true;
}
TenantIgnore classAnnotation = AnnotationUtil.getAnnotation(handlerMethod
.getBeanType(), TenantIgnore.class);
if (classAnnotation != null) {
return true;
}
}
// 设置上下文
String tenantId = request.getHeader(tenantProperties.getTenantIdHeader());
TenantContextHolder.setContext(tenantProvider.getByTenantId(tenantId, true));
// 设置是否忽略租户
TenantContextHolder.setIgnore(this.isIgnore(handler));
return true;
}
@@ -69,8 +58,20 @@ public class TenantInterceptor implements HandlerInterceptor, Ordered {
TenantContextHolder.clear();
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
/**
* 是否忽略租户
*
* @param handler 处理器
* @return 是否忽略租户
*/
private boolean isIgnore(Object handler) {
if (handler instanceof HandlerMethod handlerMethod) {
TenantIgnore methodAnnotation = handlerMethod.getMethodAnnotation(TenantIgnore.class);
if (methodAnnotation != null) {
return true;
}
return AnnotationUtil.getAnnotation(handlerMethod.getBeanType(), TenantIgnore.class) != null;
}
return false;
}
}

View File

@@ -88,19 +88,18 @@ public class TenantUtils {
*
* @param runnable 业务逻辑
*/
public void executeIgnore(Runnable runnable) {
// 未启用租户,直接执行业务逻辑
if (TenantContextHolder.isTenantDisabled()) {
public static void executeIgnore(Runnable runnable) {
// 未启用或忽略租户,直接执行业务逻辑
if (TenantContextHolder.isTenantDisabled() || TenantContextHolder.isIgnore()) {
runnable.run();
return;
}
boolean oldIgnore = TenantContextHolder.isIgnore();
try {
TenantContextHolder.setIgnore(true);
// 执行业务逻辑
runnable.run();
} finally {
TenantContextHolder.setIgnore(oldIgnore);
TenantContextHolder.setIgnore(false);
}
}
}

View File

@@ -25,7 +25,6 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import top.continew.starter.extension.tenant.annotation.ConditionalOnEnabledTenant;
@@ -47,7 +46,6 @@ import javax.sql.DataSource;
*/
@AutoConfiguration
@ConditionalOnEnabledTenant
@EnableConfigurationProperties(TenantProperties.class)
public class TenantAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(TenantAutoConfiguration.class);

View File

@@ -63,11 +63,6 @@ public class DefaultTenantLineHandler implements TenantLineHandler {
if (TenantContextHolder.isIgnore()) {
return true;
}
// 忽略超级租户
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId == null || tenantId.equals(tenantProperties.getSuperTenantId())) {
return true;
}
// 忽略数据源级隔离
if (TenantIsolationLevel.DATASOURCE.equals(TenantContextHolder.getIsolationLevel())) {
return true;

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.json.jackson.exception;
import top.continew.starter.core.exception.BaseException;
import java.io.Serial;
/**
* JSON 异常
*
* @author Charles7c
* @since 2.13.2
*/
public class JSONException extends BaseException {
@Serial
private static final long serialVersionUID = 1L;
public JSONException() {
}
public JSONException(String message) {
super(message);
}
public JSONException(Throwable cause) {
super(cause);
}
public JSONException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -27,7 +27,9 @@ import java.util.Map;
import java.util.Objects;
/**
* json 构建工具
* JSON 构建工具
*
* @see ObjectMapper
*
* @author echo
* @since 2.11.0

View File

@@ -16,36 +16,36 @@
package top.continew.starter.json.jackson.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import top.continew.starter.json.jackson.exception.JSONException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* json 工具
* JSON 工具
*
* @see ObjectMapper
* @see cn.hutool.json.JSONUtil
*
* @author echo
* @author Charles7c
* @since 2.11.0
*/
public class JSONUtils {
private static final ObjectMapper OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class);
private JSONUtils() {
}
/**
* Jackson 对象映射器,用于 JSON 解析与序列化。
*/
private static final ObjectMapper OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class);
/**
* 获取 Jackson 对象映射器。
* 获取 Jackson 对象映射器
*
* @return {@link ObjectMapper} Jackson 对象映射器
*/
@@ -54,183 +54,136 @@ public class JSONUtils {
}
/**
* 对象转为 json 字符串
* 转换对象为JsonNode<br>
*
* @param object 对象
* @return {@link String }
* @param obj 对象
* @return JsonNode
*/
public static String toJsonStr(Object object) {
if (object == null) {
public static JsonNode parseObj(Object obj) {
if (obj == null) {
return null;
}
try {
return OBJECT_MAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return OBJECT_MAPPER.valueToTree(obj);
}
/**
* 将对象转换为 JsonNode。
* 转换为JSON字符串
*
* @param obj 需要转换的对象
* @return 转换后的 {@link JsonNode},如果 obj 为空,则返回 null
* @param obj 被转为JSON的对象
* @return JSON字符串
*/
public static JsonNode toJson(Object obj) {
public static String toJsonStr(Object obj) {
if (obj == null) {
return null;
}
try {
return OBJECT_MAPPER.valueToTree(obj);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e);
}
}
/**
* 将 List 转换为 JsonNode。
*
* @param list 输入的 List
* @return 转换后的 {@link JsonNode}
*/
public static JsonNode listToJson(List<?> list) {
return toJson(list);
}
/**
* 将 Map 转换为 JsonNode。
*
* @param map 输入的 Map
* @return 转换后的 {@link JsonNode}
*/
public static JsonNode mapToJson(Map<?, ?> map) {
return toJson(map);
}
/**
* 将 JsonNode 转换为 List<String>,用于环境变量格式解析。
*
* @param jsonNode 需要转换的 JsonNode
* @return 转换后的 List<String>
*/
public static List<String> jsonToEnvList(JsonNode jsonNode) {
if (jsonNode == null || jsonNode.isNull()) {
return new ArrayList<>();
}
List<String> envList = new ArrayList<>();
jsonNode.fields().forEachRemaining(field -> {
String key = field.getKey();
JsonNode valueNode = field.getValue();
String value = valueNode.isValueNode() ? valueNode.asText() : valueNode.toString();
envList.add(key + "=" + value);
});
return envList;
}
/**
* 将 JsonNode 转换为 List<String>。
*
* @param jsonNode 需要转换的 JsonNode
* @return 转换后的 List<String>
*/
public static List<String> jsonToStringList(JsonNode jsonNode) {
if (jsonNode == null || jsonNode.isNull()) {
return new ArrayList<>();
}
try {
return OBJECT_MAPPER.convertValue(jsonNode, new TypeReference<>() {
});
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e);
}
}
/**
* 将 JsonNode 转换为指定类型的 Java 对象。
*
* @param jsonNode JSON 数据
* @param clazz 目标 Java 类
* @return 解析后的 Java 对象
*/
public static <T> T fromJson(JsonNode jsonNode, Class<T> clazz) {
try {
return OBJECT_MAPPER.treeToValue(jsonNode, clazz);
return OBJECT_MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
throw new JSONException(e);
}
}
/**
* 解析 JSON 字符串为 Java 对象。
* JSON字符串转为实体类对象,转换异常将被抛出
*
* @param str JSON 字符串
* @param clazz 目标 Java 类
* @return 解析后的 Java 对象
* @param <T> Bean类型
* @param jsonString JSON字符串
* @param beanClass 实体类对象
* @return 实体类对象
*/
public static <T> T parseObject(String str, Class<T> clazz) {
if (StrUtil.isEmpty(str)) {
public static <T> T toBean(String jsonString, Class<T> beanClass) {
if (CharSequenceUtil.isBlank(jsonString)) {
return null;
}
try {
return OBJECT_MAPPER.readValue(str, clazz);
return OBJECT_MAPPER.readValue(jsonString, beanClass);
} catch (IOException e) {
throw new RuntimeException(e);
throw new JSONException(e);
}
}
/**
* 字符串 解析为 list<T>
* 将JSON字符串转换为Bean的List默认为ArrayList
*
* @param str 字符串
* @param clazz 目标 Java 类
* @return 解析后的 List<T>
* @param jsonStr 需要转换的JSON字符串
* @param elementType 列表元素类型
* @return 转换后的 List
* @since 2.13.2
*/
public static <T> List<T> parseArray(String str, Class<T> clazz) {
if (StrUtil.isEmpty(str)) {
return new ArrayList<>();
public static <T> List<T> toList(String jsonStr, Class<T> elementType) {
if (jsonStr == null || jsonStr.trim().isEmpty()) {
return new ArrayList<>(0);
}
try {
return OBJECT_MAPPER.readValue(str, OBJECT_MAPPER.getTypeFactory()
.constructCollectionType(List.class, clazz));
return OBJECT_MAPPER.readValue(jsonStr, OBJECT_MAPPER.getTypeFactory()
.constructCollectionType(List.class, elementType));
} catch (IOException e) {
throw new RuntimeException(e);
throw new JSONException("Failed to parse JSON string to list", e);
}
}
/**
* 判断字符串是否为 JSON 格式。
* 将JSONArray转换为Bean的List默认为ArrayList
*
* @param jsonNode 需要转换的 JsonNode
* @param elementType 列表元素类型
* @return 转换后的 List
* @since 2.13.2
*/
public static <T> List<T> toList(JsonNode jsonNode, Class<T> elementType) {
if (jsonNode == null || jsonNode.isNull()) {
return new ArrayList<>(0);
}
try {
return OBJECT_MAPPER.convertValue(jsonNode, OBJECT_MAPPER.getTypeFactory()
.constructCollectionType(List.class, elementType));
} catch (IllegalArgumentException e) {
throw new JSONException("Failed to convert JSON to list", e);
}
}
/**
* 是否为JSON类型字符串首尾都为大括号或中括号判定为JSON字符串
*
* @param str 字符串
* @return 是否为 JSON 格式
* @return 是否为JSON类型字符串
* @since 2.13.2
* @see cn.hutool.json.JSONUtil#isTypeJSON(String)
* @author Looly<a href="https://gitee.com/dromara/hutool">Hutool</a>
*/
public static boolean isTypeJSON(String str) {
if (StrUtil.isEmpty(str)) {
return false;
}
try {
OBJECT_MAPPER.readTree(str);
return true;
} catch (IOException e) {
return false;
}
return isTypeJSONObject(str) || isTypeJSONArray(str);
}
/**
* JSON 字符串转换为指定类型的 Java 对象。
* 是否为JSONObject类型字符串首尾都为大括号判定为JSONObject字符串
*
* @param str 字符串
* @param clazz 目标对象的 Class 类型
* @return 解析后的 Java 对象
* @param str 字符串
* @return 是否为JSON字符串
* @since 2.13.2
* @see cn.hutool.json.JSONUtil#isTypeJSONObject(String)
* @author Looly<a href="https://gitee.com/dromara/hutool">Hutool</a>
*/
public static <T> T toBean(String str, Class<T> clazz) {
if (StrUtil.isEmpty(str)) {
return null;
}
try {
return OBJECT_MAPPER.readValue(str, clazz);
} catch (IOException e) {
throw new RuntimeException(e);
public static boolean isTypeJSONObject(String str) {
if (CharSequenceUtil.isBlank(str)) {
return false;
}
return CharSequenceUtil.isWrap(CharSequenceUtil.trim(str), '{', '}');
}
/**
* 是否为JSONArray类型的字符串首尾都为中括号判定为JSONArray字符串
*
* @param str 字符串
* @return 是否为JSONArray类型字符串
* @since 2.13.2
* @see cn.hutool.json.JSONUtil#isTypeJSONArray(String)
* @author Looly<a href="https://gitee.com/dromara/hutool">Hutool</a>
*/
public static boolean isTypeJSONArray(String str) {
if (CharSequenceUtil.isBlank(str)) {
return false;
}
return CharSequenceUtil.isWrap(CharSequenceUtil.trim(str), '[', ']');
}
}

View File

@@ -20,9 +20,9 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.CollUtils;
import top.continew.starter.license.exception.LicenseException;
import top.continew.starter.license.model.LicenseExtraModel;
@@ -128,7 +128,7 @@ public class ServerInfoUtils {
* @return {@link String}
*/
private static String getLinuxCpuSerial() {
String result = StrUtil.EMPTY;
String result = StringConstants.EMPTY;
String cpuIdCmd = "dmidecode";
BufferedReader bufferedReader = null;
try {
@@ -160,8 +160,7 @@ public class ServerInfoUtils {
* @return {@link String}
*/
private static String getWindowCpuSerial() {
StringBuilder result = new StringBuilder(StrUtil.EMPTY);
StringBuilder result = new StringBuilder(StringConstants.EMPTY);
File file = null;
BufferedReader input = null;
try {
@@ -205,11 +204,11 @@ public class ServerInfoUtils {
try {
Process process = new ProcessBuilder("sh", "-c", command).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
return reader.lines().findFirst().orElse(StrUtil.EMPTY);
return reader.lines().findFirst().orElse(StringConstants.EMPTY);
}
} catch (IOException e) {
log.error("获取 Linux 主板序列号失败: {}", e.getMessage());
return StrUtil.EMPTY;
return StringConstants.EMPTY;
}
}
@@ -219,7 +218,7 @@ public class ServerInfoUtils {
* @return {@link String}
*/
private static String getWindowMainBoardSerial() {
StringBuilder result = new StringBuilder(StrUtil.EMPTY);
StringBuilder result = new StringBuilder(StringConstants.EMPTY);
File file = null;
BufferedReader input = null;
try {

View File

@@ -22,8 +22,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
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.Configuration;
import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
import top.continew.starter.log.aspect.AccessLogAspect;
import top.continew.starter.log.aspect.LogAspect;
@@ -61,8 +63,11 @@ public class LogAutoConfiguration {
*/
@Bean
@ConditionalOnMissingBean
public LogFilter logFilter() {
return new LogFilter(logProperties);
public FilterRegistrationBean<LogFilter> logFilter() {
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.HttpServletResponse;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
import org.springframework.web.filter.OncePerRequestFilter;
import top.continew.starter.core.wrapper.RepeatReadRequestWrapper;
@@ -40,7 +39,7 @@ import java.net.URISyntaxException;
* @author echo
* @since 1.1.0
*/
public class LogFilter extends OncePerRequestFilter implements Ordered {
public class LogFilter extends OncePerRequestFilter {
private final LogProperties logProperties;
@@ -48,11 +47,6 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
this.logProperties = logProperties;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@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.ConditionalOnWebApplication;
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.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
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.log.annotation.ConditionalOnEnabledLog;
import top.continew.starter.log.dao.LogDao;
@@ -59,7 +61,8 @@ public class LogAutoConfiguration implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor(logProperties, logHandler(), logDao()))
.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
@ConditionalOnMissingBean
public LogFilter logFilter() {
return new LogFilter(logProperties);
public FilterRegistrationBean<LogFilter> logFilter() {
FilterRegistrationBean<LogFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LogFilter(logProperties));
registrationBean.setOrder(OrderedConstants.Filter.LOG_FILTER);
return registrationBean;
}
/**

View File

@@ -16,6 +16,13 @@
<description>ContiNew Starter 安全模块 - 加密</description>
<dependencies>
<!-- 安全模块 - 密码编码器 -->
<dependency>
<groupId>top.continew.starter</groupId>
<artifactId>continew-starter-security-password</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool 加密解密模块(封装 JDK 中加密解密算法) -->
<dependency>
<groupId>cn.hutool</groupId>

View File

@@ -28,6 +28,7 @@ import java.lang.annotation.Target;
* 字段加/解密注解
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@@ -37,7 +38,7 @@ public @interface FieldEncrypt {
/**
* 加密/解密算法
*/
Algorithm value() default Algorithm.AES;
Algorithm value() default Algorithm.DEFAULT;
/**
* 加密/解密处理器
@@ -51,4 +52,14 @@ public @interface FieldEncrypt {
* 对称加密算法密钥
*/
String password() default "";
/**
* 非对称加密算法公钥RSA需要
*/
String publicKey() default "";
/**
* 非对称加密算法私钥RSA需要
*/
String privateKey() default "";
}

View File

@@ -24,19 +24,24 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.crypto.core.MyBatisDecryptInterceptor;
import top.continew.starter.security.crypto.core.MyBatisEncryptInterceptor;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
import top.continew.starter.security.crypto.mybatis.MyBatisDecryptInterceptor;
import top.continew.starter.security.crypto.mybatis.MyBatisEncryptInterceptor;
import top.continew.starter.security.crypto.util.EncryptHelper;
/**
* 加/解密自动配置
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
@AutoConfiguration
@EnableConfigurationProperties(CryptoProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_CRYPTO, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@PropertySource(value = "classpath:default-crypto.yml", factory = GeneralPropertySourceFactory.class)
public class CryptoAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(CryptoAutoConfiguration.class);
@@ -51,8 +56,8 @@ public class CryptoAutoConfiguration {
*/
@Bean
@ConditionalOnMissingBean
public MyBatisEncryptInterceptor myBatisEncryptInterceptor() {
return new MyBatisEncryptInterceptor(properties);
public MyBatisEncryptInterceptor mybatisEncryptInterceptor() {
return new MyBatisEncryptInterceptor();
}
/**
@@ -60,12 +65,13 @@ public class CryptoAutoConfiguration {
*/
@Bean
@ConditionalOnMissingBean(MyBatisDecryptInterceptor.class)
public MyBatisDecryptInterceptor myBatisDecryptInterceptor() {
return new MyBatisDecryptInterceptor(properties);
public MyBatisDecryptInterceptor mybatisDecryptInterceptor() {
return new MyBatisDecryptInterceptor();
}
@PostConstruct
public void postConstruct() {
EncryptHelper.init(properties);
log.debug("[ContiNew Starter] - Auto Configuration 'Security-Crypto' completed initialization.");
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.autoconfigure;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm;
import java.util.Objects;
/**
* 加密上下文
*
* @author lishuyan
* @since 2.13.2
*/
public class CryptoContext {
/**
* 默认算法
*/
private Algorithm algorithm;
/**
* 加密/解密处理器
* <p>
* 优先级高于加密/解密算法
* </p>
*/
Class<? extends IEncryptor> encryptor;
/**
* 对称加密算法密钥
*/
private String password;
/**
* 非对称加密算法公钥
*/
private String publicKey;
/**
* 非对称加密算法私钥
*/
private String privateKey;
public Algorithm getAlgorithm() {
return algorithm;
}
public void setAlgorithm(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() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CryptoContext that = (CryptoContext)o;
return algorithm == that.algorithm && Objects.equals(encryptor, that.encryptor) && Objects
.equals(password, that.password) && Objects.equals(publicKey, that.publicKey) && Objects
.equals(privateKey, that.privateKey);
}
@Override
public int hashCode() {
return Objects.hash(algorithm, encryptor, password, publicKey, privateKey);
}
}

View File

@@ -18,11 +18,13 @@ package top.continew.starter.security.crypto.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.crypto.enums.Algorithm;
/**
* 加/解密配置属性
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
@ConfigurationProperties(PropertiesConstants.SECURITY_CRYPTO)
@@ -33,6 +35,11 @@ public class CryptoProperties {
*/
private boolean enabled = true;
/**
* 默认算法
*/
private Algorithm algorithm = Algorithm.AES;
/**
* 对称加密算法密钥
*/
@@ -56,6 +63,14 @@ public class CryptoProperties {
this.enabled = enabled;
}
public Algorithm getAlgorithm() {
return algorithm;
}
public void setAlgorithm(Algorithm algorithm) {
this.algorithm = algorithm;
}
public String getPassword() {
return password;
}

View File

@@ -0,0 +1,33 @@
/*
* 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 top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* 加密器基类
*
* @author lishuyan
* @since 2.13.2
*/
public abstract class AbstractEncryptor implements IEncryptor {
protected AbstractEncryptor(CryptoContext context) {
// 配置校验与配置注入
}
}

View File

@@ -20,6 +20,7 @@ import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@@ -29,26 +30,40 @@ import java.util.concurrent.ConcurrentHashMap;
* 对称加/解密处理器
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public abstract class AbstractSymmetricCryptoEncryptor implements IEncryptor {
public abstract class AbstractSymmetricCryptoEncryptor extends AbstractEncryptor {
/**
* 对称加密缓存
*/
private static final Map<String, SymmetricCrypto> CACHE = new ConcurrentHashMap<>();
@Override
public String encrypt(String plaintext, String password, String publicKey) throws Exception {
if (CharSequenceUtil.isBlank(plaintext)) {
return plaintext;
}
return this.getCrypto(password).encryptHex(plaintext);
/**
* 加密上下文
*/
private final CryptoContext context;
protected AbstractSymmetricCryptoEncryptor(CryptoContext context) {
super(context);
this.context = context;
}
@Override
public String decrypt(String ciphertext, String password, String privateKey) throws Exception {
public String encrypt(String plaintext) {
if (CharSequenceUtil.isBlank(plaintext)) {
return plaintext;
}
return this.getCrypto(context.getPassword()).encryptHex(plaintext);
}
@Override
public String decrypt(String ciphertext) {
if (CharSequenceUtil.isBlank(ciphertext)) {
return ciphertext;
}
return this.getCrypto(password).decryptStr(ciphertext);
return this.getCrypto(context.getPassword()).decryptStr(ciphertext);
}
/**

View File

@@ -17,6 +17,7 @@
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* AESAdvanced Encryption Standard 加/解密处理器
@@ -29,6 +30,10 @@ import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
*/
public class AesEncryptor extends AbstractSymmetricCryptoEncryptor {
public AesEncryptor(CryptoContext context) {
super(context);
}
@Override
protected SymmetricAlgorithm getAlgorithm() {
return SymmetricAlgorithm.AES;

View File

@@ -30,12 +30,12 @@ import cn.hutool.core.codec.Base64;
public class Base64Encryptor implements IEncryptor {
@Override
public String encrypt(String plaintext, String password, String publicKey) throws Exception {
public String encrypt(String plaintext) {
return Base64.encode(plaintext);
}
@Override
public String decrypt(String ciphertext, String password, String privateKey) throws Exception {
public String decrypt(String ciphertext) {
return Base64.decodeStr(ciphertext);
}
}

View File

@@ -17,6 +17,7 @@
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* DESData Encryption Standard 加/解密处理器
@@ -29,6 +30,10 @@ import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
*/
public class DesEncryptor extends AbstractSymmetricCryptoEncryptor {
public DesEncryptor(CryptoContext context) {
super(context);
}
@Override
protected SymmetricAlgorithm getAlgorithm() {
return SymmetricAlgorithm.DES;

View File

@@ -20,6 +20,7 @@ package top.continew.starter.security.crypto.encryptor;
* 加/解密接口
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public interface IEncryptor {
@@ -28,21 +29,15 @@ public interface IEncryptor {
* 加密
*
* @param plaintext 明文
* @param password 对称加密算法密钥
* @param publicKey 非对称加密算法公钥
* @return 加密后的文本
* @throws Exception /
*/
String encrypt(String plaintext, String password, String publicKey) throws Exception;
String encrypt(String plaintext);
/**
* 解密
*
* @param ciphertext 密文
* @param password 对称加密算法密钥
* @param privateKey 非对称加密算法私钥
* @return 解密后的文本
* @throws Exception /
*/
String decrypt(String ciphertext, String password, String privateKey) throws Exception;
String decrypt(String ciphertext);
}

View File

@@ -0,0 +1,59 @@
/*
* 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.password.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 PasswordEncoderProperties properties = SpringUtil.getBean(PasswordEncoderProperties.class);
public PasswordEncoderEncryptor(CryptoContext context) {
super(context);
}
@Override
public String encrypt(String plaintext) {
// 如果已经是加密格式,直接返回
if (properties.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;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* PBEWithMD5AndDESPassword Based Encryption With MD5 And DES 加/解密处理器
@@ -29,6 +30,10 @@ import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
*/
public class PbeWithMd5AndDesEncryptor extends AbstractSymmetricCryptoEncryptor {
public PbeWithMd5AndDesEncryptor(CryptoContext context) {
super(context);
}
@Override
protected SymmetricAlgorithm getAlgorithm() {
return SymmetricAlgorithm.PBEWithMD5AndDES;

View File

@@ -19,6 +19,7 @@ package top.continew.starter.security.crypto.encryptor;
import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* RSA 加/解密处理器
@@ -27,17 +28,29 @@ import cn.hutool.crypto.asymmetric.KeyType;
* </p>
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public class RsaEncryptor implements IEncryptor {
public class RsaEncryptor extends AbstractEncryptor {
@Override
public String encrypt(String plaintext, String password, String publicKey) throws Exception {
return Base64.encode(SecureUtil.rsa(null, publicKey).encrypt(plaintext, KeyType.PublicKey));
/**
* 加密上下文
*/
private final CryptoContext context;
public RsaEncryptor(CryptoContext context) {
super(context);
this.context = context;
}
@Override
public String decrypt(String ciphertext, String password, String privateKey) throws Exception {
return new String(SecureUtil.rsa(privateKey, null).decrypt(Base64.decode(ciphertext), KeyType.PrivateKey));
public String encrypt(String plaintext) {
return Base64.encode(SecureUtil.rsa(null, context.getPublicKey()).encrypt(plaintext, KeyType.PublicKey));
}
@Override
public String decrypt(String ciphertext) {
return new String(SecureUtil.rsa(context.getPrivateKey(), null)
.decrypt(Base64.decode(ciphertext), KeyType.PrivateKey));
}
}

View File

@@ -22,10 +22,16 @@ import top.continew.starter.security.crypto.encryptor.*;
* 加密/解密算法枚举
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public enum Algorithm {
/**
* 默认使用配置属性的算法
*/
DEFAULT(null),
/**
* AES
*/
@@ -49,7 +55,12 @@ public enum Algorithm {
/**
* Base64
*/
BASE64(Base64Encryptor.class),;
BASE64(Base64Encryptor.class),
/**
* 密码编码器支持算法BCrypt、SCRYPT、PBKDF2、ARGON2
*/
PASSWORD_ENCODER(PasswordEncoderEncryptor.class);
/**
* 加密/解密处理器

View File

@@ -14,18 +14,15 @@
* 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.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.mapping.MappedStatement;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BaseException;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -71,23 +68,6 @@ public abstract class AbstractMyBatisInterceptor {
.toList());
}
/**
* 获取字段加/解密处理器
*
* @param fieldEncrypt 字段加密注解
* @return /解密处理器
*/
protected IEncryptor getEncryptor(FieldEncrypt fieldEncrypt) {
Class<? extends IEncryptor> encryptorClass = fieldEncrypt.encryptor();
// 使用预定义加/解密处理器
if (encryptorClass == IEncryptor.class) {
Algorithm algorithm = fieldEncrypt.value();
return ReflectUtil.newInstance(algorithm.getEncryptor());
}
// 使用自定义加/解密处理器
return SpringUtil.getBean(encryptorClass);
}
/**
* 获取加密参数
*

View File

@@ -14,9 +14,10 @@
* 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.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
@@ -25,39 +26,30 @@ import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.type.SimpleTypeRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.util.EncryptHelper;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
/**
* 字段解密拦截器
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(MyBatisDecryptInterceptor.class);
private CryptoProperties properties;
public MyBatisDecryptInterceptor(CryptoProperties properties) {
this.properties = properties;
}
public MyBatisDecryptInterceptor() {
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行结果
Object obj = invocation.proceed();
if (obj == null) {
if (ObjectUtil.isNull(obj)) {
return null;
}
// 确保目标是 ResultSetHandler
@@ -68,6 +60,9 @@ public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implem
if (obj instanceof List<?> resultList) {
// 处理列表结果
this.decryptList(resultList);
} else if (obj instanceof Map<?, ?> map) {
// 处理Map结果
this.decryptMap(map);
} else {
// 处理单个对象结果
this.decryptObject(obj);
@@ -89,13 +84,25 @@ public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implem
}
}
/**
* 解密Map结果
*
* @param resultMap 结果Map
*/
private void decryptMap(Map<?, ?> resultMap) {
if (CollUtil.isEmpty(resultMap)) {
return;
}
new HashSet<>(resultMap.values()).forEach(this::decryptObject);
}
/**
* 解密单个对象结果
*
* @param result 结果对象
*/
private void decryptObject(Object result) {
if (result == null) {
if (ObjectUtil.isNull(result)) {
return;
}
// StringIntegerLong 等简单类型对象无需处理
@@ -109,21 +116,16 @@ public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implem
}
// 解密处理
for (Field field : fieldList) {
IEncryptor encryptor = super.getEncryptor(field.getAnnotation(FieldEncrypt.class));
Object fieldValue = ReflectUtil.getFieldValue(result, field);
if (fieldValue == null) {
continue;
}
// 优先获取自定义对称加密算法密钥获取不到时再获取全局配置
String password = ObjectUtil.defaultIfBlank(field.getAnnotation(FieldEncrypt.class).password(), properties
.getPassword());
try {
String ciphertext = encryptor.decrypt(fieldValue.toString(), password, properties.getPrivateKey());
ReflectUtil.setFieldValue(result, field, ciphertext);
} catch (Exception e) {
// 解密失败时保留原值避免影响正常业务流程
log.warn("解密失败,请检查加密配置", e);
String strValue = String.valueOf(fieldValue);
if (CharSequenceUtil.isBlank(strValue)) {
continue;
}
ReflectUtil.setFieldValue(result, field, EncryptHelper.decrypt(strValue, field
.getAnnotation(FieldEncrypt.class)));
}
}
}

View File

@@ -14,7 +14,7 @@
* 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.util.ClassUtil;
@@ -28,12 +28,9 @@ import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.util.EncryptHelper;
import java.lang.reflect.Field;
import java.util.Arrays;
@@ -47,18 +44,13 @@ import java.util.regex.Pattern;
* 字段加密拦截器
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implements InnerInterceptor {
private static final Logger log = LoggerFactory.getLogger(MyBatisEncryptInterceptor.class);
private static final Pattern PARAM_PAIRS_PATTERN = Pattern
.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");
private final CryptoProperties properties;
public MyBatisEncryptInterceptor(CryptoProperties properties) {
this.properties = properties;
}
@Override
public void beforeQuery(Executor executor,
@@ -124,22 +116,16 @@ public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implem
*/
private void encryptEntity(List<Field> fieldList, Object entity) {
for (Field field : fieldList) {
IEncryptor encryptor = super.getEncryptor(field.getAnnotation(FieldEncrypt.class));
Object fieldValue = ReflectUtil.getFieldValue(entity, field);
if (fieldValue == null) {
continue;
}
// 优先获取自定义对称加密算法密钥获取不到时再获取全局配置
String password = ObjectUtil.defaultIfBlank(field.getAnnotation(FieldEncrypt.class).password(), properties
.getPassword());
String ciphertext = fieldValue.toString();
try {
ciphertext = encryptor.encrypt(fieldValue.toString(), password, properties.getPublicKey());
} catch (Exception e) {
// 加密失败时保留原值避免影响正常业务流程
log.warn("加密失败,请检查加密配置", e);
String strValue = String.valueOf(fieldValue);
if (CharSequenceUtil.isBlank(strValue)) {
continue;
}
ReflectUtil.setFieldValue(entity, field, ciphertext);
ReflectUtil.setFieldValue(entity, field, EncryptHelper.encrypt(strValue, field
.getAnnotation(FieldEncrypt.class)));
}
}
@@ -197,8 +183,7 @@ public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implem
if (matcher.matches()) {
String valueKey = matcher.group(1);
Object value = updateWrapper.getParamNameValuePairs().get(valueKey);
Object ciphertext = this.doEncrypt(value, fieldEncrypt);
updateWrapper.getParamNameValuePairs().put(valueKey, ciphertext);
updateWrapper.getParamNameValuePairs().put(valueKey, this.doEncrypt(value, fieldEncrypt));
}
}
}
@@ -211,19 +196,14 @@ public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implem
* @param fieldEncrypt 字段加密注解
*/
private Object doEncrypt(Object parameterValue, FieldEncrypt fieldEncrypt) {
if (parameterValue == null) {
if (ObjectUtil.isNull(parameterValue)) {
return null;
}
IEncryptor encryptor = super.getEncryptor(fieldEncrypt);
// 优先获取自定义对称加密算法密钥获取不到时再获取全局配置
String password = ObjectUtil.defaultIfBlank(fieldEncrypt.password(), properties.getPassword());
try {
return encryptor.encrypt(parameterValue.toString(), password, properties.getPublicKey());
} catch (Exception e) {
// 加密失败时保留原值避免影响正常业务流程
log.warn("加密失败,请检查加密配置", e);
String strValue = String.valueOf(parameterValue);
if (CharSequenceUtil.isBlank(strValue)) {
return null;
}
return parameterValue;
return EncryptHelper.encrypt(strValue, fieldEncrypt);
}
/**

View File

@@ -0,0 +1,219 @@
/*
* 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 cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 加密助手
*
* @author lishuyan
* @since 2.13.2
*/
public class EncryptHelper {
private static final Logger log = LoggerFactory.getLogger(EncryptHelper.class);
/**
* 默认加密配置
*/
private static CryptoProperties defaultProperties;
/**
* 加密器缓存
*/
private static final Map<Integer, IEncryptor> ENCRYPTOR_CACHE = new ConcurrentHashMap<>();
private EncryptHelper() {
}
/**
* 初始化默认配置
*
* @param properties 加密配置
*/
public static void init(CryptoProperties properties) {
defaultProperties = properties;
}
/**
* 注册加密执行者到缓存
* <p>
* 计算 CryptoContext 对象的hashCode作为缓存中的key通过hashCode查询缓存中存在则直接返回不存在则创建并缓存
* </p>
*
* @param encryptContext 加密执行者需要的相关配置参数
* @return 加密执行者
*/
public static IEncryptor registerAndGetEncryptor(CryptoContext encryptContext) {
int key = encryptContext.hashCode();
return ENCRYPTOR_CACHE.computeIfAbsent(key, k -> encryptContext.getEncryptor().equals(IEncryptor.class)
? ReflectUtil.newInstance(encryptContext.getAlgorithm().getEncryptor(), encryptContext)
: ReflectUtil.newInstance(encryptContext.getEncryptor(), encryptContext));
}
/**
* 获取字段上的 @FieldEncrypt 注解
*
* @param obj 对象
* @param fieldName 字段名称
* @return 字段上的 @FieldEncrypt 注解
* @throws NoSuchFieldException /
*/
public static FieldEncrypt getFieldEncrypt(Object obj, String fieldName) throws NoSuchFieldException {
Field field = obj.getClass().getDeclaredField(fieldName);
return field.getAnnotation(FieldEncrypt.class);
}
/**
* 加密方法
*
* @param value 待加密字符串
* @param fieldEncrypt 待加密字段注解
* @return 加密后的字符串
*/
public static String encrypt(String value, FieldEncrypt fieldEncrypt) {
if (CharSequenceUtil.isBlank(value) || fieldEncrypt == null || !defaultProperties.isEnabled()) {
return value;
}
String ciphertext = value;
try {
CryptoContext encryptContext = buildEncryptContext(fieldEncrypt);
IEncryptor encryptor = registerAndGetEncryptor(encryptContext);
ciphertext = encryptor.encrypt(ciphertext);
} catch (Exception e) {
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
}
return ciphertext;
}
/**
* 加密方法
*
* @param value 待加密字符串
* @return 加密后的字符串
*/
public static String encrypt(String value) {
if (CharSequenceUtil.isBlank(value) || !defaultProperties.isEnabled()) {
return value;
}
String ciphertext = value;
try {
CryptoContext encryptContext = buildEncryptContext();
IEncryptor encryptor = registerAndGetEncryptor(encryptContext);
ciphertext = encryptor.encrypt(ciphertext);
} catch (Exception e) {
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
}
return ciphertext;
}
/**
* 解密方法
*
* @param value 待解密字符串
* @param fieldEncrypt 待解密字段注解
* @return 解密后的字符串
*/
public static String decrypt(String value, FieldEncrypt fieldEncrypt) {
if (CharSequenceUtil.isBlank(value) || fieldEncrypt == null || !defaultProperties.isEnabled()) {
return value;
}
String plaintext = value;
try {
CryptoContext encryptContext = buildEncryptContext(fieldEncrypt);
IEncryptor encryptor = registerAndGetEncryptor(encryptContext);
plaintext = encryptor.decrypt(plaintext);
} catch (Exception e) {
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
}
return plaintext;
}
/**
* 解密方法
*
* @param value 待解密字符串
* @return 解密后的字符串
*/
public static String decrypt(String value) {
if (CharSequenceUtil.isBlank(value) || !defaultProperties.isEnabled()) {
return value;
}
String plaintext = value;
try {
CryptoContext encryptContext = buildEncryptContext();
IEncryptor encryptor = registerAndGetEncryptor(encryptContext);
plaintext = encryptor.decrypt(plaintext);
} catch (Exception e) {
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
}
return plaintext;
}
/**
* 构建加密上下文
*
* @param fieldEncrypt 字段加密注解
* @return 加密上下文
*/
private static CryptoContext buildEncryptContext(FieldEncrypt fieldEncrypt) {
CryptoContext encryptContext = new CryptoContext();
encryptContext.setAlgorithm(fieldEncrypt.value() == Algorithm.DEFAULT
? defaultProperties.getAlgorithm()
: fieldEncrypt.value());
encryptContext.setEncryptor(fieldEncrypt.encryptor().equals(IEncryptor.class)
? IEncryptor.class
: fieldEncrypt.encryptor());
encryptContext.setPassword(fieldEncrypt.password().isEmpty()
? defaultProperties.getPassword()
: fieldEncrypt.password());
encryptContext.setPrivateKey(fieldEncrypt.privateKey().isEmpty()
? defaultProperties.getPrivateKey()
: fieldEncrypt.privateKey());
encryptContext.setPublicKey(fieldEncrypt.publicKey().isEmpty()
? defaultProperties.getPublicKey()
: fieldEncrypt.publicKey());
return encryptContext;
}
/**
* 构建加密上下文
*
* @return 加密上下文
*/
private static CryptoContext buildEncryptContext() {
CryptoContext encryptContext = new CryptoContext();
encryptContext.setAlgorithm(defaultProperties.getAlgorithm());
encryptContext.setPassword(defaultProperties.getPassword());
encryptContext.setPrivateKey(defaultProperties.getPrivateKey());
encryptContext.setPublicKey(defaultProperties.getPublicKey());
return encryptContext;
}
}

View File

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

View File

@@ -16,8 +16,6 @@
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;
@@ -25,18 +23,17 @@ 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.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 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.GeneralPropertySourceFactory;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.security.password.enums.PasswordEncoderAlgorithm;
import top.continew.starter.security.password.util.PasswordEncoderUtil;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@@ -55,15 +52,11 @@ import java.util.Map;
*/
@AutoConfiguration
@EnableConfigurationProperties(PasswordEncoderProperties.class)
@PropertySource(value = "classpath:default-password.yml", factory = GeneralPropertySourceFactory.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;
}
/**
* 密码编码器
@@ -72,23 +65,19 @@ public class PasswordEncoderAutoConfiguration {
* @see PasswordEncoderFactories
*/
@Bean
public PasswordEncoder passwordEncoder(List<PasswordEncoder> passwordEncoderList) {
public PasswordEncoder passwordEncoder(PasswordEncoderProperties properties) {
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);
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 = properties.getAlgorithm();
CheckUtils.throwIf(PasswordEncoderUtil.getEncoder(algorithm) == null, "不支持的加密算法: {}", algorithm);
return new DelegatingPasswordEncoder(algorithm.name().toLowerCase(), encoders);
}
@PostConstruct

View File

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

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.password.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.password.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

@@ -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.password.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.password.enums.PasswordEncoderAlgorithm;
import top.continew.starter.security.password.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:
password:
enabled: true
# 默认启用的编码器算法默认BCrypt 加密算法)
algorithm: BCRYPT

View File

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

View File

@@ -16,7 +16,7 @@
package top.continew.starter.storage.enums;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.text.CharSequenceUtil;
import top.continew.starter.core.enums.BaseEnum;
import java.util.Arrays;
@@ -69,7 +69,7 @@ public enum FileType implements BaseEnum<Integer> {
*/
public static FileType getByExtension(String extension) {
return Arrays.stream(FileType.values())
.filter(t -> t.getExtensions().contains(StrUtil.emptyIfNull(extension).toLowerCase()))
.filter(t -> t.getExtensions().contains(CharSequenceUtil.emptyIfNull(extension).toLowerCase()))
.findFirst()
.orElse(FileType.UNKNOWN);
}

View File

@@ -19,7 +19,7 @@ package top.continew.starter.storage.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.text.CharSequenceUtil;
import top.continew.starter.core.constant.StringConstants;
import java.io.ByteArrayInputStream;
@@ -121,7 +121,7 @@ public class StorageUtils {
// 获取文件的扩展名
String extName = FileNameUtil.extName(fileName);
// 去掉扩展名
String baseName = StrUtil.subBefore(fileName, StringConstants.DOT, true);
String baseName = CharSequenceUtil.subBefore(fileName, StringConstants.DOT, true);
// 拼接新的路径:原始路径 + .缩略图后缀 + .扩展名
return baseName + "." + suffix + "." + extName;
}

View File

@@ -19,8 +19,8 @@ package top.continew.starter.storage.strategy;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BusinessException;
@@ -123,7 +123,7 @@ public class LocalStorageStrategy implements StorageStrategy<LocalClient> {
// 格式化文件名 防止上传后重复
String formatFileName = StorageUtils.formatFileName(fileName);
// 判断文件路径是否为空 为空给默认路径 格式 2024/12/30/
if (StrUtil.isEmpty(path)) {
if (CharSequenceUtil.isEmpty(path)) {
path = StorageUtils.localDefaultPath();
}
// 判断文件夹是否存在 不存在则创建

View File

@@ -19,8 +19,8 @@ package top.continew.starter.storage.strategy;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -156,7 +156,7 @@ public class OssStorageStrategy implements StorageStrategy<OssClient> {
// 格式化文件名 防止上传后重复
String formatFileName = StorageUtils.formatFileName(fileName);
// 判断文件路径是否为空 为空给默认路径 格式 2024/12/30/
if (StrUtil.isEmpty(path)) {
if (CharSequenceUtil.isEmpty(path)) {
path = StorageUtils.ossDefaultPath();
}
ThumbnailResp thumbnailResp = null;

View File

@@ -16,7 +16,7 @@
package top.continew.starter.storage.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.text.CharSequenceUtil;
import software.amazon.awssdk.regions.Region;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.storage.constant.StorageConstant;
@@ -39,7 +39,7 @@ public class OssUtils {
* @return {@link Region }
*/
public static Region getRegion(String region) {
return StrUtil.isEmpty(region) ? Region.US_EAST_1 : Region.of(region);
return CharSequenceUtil.isEmpty(region) ? Region.US_EAST_1 : Region.of(region);
}
/**
@@ -51,7 +51,7 @@ public class OssUtils {
*/
public static String getUrl(String endpoint, String bucketName) {
// 如果是云服务商,直接返回域名或终端点
if (StrUtil.containsAny(endpoint, StorageConstant.CLOUD_SERVICE_PREFIX)) {
if (CharSequenceUtil.containsAny(endpoint, StorageConstant.CLOUD_SERVICE_PREFIX)) {
return "http://" + bucketName + StringConstants.DOT + endpoint;
} else {
return "http://" + endpoint + StringConstants.SLASH + bucketName;

View File

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

View File

@@ -16,6 +16,7 @@
package top.continew.starter.validation.constraints;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.json.JSONUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
@@ -30,7 +31,7 @@ public class JsonStringValidator implements ConstraintValidator<JsonString, Stri
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
if (CharSequenceUtil.isBlank(value)) {
return true;
}
return JSONUtil.isTypeJSON(value);

View File

@@ -16,6 +16,7 @@
package top.continew.starter.validation.constraints;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.PhoneUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
@@ -34,7 +35,7 @@ public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
if (CharSequenceUtil.isBlank(value)) {
return true;
}
return PhoneUtil.isMobile(value);

View File

@@ -16,6 +16,7 @@
package top.continew.starter.validation.constraints;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.PhoneUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
@@ -34,7 +35,7 @@ public class PhoneValidator implements ConstraintValidator<Phone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
if (CharSequenceUtil.isBlank(value)) {
return true;
}
return PhoneUtil.isPhone(value);

View File

@@ -54,7 +54,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GlobalResponseProperties.class)
@PropertySource(value = "classpath:default-web.yml", factory = GeneralPropertySourceFactory.class)
@PropertySource(value = "classpath:default-response.yml", factory = GeneralPropertySourceFactory.class)
public class GlobalResponseAutoConfiguration {
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.server.handlers.DisallowedMethodsHandler;
import io.undertow.util.HttpString;
import org.springframework.context.annotation.PropertySource;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.util.CollUtils;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
/**
* Undertow 自动配置
@@ -44,6 +46,7 @@ import top.continew.starter.core.util.CollUtils;
@ConditionalOnWebApplication
@ConditionalOnClass(Undertow.class)
@EnableConfigurationProperties(ServerExtensionProperties.class)
@PropertySource(value = "classpath:default-server.yml", factory = GeneralPropertySourceFactory.class)
@ConditionalOnProperty(prefix = "server.extension", name = PropertiesConstants.ENABLED, havingValue = "true")
public class UndertowAutoConfiguration {

View File

@@ -23,20 +23,3 @@ continew-starter.web.response:
- io.swagger.**
- org.springdoc.**
- 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