refactor(pigx-common-data): 解耦 pigx-common-data 对 pigx-common-security 的依赖

This commit is contained in:
PIGCLOUD committed 2026-05-21 10:39:50 +08:00
1 parent 5ad4dfb711
commit 0fca2924d4
12 files changed
+275 -340

No files matched your search

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -49,12 +49,10 @@ public class RedisAutoCacheManager extends RedisCacheManager {
private static final int CACHE_LENGTH = 2;
public RedisAutoCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
boolean allowInFlightCacheCreation,
Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
boolean allowInFlightCacheCreation, Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheConfigurations);
}
@Override
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
if (StrUtil.isBlank(name) || !name.contains(SPLIT_FLAG)) {
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -17,16 +17,14 @@
package com.pig4cloud.pigx.common.data.cache;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pig4cloud.pigx.common.core.jackson.PigxJavaTimeModule;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.cache.autoconfigure.CacheManagerCustomizers;
import org.springframework.boot.cache.autoconfigure.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -49,9 +47,10 @@ import java.util.Objects;
* @author L.cm
*/
@Configuration
@AutoConfigureAfter({RedisAutoConfiguration.class})
@AutoConfigureAfter({DataRedisAutoConfiguration.class})
@ConditionalOnBean({RedisConnectionFactory.class})
@EnableConfigurationProperties(CacheProperties.class)
@SuppressWarnings("removal")
public class RedisCacheAutoConfiguration {
private final CacheProperties cacheProperties;
@@ -70,11 +69,12 @@ public class RedisCacheAutoConfiguration {
@Primary
@Bean("cacheResolver")
public CacheManager redisCacheManager(ObjectProvider<RedisConnectionFactory> connectionFactoryObjectProvider) {
public CacheManager redisCacheManager(ObjectProvider<RedisConnectionFactory> connectionFactoryObjectProvider,
ObjectMapper objectMapper) {
RedisConnectionFactory connectionFactory = connectionFactoryObjectProvider.getIfAvailable();
Objects.requireNonNull(connectionFactory, "Bean RedisConnectionFactory is null.");
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration cacheConfiguration = this.determineConfiguration();
RedisCacheConfiguration cacheConfiguration = this.determineConfiguration(objectMapper);
List<String> cacheNames = this.cacheProperties.getCacheNames();
Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>();
if (!cacheNames.isEmpty()) {
@@ -91,16 +91,18 @@ public class RedisCacheAutoConfiguration {
return this.customizerInvoker.customize(cacheManager);
}
private RedisCacheConfiguration determineConfiguration() {
private RedisCacheConfiguration determineConfiguration(ObjectMapper objectMapper) {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
RedisSerializer<Object> jsonRedisSerializer = RedisSerializer.json();
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(jsonRedisSerializer, "mapper");
objectMapper.registerModules(new PigxJavaTimeModule());
ObjectMapper redisObjectMapper = objectMapper.copy();
RedisSerializer<Object> jsonRedisSerializer = org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.builder()
.objectMapper(redisObjectMapper)
.defaultTyping(true)
.build();
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jsonRedisSerializer));
if (redisProperties.getTimeToLive() != null) {
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -18,9 +18,9 @@
package com.pig4cloud.pigx.common.data.cache;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.cache.autoconfigure.CacheManagerCustomizer;
import org.springframework.boot.cache.autoconfigure.CacheManagerCustomizers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -18,6 +18,7 @@
package com.pig4cloud.pigx.common.data.cache;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -33,8 +34,7 @@ import org.springframework.data.redis.serializer.RedisSerializer;
*/
@EnableCaching
@Configuration
@AutoConfigureBefore(name = { "org.redisson.spring.starter.RedissonAutoConfigurationV2",
"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration" })
@AutoConfigureBefore(value = DataRedisAutoConfiguration.class, name = "org.redisson.spring.starter.RedissonAutoConfigurationV2")
public class RedisTemplateConfiguration {
@Bean
@@ -4,18 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
@@ -26,17 +15,9 @@ import java.util.List;
import java.util.Map;
/**
* 数据权限内部拦截器
* <p>
* 参照 MyBatis Plus {@code DataPermissionInterceptor} 的实现思路,
* 通过 JSqlParser 解析 SQL 为 AST,使用 {@code PlainSelect.setWhere()} 直接在
* WHERE 子句上追加数据权限条件,避免子查询包装导致索引失效。
* 对于 UNION 等复杂 SQL 或解析失败的情况,回退到子查询包装方式保证兼容性。
*
* @author lengleng
* @date 2020/11/29
*/
@Slf4j
public class DataScopeInnerInterceptor implements DataScopeInterceptor {
@Setter
@@ -72,148 +53,37 @@ public class DataScopeInnerInterceptor implements DataScopeInterceptor {
return;
}
try {
Statement statement = CCJSqlParserUtil.parse(originalSql);
if (!(statement instanceof Select selectStatement)) {
return;
}
// UNION 等复杂语句无法直接操作 WHERE,回退到子查询包装
if (!(selectStatement.getSelectBody() instanceof PlainSelect plainSelect)) {
mpBs.sql(buildSubQuerySql(originalSql, dataScope));
return;
}
// 构建数据权限条件表达式
Expression scopeExpression = buildScopeExpression(plainSelect, dataScope);
if (scopeExpression == null) {
return;
}
// 与原有 WHERE 条件合并(数据权限条件 AND 原条件)
Expression existingWhere = plainSelect.getWhere();
if (existingWhere != null) {
plainSelect.setWhere(new AndExpression(scopeExpression, existingWhere));
} else {
plainSelect.setWhere(scopeExpression);
}
mpBs.sql(selectStatement.toString());
} catch (JSQLParserException e) {
log.warn("数据权限 SQL 解析失败,回退到子查询包装: {}", originalSql, e);
mpBs.sql(buildSubQuerySql(originalSql, dataScope));
}
}
/**
* 根据 DataScope 配置构建数据权限条件表达式
* <p>
* 使用 {@code CCJSqlParserUtil.parseCondExpression()} 解析条件字符串为 Expression 对象,
* 与 MyBatis Plus DataPermissionInterceptor 保持一致的实现方式。
*
* @param plainSelect 解析后的 PlainSelect 对象
* @param dataScope 数据权限参数
* @return 数据权限条件表达式,无需处理时返回 null
*/
private Expression buildScopeExpression(PlainSelect plainSelect, DataScope dataScope)
throws JSQLParserException {
List<Long> deptIds = dataScope.getDeptList();
String username = dataScope.getUsername();
// 获取表引用(优先别名,其次表名),用于限定列名
String tableRef = getTableReference(plainSelect.getFromItem());
String prefix = StrUtil.isNotBlank(tableRef) ? tableRef + "." : "";
// 1. 无数据权限限制,返回 0 条数据
if (CollUtil.isEmpty(deptIds) && StrUtil.isBlank(username)) {
return CCJSqlParserUtil.parseCondExpression("1 = 2");
// 1.无数据权限限制,则直接返回 0 条数据
if (CollUtil.isEmpty(deptIds) && StrUtil.isBlank(dataScope.getUsername())) {
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE 1 = 2",
dataScope.getFunc().getType(), originalSql);
}
// SQL 注入检查
if (StrUtil.isNotBlank(username) && SqlInjectionUtils.check(username)) {
return CCJSqlParserUtil.parseCondExpression("1 = 2");
}
// 2. 本人权限 + 部门权限:加括号保证与原有条件组合时的优先级
if (StrUtil.isNotBlank(username) && CollUtil.isNotEmpty(deptIds)) {
// 2.如果为本人权限 + 部门权限控制
else if (StrUtil.isNotBlank(dataScope.getUsername()) && CollUtil.isNotEmpty(deptIds)) {
String join = CollectionUtil.join(deptIds, ",");
// 使用配置的组合模式(AND/OR),默认OR保持向后兼容
String logicKeyword = dataScope.getLogicMode() != null ? dataScope.getLogicMode().getKeyword()
: DataScopeLogicEnum.OR.getKeyword();
String condSql = String.format("(%s%s = '%s' %s %s%s IN (%s))", prefix, dataScope.getScopeUserName(),
username, logicKeyword, prefix, dataScope.getScopeDeptName(), join);
return CCJSqlParserUtil.parseCondExpression(condSql);
originalSql = String.format(
"SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s' %s temp_data_scope.%s IN (%s)",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername(),
logicKeyword, dataScope.getScopeDeptName(), join);
}
// 3. 仅本人权限
if (StrUtil.isNotBlank(username)) {
String condSql = String.format("%s%s = '%s'", prefix, dataScope.getScopeUserName(), username);
return CCJSqlParserUtil.parseCondExpression(condSql);
// 3. 如果为本人
else if (StrUtil.isNotBlank(dataScope.getUsername())) {
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s'",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername());
}
// 4. 仅部门权限
String join = CollectionUtil.join(deptIds, ",");
String condSql = String.format("%s%s IN (%s)", prefix, dataScope.getScopeDeptName(), join);
return CCJSqlParserUtil.parseCondExpression(condSql);
}
/**
* 获取表引用名称(优先别名,其次表名)
*
* @param fromItem FROM 子句项
* @return 表引用名称,无法获取时返回 null
*/
private String getTableReference(FromItem fromItem) {
if (fromItem == null) {
return null;
}
if (fromItem.getAlias() != null) {
return fromItem.getAlias().getName();
}
if (fromItem instanceof Table table) {
return table.getName();
}
return null;
}
/**
* 回退方案:子查询包装(用于 UNION 等复杂 SQL 或解析失败时)
*/
private String buildSubQuerySql(String originalSql, DataScope dataScope) {
List<Long> deptIds = dataScope.getDeptList();
String username = dataScope.getUsername();
String selectType = dataScope.getFunc().getType();
// 无权限
if (CollUtil.isEmpty(deptIds) && StrUtil.isBlank(username)) {
return String.format("SELECT %s FROM (%s) temp_data_scope WHERE 1 = 2", selectType, originalSql);
}
// SQL 注入检查
if (StrUtil.isNotBlank(username) && SqlInjectionUtils.check(username)) {
return String.format("SELECT %s FROM (%s) temp_data_scope WHERE 1 = 2", selectType, originalSql);
}
// 本人 + 部门
if (StrUtil.isNotBlank(username) && CollUtil.isNotEmpty(deptIds)) {
// 4.部门权限控制
else {
String join = CollectionUtil.join(deptIds, ",");
String logicKeyword = dataScope.getLogicMode() != null ? dataScope.getLogicMode().getKeyword()
: DataScopeLogicEnum.OR.getKeyword();
return String.format(
"SELECT %s FROM (%s) temp_data_scope WHERE (temp_data_scope.%s = '%s' %s temp_data_scope.%s IN (%s))",
selectType, originalSql, dataScope.getScopeUserName(), username, logicKeyword,
dataScope.getScopeDeptName(), join);
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s IN (%s)",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeDeptName(), join);
}
// 仅本人
if (StrUtil.isNotBlank(username)) {
return String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s'", selectType,
originalSql, dataScope.getScopeUserName(), username);
}
// 仅部门
String join = CollectionUtil.join(deptIds, ",");
return String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s IN (%s)", selectType,
originalSql, dataScope.getScopeDeptName(), join);
mpBs.sql(originalSql);
}
/**
@@ -228,11 +98,11 @@ public class DataScopeInnerInterceptor implements DataScopeInterceptor {
} else if (parameterObj instanceof Map) {
for (Object val : ((Map<?, ?>) parameterObj).values()) {
if (val instanceof DataScope) {
return (DataScope) val;
}
}
}
return null;
}
return (DataScope) val;
}
}
}
return null;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -22,26 +22,16 @@ import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pigx.admin.api.entity.SysDept;
import com.pig4cloud.pigx.admin.api.entity.SysRole;
import com.pig4cloud.pigx.admin.api.feign.RemoteDataScopeService;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum;
import com.pig4cloud.pigx.common.core.context.UserContext;
import com.pig4cloud.pigx.common.core.context.UserContextHolder;
import com.pig4cloud.pigx.common.core.util.RetOps;
import com.pig4cloud.pigx.common.security.service.PigxUser;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* @author lengleng
* @date 2019-09-07
* <p>
* 默认data scope 判断处理器
*/
/**
* @author lengleng
* @date 2019-09-07
@@ -53,12 +43,13 @@ public class PigxDefaultDataScopeHandle implements DataScopeHandle {
private final RemoteDataScopeService dataScopeService;
private final UserContextHolder userContextHolder;
@Override
public Boolean calcScope(DataScope dataScope) {
// 对于TOC客户端,不进行数据权限校验
PigxUser user = SecurityUtils.getUser();
if (UserTypeEnum.TOC.getStatus().equals(user.getUserType())) {
return true;
UserContext user = userContextHolder.get();
if (user == null) {
return false;
}
// 业务代码里的规则,覆盖计算规则
@@ -66,19 +57,13 @@ public class PigxDefaultDataScopeHandle implements DataScopeHandle {
return false;
}
// 获取用户角色ID列表
List<String> roleIdList = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.filter(authority -> authority.startsWith(SecurityConstants.ROLE))
.map(authority -> authority.split(StrUtil.UNDERLINE)[1])
.toList();
List<Long> roleIdList = user.getRoleIds();
if (CollUtil.isEmpty(roleIdList)) {
return false;
}
// 获取角色列表
List<SysRole> roleList = RetOps.of(dataScopeService.getRoleList(roleIdList))
List<SysRole> roleList = RetOps.of(dataScopeService.getRoleList(roleIdList.stream().map(String::valueOf).toList()))
.getData()
.orElseGet(Collections::emptyList);
if (CollUtil.isEmpty(roleList)) {
@@ -92,7 +77,7 @@ public class PigxDefaultDataScopeHandle implements DataScopeHandle {
/**
* 处理数据权限
*/
private boolean processDataScope(PigxUser user, DataScope dataScope, List<SysRole> roleList) {
private boolean processDataScope(UserContext user, DataScope dataScope, List<SysRole> roleList) {
List<Long> deptList = dataScope.getDeptList();
for (SysRole role : roleList) {
@@ -134,7 +119,7 @@ public class PigxDefaultDataScopeHandle implements DataScopeHandle {
/**
* 处理本级及下级数据权限
*/
private void handleOwnChildLevelScope(PigxUser user, List<Long> deptList) {
private void handleOwnChildLevelScope(UserContext user, List<Long> deptList) {
List<Long> descendantDeptIds = RetOps.of(dataScopeService.getDescendantList(user.getDeptId()))
.getData()
.orElseGet(Collections::emptyList)
@@ -147,14 +132,14 @@ public class PigxDefaultDataScopeHandle implements DataScopeHandle {
/**
* 处理本级数据权限
*/
private void handleOwnLevelScope(PigxUser user, List<Long> deptList) {
private void handleOwnLevelScope(UserContext user, List<Long> deptList) {
deptList.add(user.getDeptId());
}
/**
* 处理个人数据权限
*/
private void handleSelfLevelScope(PigxUser user, DataScope dataScope) {
private void handleSelfLevelScope(UserContext user, DataScope dataScope) {
dataScope.setUsername(user.getUsername());
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -23,6 +23,7 @@ import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInt
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.pig4cloud.pigx.admin.api.feign.RemoteDataScopeService;
import com.pig4cloud.pigx.common.core.context.UserContextHolder;
import com.pig4cloud.pigx.common.data.datascope.DataScopeInnerInterceptor;
import com.pig4cloud.pigx.common.data.datascope.DataScopeInterceptor;
import com.pig4cloud.pigx.common.data.datascope.DataScopeSqlInjector;
@@ -30,15 +31,15 @@ import com.pig4cloud.pigx.common.data.datascope.PigxDefaultDataScopeHandle;
import com.pig4cloud.pigx.common.data.resolver.SqlFilterArgumentResolver;
import com.pig4cloud.pigx.common.data.tenant.PigxTenantConfigProperties;
import com.pig4cloud.pigx.common.data.tenant.PigxTenantHandler;
import com.pig4cloud.pigx.common.security.service.PigxUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@@ -53,6 +54,7 @@ import java.util.Properties;
* @author lengleng
* @date 2020-02-08
*/
@Slf4j
@Configuration
@ConditionalOnBean(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@@ -71,17 +73,26 @@ public class MybatisPlusConfiguration implements WebMvcConfigurer {
/**
* mybatis plus 拦截器配置
* <p>
* 数据权限拦截器 {@link DataScopeInterceptor} 改为可选注入:
* 未提供 {@link com.pig4cloud.pigx.common.core.context.UserContextHolder} 的场景下不装配,
* 同时记录 WARN 日志,租户/分页/乐观锁等其它能力仍正常工作。
*
* @return PigxDefaultDatascopeHandle
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(TenantLineInnerInterceptor tenantLineInnerInterceptor,
DataScopeInterceptor dataScopeInterceptor) {
ObjectProvider<DataScopeInterceptor> dataScopeInterceptorProvider) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 注入多租户支持
interceptor.addInnerInterceptor(tenantLineInnerInterceptor);
// 数据权限
interceptor.addInnerInterceptor(dataScopeInterceptor);
// 数据权限(可选)
DataScopeInterceptor dataScopeInterceptor = dataScopeInterceptorProvider.getIfAvailable();
if (dataScopeInterceptor != null) {
interceptor.addInnerInterceptor(dataScopeInterceptor);
} else {
log.warn("No UserContextHolder bean found, DataScopeInterceptor disabled.");
}
// 分页支持
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setMaxLimit(1000L);
@@ -112,10 +123,11 @@ public class MybatisPlusConfiguration implements WebMvcConfigurer {
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(PigxUser.class)
public DataScopeInterceptor dataScopeInterceptor(RemoteDataScopeService dataScopeService) {
@ConditionalOnBean(UserContextHolder.class)
public DataScopeInterceptor dataScopeInterceptor(RemoteDataScopeService dataScopeService,
UserContextHolder userContextHolder) {
DataScopeInnerInterceptor dataScopeInnerInterceptor = new DataScopeInnerInterceptor();
dataScopeInnerInterceptor.setDataScopeHandle(new PigxDefaultDataScopeHandle(dataScopeService));
dataScopeInnerInterceptor.setDataScopeHandle(new PigxDefaultDataScopeHandle(dataScopeService, userContextHolder));
return dataScopeInnerInterceptor;
}
@@ -3,11 +3,16 @@ package com.pig4cloud.pigx.common.data.resolver;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pigx.admin.api.feign.RemoteParamService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
import lombok.experimental.UtilityClass;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author lengleng
@@ -20,17 +25,42 @@ public class ParamResolver {
/**
* 根据多个key 查询value 配置 结果使用hutool 的maputil 进行包装处理 MapUtil.getBool(result,key)
* @param key key
* @param keys key
* @return Map<String,Object>
*/
public Map<String, Object> getMap(String... key) {
public Map<String, Object> getMap(String... keys) {
// 校验入参是否合法
if (Objects.isNull(key)) {
if (keys == null) {
throw new IllegalArgumentException("参数不合法");
}
RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
return remoteParamService.getByKeys(key).getData();
Cache cache = paramsCache();
Map<String, Object> result = new LinkedHashMap<>();
List<String> missedKeys = new ArrayList<>();
for (String k : keys) {
Cache.ValueWrapper wrapper = (cache != null) ? cache.get(k) : null;
if (wrapper != null) {
result.put(k, wrapper.get());
}
else {
missedKeys.add(k);
}
}
if (!missedKeys.isEmpty()) {
RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
Map<String, Object> fetched = remoteParamService.getByKeys(missedKeys.toArray(new String[0])).getData();
if (fetched != null) {
result.putAll(fetched);
if (cache != null) {
fetched.forEach(cache::put);
}
}
}
return result;
}
/**
@@ -59,19 +89,35 @@ public class ParamResolver {
throw new IllegalArgumentException("参数不合法");
}
RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
// 先查缓存
Cache cache = paramsCache();
if (cache != null) {
String cached = cache.get(key, String.class);
if (StrUtil.isNotBlank(cached)) {
return Convert.convert(clazz, cached);
}
}
// 缓存未命中,走 Feign
RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
String result = remoteParamService.getByKey(key).getData();
// 非空结果写入缓存
if (cache != null && StrUtil.isNotBlank(result)) {
cache.put(key, result);
}
if (StrUtil.isNotBlank(result)) {
return Convert.convert(clazz, result);
}
if (defaultVal.length == 1) {
return Convert.convert(clazz, defaultVal.clone()[0]);
return Convert.convert(clazz, defaultVal[0]);
}
return null;
}
private Cache paramsCache() {
return SpringContextHolder.getBean(CacheManager.class).getCache(CacheConstants.PARAMS_DETAILS);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -47,7 +47,6 @@ public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver
/**
* 判断Controller是否包含page 参数
*
* @param parameter 参数
* @return 是否过滤
*/
@@ -57,9 +56,9 @@ public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver
}
/**
* @param parameter 入参集合
* @param mavContainer model 和 view
* @param webRequest web相关
* @param parameter 入参集合
* @param mavContainer model 和 view
* @param webRequest web相关
* @param binderFactory 入参解析
* @return 检查后新的page对象
* <p>
@@ -67,7 +66,7 @@ public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
@@ -92,15 +91,15 @@ public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver
List<OrderItem> orderItemList = new ArrayList<>();
Optional.ofNullable(ascs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(asc -> !SqlInjectionUtils.check(asc))
.map(OrderItem::asc)
.toList()));
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(asc -> !SqlInjectionUtils.check(asc))
.map(OrderItem::asc)
.toList()));
Optional.ofNullable(descs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(desc -> !SqlInjectionUtils.check(desc))
.map(OrderItem::desc)
.toList()));
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(desc -> !SqlInjectionUtils.check(desc))
.map(OrderItem::desc)
.toList()));
page.addOrder(orderItemList);
return page;
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -40,63 +40,60 @@ import java.util.Objects;
@RequiredArgsConstructor
public class PigxTenantHandler implements TenantLineHandler {
private final PigxTenantConfigProperties properties;
private final PigxTenantConfigProperties properties;
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
*
* @return 租户 ID 值表达式
*/
@Override
public Expression getTenantId() {
Long tenantId = TenantContextHolder.getTenantId();
log.debug("当前租户为 >> {}", tenantId);
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
* @return 租户 ID 值表达式
*/
@Override
public Expression getTenantId() {
Long tenantId = TenantContextHolder.getTenantId();
log.debug("当前租户为 >> {}", tenantId);
if (tenantId == null) {
return new NullValue();
}
return new LongValue(tenantId);
}
if (tenantId == null) {
return new NullValue();
}
return new LongValue(tenantId);
}
/**
* 获取租户字段名
*
* @return 租户字段名
*/
@Override
public String getTenantIdColumn() {
return properties.getColumn();
}
/**
* 获取租户字段名
* @return 租户字段名
*/
@Override
public String getTenantIdColumn() {
return properties.getColumn();
}
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
// 判断是否跳过当前查询的租户过滤
if (TenantContextHolder.getTenantSkip()) {
return Boolean.TRUE;
}
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
// 判断是否跳过当前查询的租户过滤
if (TenantContextHolder.getTenantSkip()) {
return Boolean.TRUE;
}
Long tenantId = TenantContextHolder.getTenantId();
// 租户中ID 为空,查询全部,不进行过滤
if (tenantId == null) {
return Boolean.TRUE;
}
Long tenantId = TenantContextHolder.getTenantId();
// 租户中ID 为空,查询全部,不进行过滤
if (tenantId == null) {
return Boolean.TRUE;
}
// 判断实体类是否标记为多租户隔离
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
TenantTable annotation = null;
if (Objects.nonNull(tableInfo)) {
annotation = AnnotationUtils.findAnnotation(tableInfo.getEntityType(), TenantTable.class);
}
return !properties.getTables().contains(tableName) && annotation == null;
}
// 判断实体类是否标记为多租户隔离
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
TenantTable annotation = null;
if (Objects.nonNull(tableInfo)) {
annotation = AnnotationUtils.findAnnotation(tableInfo.getEntityType(), TenantTable.class);
}
return !properties.getTables().contains(tableName) && annotation == null;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -37,7 +37,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
@@ -71,25 +70,23 @@ public class TenantContextHolderFilter extends GenericFilterBean {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String headerTenantId = request.getHeader(CommonConstants.TENANT_ID);
String paramTenantId = request.getParameter(CommonConstants.TENANT_ID);
log.debug("获取header中的租户ID为:{}", headerTenantId);
if (StrUtil.isNotBlank(headerTenantId) && !StrUtil.equals(UNDEFINED_STR, headerTenantId)) {
TenantContextHolder.setTenantId(Long.parseLong(headerTenantId));
} else if (StrUtil.isNotBlank(paramTenantId) && !StrUtil.equals(UNDEFINED_STR, paramTenantId)) {
TenantContextHolder.setTenantId(Long.parseLong(paramTenantId));
} else {
TenantContextHolder.setTenantId(CommonConstants.TENANT_ID_1);
}
if (!checkTenantStatus(request, response)) {
TenantContextHolder.clear();
return;
}
try {
String headerTenantId = request.getHeader(CommonConstants.TENANT_ID);
String paramTenantId = request.getParameter(CommonConstants.TENANT_ID);
log.debug("获取header中的租户ID为:{}", headerTenantId);
if (StrUtil.isNotBlank(headerTenantId) && !StrUtil.equals(UNDEFINED_STR, headerTenantId)) {
TenantContextHolder.setTenantId(Long.parseLong(headerTenantId));
} else if (StrUtil.isNotBlank(paramTenantId) && !StrUtil.equals(UNDEFINED_STR, paramTenantId)) {
TenantContextHolder.setTenantId(Long.parseLong(paramTenantId));
} else {
TenantContextHolder.setTenantId(CommonConstants.TENANT_ID_1);
}
if (!checkTenantStatus(request, response)) {
return;
}
filterChain.doFilter(request, response);
} finally {
TenantContextHolder.clear();
@@ -110,27 +107,24 @@ public class TenantContextHolderFilter extends GenericFilterBean {
return true;
}
List<SysTenant> tenantList = null;
// 判断租户状态
Cache cache = cacheManager.getCache(CacheConstants.TENANT_DETAILS);
if (Objects.nonNull(cache) && Objects.nonNull(cache.get(SimpleKey.EMPTY, List.class))) {
tenantList = cache.get(SimpleKey.EMPTY, List.class);
} else {
R<List<SysTenant>> tenantListR = remoteTenantService.list();
tenantList = tenantListR.getData();
List<SysTenant> tenantList;
try {
tenantList = getTenantList();
} catch (Exception ex) {
log.error("获取租户列表失败", ex);
writeTenantExceptionResponse(response, "租户列表获取失败,请检查租户服务状态");
return false;
}
if (CollUtil.isEmpty(tenantList)) {
writeTenantExceptionResponse(response, "租户列表获取失败,请检查租户服务状态");
return false;
}
// 获取当前请求的租户ID
Long currentTenantId = TenantContextHolder.getTenantId();
// 检查租户是否存在且在有效期内
boolean exist = tenantList.stream()
.filter(tenant -> NumberUtil.equals(tenant.getId(), currentTenantId)) // 匹配租户ID
boolean exist = tenantList.stream().filter(tenant -> NumberUtil.equals(tenant.getId(), currentTenantId)) // 匹配租户ID
.filter(tenant -> tenant.getStartTime().isBefore(LocalDateTime.now())) // 检查开始时间:租户已生效
.anyMatch(tenant -> tenant.getEndTime().isAfter(LocalDateTime.now()));
@@ -139,10 +133,42 @@ public class TenantContextHolderFilter extends GenericFilterBean {
}
// 如果租户不存在或已失效,返回426状态码和错误信息
response.setStatus(HttpStatus.UPGRADE_REQUIRED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(JSONUtil.toJsonStr(R.failed(StrUtil.format("租户ID为{}的租户不存在或已过期失效", currentTenantId))));
writeTenantExceptionResponse(response, StrUtil.format("租户ID为{}的租户不存在或已过期失效", currentTenantId));
return false;
}
/**
* 获取租户列表,缓存不存在时从远程服务读取。
*
* @return 租户列表
*/
private List<SysTenant> getTenantList() {
Cache cache = cacheManager.getCache(CacheConstants.TENANT_DETAILS);
if (Objects.nonNull(cache)) {
List<SysTenant> tenantList = cache.get(SimpleKey.EMPTY, List.class);
if (Objects.nonNull(tenantList)) {
return tenantList;
}
}
R<List<SysTenant>> tenantListR = remoteTenantService.list();
return Objects.nonNull(tenantListR) ? tenantListR.getData() : null;
}
/**
* 写入租户异常响应。
*
* @param response 响应
* @param message 错误信息
*/
@SneakyThrows
private void writeTenantExceptionResponse(HttpServletResponse response, String message) {
if (response.isCommitted()) {
return;
}
response.setStatus(HttpStatus.UPGRADE_REQUIRED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(JSONUtil.toJsonStr(R.failed(message)));
}
}
@@ -16,15 +16,15 @@ import java.io.IOException;
*/
public class TenantRequestInterceptor implements ClientHttpRequestInterceptor {
/**
* 拦截HTTP请求,添加租户ID到请求头
*
* @param request HTTP请求对象
* @param body 请求体字节数组
* @param execution 请求执行器
* @return 客户端HTTP响应
* @throws IOException 当I/O操作失败时抛出
*/
/**
* 拦截HTTP请求,添加租户ID到请求头
*
* @param request HTTP请求对象
* @param body 请求体字节数组
* @param execution 请求执行器
* @return 客户端HTTP响应
* @throws IOException 当I/O操作失败时抛出
*/
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {