Image


🧱 多租戶系統的核心:Tenant Boundary(租戶邊界)

不論是「店」或「會員」,都必須有一個一致的「租戶識別」,且整個系統要從上到下都尊重這邊界

1
使用者 → Token → Context → DbContext / Repository → SQL

只要其中一層沒守好,就可能造成資料外洩想像你有「很多間分店」都使用同一套後台系統

  • A 店的會員不能看到 B 店的訂單
  • B 店的商品不能被 A 店誤改
  • C 店的營收絕不能被其他店家查到

👉 ShopId 就像房間鑰匙,沒有鑰匙就不能進別人的房間



🧱 後端層面防範策略(最重要的三層)

這三層是「後端防呆」的核心,只要其中一層沒做好,就可能讓使用者查到不是自己的資料

1️⃣ Token / 身分驗證層:登入時就綁 ShopId(第一道鎖)

當會員登入後,你發 JWT 時,「必須」把該會員的 ShopId 寫進 Token

1
2
3
4
5
{
"MemberId": "M123",
"ShopId": "A001",
"Role": "Member"
}

以後每次 API 呼叫,後端都要比對

1
2
if (user.ShopId != request.ShopId)
return Forbid();

📌 不允許客戶端在 request 裡手動帶 ShopId,因為只要能自己帶 ShopId,就等於能切換成別家店 → 超危險!


2️⃣ 資料查詢層:Global Query Filter 自動帶出 ShopId(第二道鎖)

使用 EF Core 時,最容易發生的就是開發者忘記寫 where ShopId,為避免人為失誤,可以用 Global Query Filter

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasQueryFilter(o => o.ShopId == _currentShopId);
}

所以

1
context.Orders.ToList();

實際 SQL 會變成

1
SELECT * FROM Orders WHERE ShopId = 'A001';

3️⃣ API 授權層:Claim + Policy(第三道鎖)

在 ASP.NET Core 的 Authorization 層加入 ShopId 驗證

1
2
3
4
5
6
7
8
9
10
options.AddPolicy("ShopScope", policy =>
{
policy.RequireClaim("ShopId");
});

[Authorize(Policy = "ShopScope")]
public async Task<IActionResult> GetOrders()
{
var shopId = User.FindFirst("ShopId").Value;
}

即使有人嘗試竄改 URL 或 Body 中的 ShopId,也會被攔下


🎨 前端與 API 層的安全策略(使用者看不到的防護)

層級 防範重點
API Gateway 驗證 JWT、強制綁定 ShopId,不讓亂帶
Header ShopId 不能 客戶端提供,由後端自動注入
URL 避免直接寫 /shop/A001/order,不給人亂猜
前端 UI 不顯示不屬於自己的資料,只提供授權內容

前端只能「顯示」資料,不負責安全。安全永遠在後端強制



🧍‍♂️ 會員隔離(Member Isolation)

租戶隔離是「店 vs 店」的隔離,而會員隔離則是同一個店裡的每個會員只能看到「自己的資料」
例如:訂單、點數、收藏列表 Order.MemberId == currentMemberId



📎 Coding Guideline(實作習慣)

✔ Service / Repository 不接受「外部傳入的 ShopId」

只能從 Token / Context 拿


✔ 每個 Entity 必須有 ShopId

1
2
3
4
5
public class Order 
{
public string Id { get; set; }
public string ShopId { get; set; }
}

✔ API 參數禁止明碼傳 ShopId

GET /shop/A001/orders => GET /orders 後端自動用 Token 取 ShopId。


✔ DbContext 必須存當前 ShopId(Tenant Context)

方便 Global Filter 使用



AB店

https://wiki.91app.com/pages/viewpage.action?pageId=169902529



coding guideline

https://docs.google.com/presentation/d/1F6YWQaue_ArsIfXXNZ7cy98TUb_61FfLCYhF0h-KGjc/edit?slide=id.g1f93364fe92_0_45#slide=id.g1f93364fe92_0_45