You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot+Hibernate多租户:超级管理员读写租户ID分离需求咨询

解决方案:超级管理员读写操作动态切换租户ID

完全可以实现超级管理员读操作使用ALL租户编码、增删改操作使用指定租户编码的需求,核心思路是根据操作类型(CRUD)动态切换租户标识,结合请求上下文和Hibernate的租户解析器完成适配。以下是具体实现步骤:


1. 扩展租户上下文,增加操作类型标识

首先在TenantContext中添加操作类型枚举,用于标记当前请求是读还是写操作:

public enum OperationType {
    READ, WRITE
}

public class TenantContext {
    private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
    private static final ThreadLocal<OperationType> currentOperation = new ThreadLocal<>();

    public static void setCurrentTenant(String tenantCode) {
        currentTenant.set(tenantCode);
    }

    public static String getCurrentTenant() {
        return currentTenant.get();
    }

    public static void setCurrentOperation(OperationType operationType) {
        currentOperation.set(operationType);
    }

    public static OperationType getCurrentOperation() {
        return currentOperation.get();
    }

    public static void clear() {
        currentTenant.remove();
        currentOperation.remove();
    }
}

2. 修改租户过滤器,按操作类型设置租户ID

在TenantFilter中根据HTTP请求方法判断操作类型,为超级管理员动态分配租户编码:

@Component
public class TenantFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            // 标记操作类型:GET/HEAD为读,POST/PUT/DELETE/PATCH为写
            OperationType operationType = switch (request.getMethod().toUpperCase()) {
                case "GET", "HEAD" -> OperationType.READ;
                case "POST", "PUT", "DELETE", "PATCH" -> OperationType.WRITE;
                default -> OperationType.READ;
            };
            TenantContext.setCurrentOperation(operationType);

            // 获取当前用户权限(从Spring Security上下文获取)
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth != null && auth.getPrincipal() instanceof UserDetails userDetails) {
                boolean isSuperAdmin = userDetails.getAuthorities().stream()
                        .anyMatch(a -> a.getAuthority().equals("ROLE_SUPER_ADMIN"));

                String targetTenant = request.getHeader("X-Tenant-Code");
                if (isSuperAdmin) {
                    if (operationType == OperationType.READ) {
                        // 读操作使用ALL租户
                        TenantContext.setCurrentTenant("ALL");
                    } else {
                        // 写操作必须传递目标租户编码,此处可添加非空校验
                        Assert.notNull(targetTenant, "X-Tenant-Code header is required for write operations");
                        TenantContext.setCurrentTenant(targetTenant);
                    }
                } else {
                    // 普通用户固定使用请求头租户编码
                    Assert.notNull(targetTenant, "X-Tenant-Code header is required");
                    TenantContext.setCurrentTenant(targetTenant);
                }
            }

            filterChain.doFilter(request, response);
        } finally {
            // 清理线程上下文,避免内存泄漏
            TenantContext.clear();
        }
    }
}

3. 适配Hibernate租户解析器

修改CurrentTenantIdentifierResolver,直接从TenantContext获取动态设置的租户编码:

@Component
public class CustomTenantIdentifierResolver implements CurrentTenantIdentifierResolver {
    @Override
    public String resolveCurrentTenantIdentifier() {
        // 兜底处理,防止租户编码为空
        return Optional.ofNullable(TenantContext.getCurrentTenant()).orElse("ALL");
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

4. 修复AuditorAware的租户匹配问题

超级管理员写操作时,查询自身用户信息需要强制使用ALL租户,避免因租户不匹配导致createBy为空:

@Component
public class CustomAuditorAware implements AuditorAware<User> {
    private final UserRepository userRepository;

    public CustomAuditorAware(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public Optional<User> getCurrentAuditor() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            return Optional.empty();
        }

        String username = auth.getName();
        boolean isSuperAdmin = auth.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_SUPER_ADMIN"));

        if (isSuperAdmin) {
            // 超级管理员查询自身时,强制指定ALL租户
            return userRepository.findByUsernameAndTenantCode(username, "ALL");
        } else {
            // 普通用户使用当前租户查询
            return userRepository.findByUsername(username);
        }
    }
}

对应的UserRepository需添加专用查询方法:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    Optional<User> findByUsernameAndTenantCode(String username, String tenantCode);
}

5. 确认Hibernate多租户配置

确保共享架构(鉴别列)的配置正确生效:

@Configuration
public class HibernateTenantConfig implements HibernatePropertiesCustomizer {
    private final CustomTenantIdentifierResolver tenantResolver;

    public HibernateTenantConfig(CustomTenantIdentifierResolver tenantResolver) {
        this.tenantResolver = tenantResolver;
    }

    @Override
    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put(AvailableSettings.MULTI_TENANT, MultiTenancyStrategy.DISCRIMINATOR);
        hibernateProperties.put(AvailableSettings.MULTI_TENANT_DISCRIMINATOR_COLUMN_NAME, "tenant_code");
        hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantResolver);
    }
}

关键说明

  • 写操作强制要求超级管理员传递X-Tenant-Code请求头,确保数据存入目标租户
  • 线程上下文在过滤器finally块中清理,避免线程复用导致的租户信息污染
  • AuditorAware的特殊处理保证超级管理员写操作时能正常查询到自身用户,满足createBy非空约束

内容的提问来源于stack exchange,提问作者Igor Milovanovic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 02:10:35