跳至主要内容

Tenant API

Tenant 模組提供多租戶支援,包含租戶上下文管理和租戶 ID 解析。

套件: io.leandev.appfuse.security.tenant

核心類別

TenantContext

租戶上下文管理。

public class TenantContext {
// 配置方式
static void configure(TenantIdResolver tenantIdResolver)
static void configureWithDefaults()
static void configureWithDefaults(String claimName)

// 取得租戶 ID
static String getCurrentTenantId()
static String getTenantIdOrNull()
static boolean hasTenantContext()

// 手動設定(測試/排程)
static void setCurrentTenantId(String tenantId)
static void clear()

// 在租戶上下文中執行
static void runAs(String tenantId, Runnable action)
static <T> T callAs(String tenantId, Callable<T> action) throws Exception
static <T> T supplyAs(String tenantId, Supplier<T> supplier)
}

TenantAwareUserDetails

租戶感知的 UserDetails 介面。

public interface TenantAwareUserDetails extends UserDetails {
String getTenantId();
}

租戶 ID 解析器

TenantIdResolver

租戶 ID 解析介面。

public interface TenantIdResolver {
String resolve();
}

內建解析器

解析器說明
JwtClaimTenantIdResolver從 JWT Claim 解析
JwtDetailsTenantIdResolver從 JWT Details 解析
UserDetailsTenantIdResolver從 UserDetails 解析
CompositeTenantIdResolver組合多個解析器

使用範例

配置 TenantContext

@Configuration
public class TenantConfig {

@Bean
public TenantIdResolver tenantIdResolver() {
// 使用預設配置(從 JWT 的 tenantId claim 解析)
TenantContext.configureWithDefaults();

// 或自訂 claim 名稱
TenantContext.configureWithDefaults("tenant_id");

// 或使用組合解析器
return new CompositeTenantIdResolver(
new JwtClaimTenantIdResolver("tenantId"),
new UserDetailsTenantIdResolver()
);
}
}

取得當前租戶 ID

@Service
public class OrderService {

public List<Order> findAll() {
String tenantId = TenantContext.getCurrentTenantId();
return orderRepository.findByTenantId(tenantId);
}

public Order save(Order order) {
// 自動設定租戶 ID
order.setTenantId(TenantContext.getCurrentTenantId());
return orderRepository.save(order);
}
}

在特定租戶上下文執行

// 排程任務中使用
@Scheduled(cron = "0 0 * * * *")
public void processAllTenants() {
List<String> tenantIds = tenantService.getAllTenantIds();

for (String tenantId : tenantIds) {
TenantContext.runAs(tenantId, () -> {
// 在該租戶上下文中執行
processOrders();
});
}
}

// 或使用 callAs(有回傳值)
public int countOrdersForTenant(String tenantId) {
return TenantContext.callAs(tenantId, () -> {
return orderRepository.count();
});
}

// 或使用 supplyAs
public List<Order> getOrdersForTenant(String tenantId) {
return TenantContext.supplyAs(tenantId, () -> {
return orderRepository.findAll();
});
}

實作 TenantAwareUserDetails

public class CustomUserDetails implements TenantAwareUserDetails {
private final User user;

public CustomUserDetails(User user) {
this.user = user;
}

@Override
public String getTenantId() {
return user.getTenantId();
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}

// 其他 UserDetails 方法實作...
}

租戶感知 Repository

public interface OrderRepository extends JpaRepository<Order, Long> {

@Query("SELECT o FROM Order o WHERE o.tenantId = :tenantId")
List<Order> findByTenantId(@Param("tenantId") String tenantId);

// 使用 SpEL 自動注入租戶 ID
@Query("SELECT o FROM Order o WHERE o.tenantId = ?#{T(io.leandev.appfuse.security.tenant.TenantContext).getCurrentTenantId()}")
List<Order> findAllForCurrentTenant();
}

過濾器

TenantContextCleanupFilter

租戶上下文清理過濾器,確保請求結束後清理上下文。

@Bean
public TenantContextCleanupFilter tenantContextCleanupFilter() {
return new TenantContextCleanupFilter();
}

@Bean
public FilterRegistrationBean<TenantContextCleanupFilter> tenantFilterRegistration(
TenantContextCleanupFilter filter) {
FilterRegistrationBean<TenantContextCleanupFilter> registration =
new FilterRegistrationBean<>(filter);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}

JPA 整合

TenantAwareEntity

租戶感知實體基類。

@MappedSuperclass
public abstract class TenantAwareEntity {
@Column(name = "tenant_id", nullable = false, updatable = false)
private String tenantId;

@PrePersist
protected void prePersist() {
if (tenantId == null) {
tenantId = TenantContext.getCurrentTenantId();
}
}

// getter/setter
}

使用範例

@Entity
@Table(name = "orders")
public class Order extends TenantAwareEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String orderNumber;
private BigDecimal amount;

// getter/setter
}

最佳實踐

  1. 一律使用 TenantContext - 避免手動傳遞租戶 ID
  2. 配置清理過濾器 - 確保請求結束後清理上下文
  3. 繼承 TenantAwareEntity - 自動設定租戶 ID
  4. 排程任務使用 runAs - 在正確的租戶上下文中執行
  5. 測試時手動設定 - 使用 setCurrentTenantId() 設定測試租戶

相關連結