跳至主要内容

多租戶(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,可以在繼承類別中覆寫欄位定義。


相關資源