From 0fca2924d4d97a8d972ade653ca3e31bb6fbb0f4 Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Thu, 21 May 2026 10:39:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor(pigx-common-data):=20=E8=A7=A3?= =?UTF-8?q?=E8=80=A6=20pigx-common-data=20=E5=AF=B9=20pigx-common-security?= =?UTF-8?q?=20=E7=9A=84=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/cache/RedisAutoCacheManager.java | 6 +- .../cache/RedisCacheAutoConfiguration.java | 28 +-- .../cache/RedisCacheManagerConfiguration.java | 6 +- .../cache/RedisTemplateConfiguration.java | 6 +- .../datascope/DataScopeInnerInterceptor.java | 182 +++--------------- .../datascope/PigxDefaultDataScopeHandle.java | 43 ++--- .../mybatis/MybatisPlusConfiguration.java | 34 ++-- .../common/data/resolver/ParamResolver.java | 66 ++++++- .../resolver/SqlFilterArgumentResolver.java | 27 ++- .../common/data/tenant/PigxTenantHandler.java | 103 +++++----- .../tenant/TenantContextHolderFilter.java | 96 +++++---- .../data/tenant/TenantRequestInterceptor.java | 18 +- 12 files changed, 275 insertions(+), 340 deletions(-) diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisAutoCacheManager.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisAutoCacheManager.java index 43e32726a..abdadffee 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisAutoCacheManager.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisAutoCacheManager.java @@ -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 initialCacheConfigurations) { + boolean allowInFlightCacheCreation, Map initialCacheConfigurations) { super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheConfigurations); } - @Override protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) { if (StrUtil.isBlank(name) || !name.contains(SPLIT_FLAG)) { diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisCacheAutoConfiguration.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisCacheAutoConfiguration.java index 3027ded3a..b6a32782d 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisCacheAutoConfiguration.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisCacheAutoConfiguration.java @@ -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 connectionFactoryObjectProvider) { + public CacheManager redisCacheManager(ObjectProvider 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 cacheNames = this.cacheProperties.getCacheNames(); Map 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 jsonRedisSerializer = RedisSerializer.json(); - ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(jsonRedisSerializer, "mapper"); - objectMapper.registerModules(new PigxJavaTimeModule()); + ObjectMapper redisObjectMapper = objectMapper.copy(); + RedisSerializer jsonRedisSerializer = org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.builder() + .objectMapper(redisObjectMapper) + .defaultTyping(true) + .build(); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(jsonRedisSerializer)); if (redisProperties.getTimeToLive() != null) { diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisCacheManagerConfiguration.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisCacheManagerConfiguration.java index 7a302c605..ab05e09bb 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisCacheManagerConfiguration.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisCacheManagerConfiguration.java @@ -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; diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisTemplateConfiguration.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisTemplateConfiguration.java index f77376161..1ee01a768 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisTemplateConfiguration.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/cache/RedisTemplateConfiguration.java @@ -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 diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/datascope/DataScopeInnerInterceptor.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/datascope/DataScopeInnerInterceptor.java index dbb3f51b2..74f2e8d3e 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/datascope/DataScopeInnerInterceptor.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/datascope/DataScopeInnerInterceptor.java @@ -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; /** - * 数据权限内部拦截器 - *

- * 参照 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 配置构建数据权限条件表达式 - *

- * 使用 {@code CCJSqlParserUtil.parseCondExpression()} 解析条件字符串为 Expression 对象, - * 与 MyBatis Plus DataPermissionInterceptor 保持一致的实现方式。 - * - * @param plainSelect 解析后的 PlainSelect 对象 - * @param dataScope 数据权限参数 - * @return 数据权限条件表达式,无需处理时返回 null - */ - private Expression buildScopeExpression(PlainSelect plainSelect, DataScope dataScope) - throws JSQLParserException { List 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 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; + } } diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/datascope/PigxDefaultDataScopeHandle.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/datascope/PigxDefaultDataScopeHandle.java index 6ad0b1d4e..b8957d89f 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/datascope/PigxDefaultDataScopeHandle.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/datascope/PigxDefaultDataScopeHandle.java @@ -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 - *

- * 默认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 roleIdList = user.getAuthorities() - .stream() - .map(GrantedAuthority::getAuthority) - .filter(authority -> authority.startsWith(SecurityConstants.ROLE)) - .map(authority -> authority.split(StrUtil.UNDERLINE)[1]) - .toList(); + List roleIdList = user.getRoleIds(); if (CollUtil.isEmpty(roleIdList)) { return false; } // 获取角色列表 - List roleList = RetOps.of(dataScopeService.getRoleList(roleIdList)) + List 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 roleList) { + private boolean processDataScope(UserContext user, DataScope dataScope, List roleList) { List deptList = dataScope.getDeptList(); for (SysRole role : roleList) { @@ -134,7 +119,7 @@ public class PigxDefaultDataScopeHandle implements DataScopeHandle { /** * 处理本级及下级数据权限 */ - private void handleOwnChildLevelScope(PigxUser user, List deptList) { + private void handleOwnChildLevelScope(UserContext user, List deptList) { List 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 deptList) { + private void handleOwnLevelScope(UserContext user, List deptList) { deptList.add(user.getDeptId()); } /** * 处理个人数据权限 */ - private void handleSelfLevelScope(PigxUser user, DataScope dataScope) { + private void handleSelfLevelScope(UserContext user, DataScope dataScope) { dataScope.setUsername(user.getUsername()); } diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/mybatis/MybatisPlusConfiguration.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/mybatis/MybatisPlusConfiguration.java index bd0d9d61e..852be5330 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/mybatis/MybatisPlusConfiguration.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/mybatis/MybatisPlusConfiguration.java @@ -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 拦截器配置 + *

+ * 数据权限拦截器 {@link DataScopeInterceptor} 改为可选注入: + * 未提供 {@link com.pig4cloud.pigx.common.core.context.UserContextHolder} 的场景下不装配, + * 同时记录 WARN 日志,租户/分页/乐观锁等其它能力仍正常工作。 * - * @return PigxDefaultDatascopeHandle + * @return MybatisPlusInterceptor */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(TenantLineInnerInterceptor tenantLineInnerInterceptor, - DataScopeInterceptor dataScopeInterceptor) { + ObjectProvider 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; } diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/resolver/ParamResolver.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/resolver/ParamResolver.java index eaefbe093..d0cdf29bb 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/resolver/ParamResolver.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/resolver/ParamResolver.java @@ -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 */ - public Map getMap(String... key) { + public Map 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 result = new LinkedHashMap<>(); + List 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 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); + } + } diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/resolver/SqlFilterArgumentResolver.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/resolver/SqlFilterArgumentResolver.java index f80835f04..9cd531a28 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/resolver/SqlFilterArgumentResolver.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/resolver/SqlFilterArgumentResolver.java @@ -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对象 *

@@ -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 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; diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/PigxTenantHandler.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/PigxTenantHandler.java index 2915a8d3d..0dd945931 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/PigxTenantHandler.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/PigxTenantHandler.java @@ -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 值 - *

- * - * @return 租户 ID 值表达式 - */ - @Override - public Expression getTenantId() { - Long tenantId = TenantContextHolder.getTenantId(); - log.debug("当前租户为 >> {}", tenantId); + /** + * 获取租户 ID 值表达式,只支持单个 ID 值 + *

+ * @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(); + } - /** - * 根据表名判断是否忽略拼接多租户条件 - *

- * 默认都要进行解析并拼接多租户条件 - * - * @param tableName 表名 - * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件 - */ - @Override - public boolean ignoreTable(String tableName) { - // 判断是否跳过当前查询的租户过滤 - if (TenantContextHolder.getTenantSkip()) { - return Boolean.TRUE; - } + /** + * 根据表名判断是否忽略拼接多租户条件 + *

+ * 默认都要进行解析并拼接多租户条件 + * @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; + } } diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/TenantContextHolderFilter.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/TenantContextHolderFilter.java index ee70dc595..feaa8665f 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/TenantContextHolderFilter.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/TenantContextHolderFilter.java @@ -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 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> tenantListR = remoteTenantService.list(); - tenantList = tenantListR.getData(); + List 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 getTenantList() { + Cache cache = cacheManager.getCache(CacheConstants.TENANT_DETAILS); + if (Objects.nonNull(cache)) { + List tenantList = cache.get(SimpleKey.EMPTY, List.class); + if (Objects.nonNull(tenantList)) { + return tenantList; + } + } + + R> 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))); + } + } diff --git a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/TenantRequestInterceptor.java b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/TenantRequestInterceptor.java index 62a8de81b..329bd8158 100644 --- a/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/TenantRequestInterceptor.java +++ b/pigx-common/pigx-common-data/src/main/java/com/pig4cloud/pigx/common/data/tenant/TenantRequestInterceptor.java @@ -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 {