多租戶(Multi-Tenant)使用指南
實現 SaaS 應用的多租戶數據隔離
簡介
AppFuse Server 提供完整的多租戶支援,包括:
- TenantAwareEntity - 多租戶實體基類,自動處理租戶 ID
- TenantContext - 租戶上下文管理,從認證資訊解析租戶
- TenantFilterSupport - Hibernate Filter 工具,自動過濾查詢結果
- TenantAware - 租戶感知介面
核心特色
| 特色 | 說明 |
|---|---|
| 自動注入 | @PrePersist 自動設定 tenantId |
| 安全驗證 | @PreUpdate 防止跨租戶更新 |
| 查詢過濾 | Hibernate Filter 自動過濾查詢結果 |
| 彈性擴展 | 可搭配審計功能使用 |
快速開始
1. 繼承 TenantAwareEntity
import io.leandev.appfuse.jpa.tenant.TenantAwareEntity;
@Entity
@Table(name = "products")
public class Product extends TenantAwareEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
private String name;
private BigDecimal price;
// getters and setters
}
2. 配置 TenantContext
在應用程式啟動時配置租戶 ID 解析器:
@Configuration
public class TenantConfig {
@PostConstruct
public void configureTenantContext() {
// 使用預設配置(支援 JWT、OAuth2、Basic Auth)
TenantContext.configureWithDefaults();
// 或指定 JWT claim 名稱
// TenantContext.configureWithDefaults("tenant_id");
}
}
3. 啟用 Hibernate Filter
使用 AOP 在事務開始時啟用租戶過濾:
@Aspect
@Component
@Order(1)
public class TenantFilterAspect {
@PersistenceContext
private EntityManager entityManager;
@Before("@within(org.springframework.transaction.annotation.Transactional)")
public void enableTenantFilter(JoinPoint joinPoint) {
if (TenantContext.hasTenantContext()) {
String tenantId = TenantContext.getCurrentTenantId();
TenantFilterSupport.enableFilter(entityManager, tenantId);
}
}
}
常見場景
場景 1:多租戶 + 審計功能
大多數業務實體需要同時具備多租戶和審計功能:
// 建立應用層基類
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableTenantEntity extends TenantAwareEntity {
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@CreatedDate
@Column(name = "created_date", updatable = false)
private Instant createdDate;
@LastModifiedBy
@Column(name = "last_modified_by")
private String lastModifiedBy;
@LastModifiedDate
@Column(name = "last_modified_date")
private Instant lastModifiedDate;
// getters and setters
}
// 業務實體繼承應用層基類
@Entity
public class Customer extends AuditableTenantEntity {
// ...
}
場景 2:排程任務中設定租戶
排程任務沒有 HTTP 請求上下文,需手動設定租戶:
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReport() {
List<Tenant> tenants = tenantRepository.findAllActive();
for (Tenant tenant : tenants) {
TenantContext.runAs(tenant.getId(), () -> {
// 這裡的所有操作都在指定租戶上下文中執行
reportService.generateDailyReport();
});
}
}
場景 3:管理員跨租戶查詢
管理員需要查詢所有租戶的數據時,停用租戶過濾:
public List<Order> findAllOrdersAcrossTenants() {
// 停用租戶過濾
TenantFilterSupport.disableFilter(entityManager);
try {
return orderRepository.findAll();
} finally {
// 重新啟用(如果需要)
if (TenantContext.hasTenantContext()) {
TenantFilterSupport.enableFilter(
entityManager,
TenantContext.getCurrentTenantId()
);
}
}
}
場景 4:自訂租戶 ID 解析
如果預設的解析器不符合需求,可自訂:
TenantContext.configure(
CompositeTenantIdResolver.builder()
.withJwtClaim("organization_id") // 從 JWT 的 organization_id claim 取得
.withUserDetails() // 或從 UserDetails 取得
.build()
);
運作機制
整體架構
┌─────────────────────────────────────────────────────────────┐
│ HTTP Request (JWT 包含 tenantId claim) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TenantContext │
│ - 從 JWT / Authentication 解析 tenantId │
│ - ThreadLocal 儲存當前租戶 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TenantFilterAspect (應用層) │
│ - 攔截 @Transactional 方法 │
│ - 呼叫 TenantFilterSupport 啟用 Hibernate Filter │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Hibernate Filter │
│ - 自動在所有查詢加上 WHERE tenant_id = :tenantId │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TenantAwareEntity │
│ - @PrePersist: 自動注入 tenantId │
│ - @PreUpdate: 驗證 tenantId(防止跨租戶更新) │
└─────────────────────────────────────────────────────────────┘
Hibernate Filter 配置
TenantAwareEntity 包含以下 Filter 配置:
@MappedSuperclass
@FilterDef(
name = "tenantFilter",
parameters = @ParamDef(name = "tenantId", type = String.class)
)
@Filter(
name = "tenantFilter",
condition = "tenant_id = :tenantId"
)
public abstract class TenantAwareEntity implements TenantAware {
// ...
}
| 註解 | 說明 |
|---|---|
@FilterDef | 定義 Filter 名稱和參數 |
@Filter | 指定 SQL WHERE 條件 |
安全性保護
| 操作 | 保護機制 |
|---|---|
| 新增(INSERT) | @PrePersist 自動注入 tenantId |
| 查詢(SELECT) | Hibernate Filter 自動過濾 |
| 更新(UPDATE) | @PreUpdate 驗證 tenantId 一致性 |
| 刪除(DELETE) | @PreUpdate 驗證(刪除前會觸發) |
進階配置
自訂 Filter 名稱
如果需要使用不同的 Filter 名稱:
TenantFilterSupport.enableFilter(
entityManager,
tenantId,
"customTenantFilter", // 自訂 filter 名稱
"organizationId" // 自訂參數名稱
);
複合租戶策略
對於需要多層租戶隔離的場景(如:組織 > 部門 > 團隊):
// 可定義多個 Filter
@FilterDef(name = "orgFilter", parameters = @ParamDef(name = "orgId", type = String.class))
@FilterDef(name = "deptFilter", parameters = @ParamDef(name = "deptId", type = String.class))
@Filter(name = "orgFilter", condition = "organization_id = :orgId")
@Filter(name = "deptFilter", condition = "department_id = :deptId")
public abstract class HierarchicalTenantEntity {
// ...
}
常見問題
Q: Filter 只影響查詢嗎?
是的,Hibernate Filter 只影響 SELECT 操作。INSERT、UPDATE、DELETE 由 @PrePersist 和 @PreUpdate 保護。
Q: 為什麼用 AOP 而不是 Interceptor 啟用 Filter?
HandlerInterceptor 在 HTTP 請求開始時執行,此時 Hibernate Session 可能尚未綁定。AOP 在 @Transactional 方法開始後執行,確保使用正確的 Session。
Q: 如何測試多租戶功能?
@BeforeEach
void setUp() {
TenantContext.setCurrentTenantId("test-tenant-001");
}
@AfterEach
void tearDown() {
TenantContext.clear();
}
@Test
void shouldIsolateDataByTenant() {
// 測試代碼
}
Q: tenantId 欄位長度為什麼是 36?
36 是 UUID 的標準長度(含連字符)。如果使用其他格式的租戶 ID,可以在繼承類別中覆寫欄位定義。
相關資源
- API 參考: API 參考: Tenant
- Javadoc: Javadoc