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
相关产品推荐
相关产品推荐

