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
}
最佳實踐
- 一律使用 TenantContext - 避免手動傳遞租戶 ID
- 配置清理過濾器 - 確保請求結束後清理上下文
- 繼承 TenantAwareEntity - 自動設定租戶 ID
- 排程任務使用 runAs - 在正確的租戶上下文中執行
- 測試時手動設定 - 使用
setCurrentTenantId()設定測試租戶