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