iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Modern Web

Laravel Filament 從入門到實戰系列 第 18 篇

Day 17:多租戶隔離,讓每個經銷據點只看到自己的資料

  • 分享至 

  • xImage
  •  

Day 16:設計一份貼近真實團隊的角色權限矩陣 結尾留下一個新的落差。角色矩陣解決的是「這個角色能不能做這件事」,但如果這間批發商同時服務多個經銷據點,兩個同樣是業務角色的人,一個在台北據點、一個在高雄據點,此刻打開訂單列表,看到的仍是完全相同的一份資料。

矩陣完全沒有處理「這個角色只能看到自己據點的資料」這一層範圍限制。

同一份訂單列表,兩個據點的業務都看得到彼此的客戶

具體攤開這個問題會更清楚。台北據點的業務小陳,跟高雄據點的業務小林,都是業務角色,依 Day 16 建立的矩陣,兩人在訂單模組上擁有一模一樣的操作權限,都能檢視、建立、編輯訂單。問題出在小陳打開訂單列表時,畫面上除了自己談成的訂單,還混著小林經手的高雄客戶資料,客戶名稱、聯絡電話、訂單金額,一覽無遺。

這在業務常理上站不住腳。高雄據點的業務理應只需要、也只該看到自己經手的客戶跟訂單,看到台北據點的客戶資料,不只是無關的畫面干擾,更是不該發生的資料外洩。實務上,這種同一品牌不同據點的經銷關係,彼此可能是分別營運、甚至存在競爭關係的合作夥伴,小陳如果能看到高雄某個大客戶的訂單金額與往來紀錄,對高雄據點來說就是一次不該發生的資訊洩漏。

今天的任務,是正式定案並實作一套機制,讓後台依登入使用者所屬的據點,自動限制看得到的資料範圍,不需要在角色矩陣的每一格判斷邏輯裡,額外塞進一句「而且要是同一個據點」這種重複的補丁。

定案多租戶隔離,跟角色矩陣是兩層不同的問題

這套機制有個正式名稱,今天要把它講清楚:多租戶隔離。指的是讓同一套後台依登入使用者所屬的組織或商店,只看得到自己範圍內的資料。

台北據點看不到高雄的客戶跟訂單,高雄據點也看不到台北的,這件事今天之後就是後台的預設行為,不需要每次新增功能都回頭再三確認一次。

多租戶隔離跟 Day 16 的角色矩陣,是兩層性質完全不同的機制,容易被誤以為是同一件事,實際上疊加運作、互不取代。

角色矩陣回答的是「這個人的身分能不能做這件事」,屬於操作權限層面,一個業務角色能不能建立訂單,是矩陣要回答的問題。

多租戶隔離回答的是「這個人看得到的資料範圍有多大」,屬於資料範圍層面,這個業務只能替自己據點的客戶建立訂單,看不到別的據點的客戶,是隔離要回答的問題。兩者各自獨立存在,同時作用在同一次操作上,一個人的角色權限再高,也不代表能看到別的據點的資料,反過來,就算資料範圍被隔離得再乾淨,角色矩陣該擋下的操作一樣要擋。可以想成角色矩陣決定這個人能不能開門,多租戶隔離決定這道門後面的房間屬於哪一棟大樓,兩者是不同維度的限制,一個管的是動作,一個管的是空間。

本篇示範情境裡,「租戶」對應的具體單位,就是這間批發商各地的經銷據點。每一個經銷據點可以視為一個獨立的資料範圍邊界,客戶、訂單這幾個模組的每一筆資料,理應都歸屬於某一個具體的據點,而不是懸空存在。往後系列提到讓資料依組織範圍隔離,指的都是這套機制,「經銷據點」也會是接下來反覆出現的具體名詞。

有開發經驗的讀者可能已經想到,這聽起來不難,不就是在資料表加一個記錄所屬據點的欄位,查詢時多加一個條件嗎。方向是對的,但這句話裡藏著一個容易被忽略的陷阱,留到下一段拆開講。

動手隔離,讓每一次查詢都自動只看自己據點的資料

先處理資料歸屬。我們將客戶跟訂單這兩個模組,各自補上一個 branch_id 欄位,記錄這筆資料歸屬於哪一個經銷據點,對應一張新的據點資料表。既有資料需要先跑一次資料修正,把台北據點跟高雄據點原本混在一起的客戶跟訂單,依實際歸屬回填正確的 branch_id,這一步做得不確實,後面的隔離邏輯再嚴謹也沒有意義。

接下來是關鍵的第二步,也是前面留的陷阱。如果做法是在每一個查詢客戶、訂單的地方,手動加一句 where('branch_id', $currentBranchId),這件事會散落在 Resource 的列表查詢、表單的下拉選單、報表的統計語句裡,任何一處忘記加,就是一次資料外洩。這正是 Day 15 建立 Policy 整合時處理過的同一種問題,只是換了一個層面,判斷邏輯不能散落在各處各自維護,必須集中在一個地方自動生效。

Laravel 既有的 Global Scope 機制,正好是集中管理查詢條件的地方。在 Order 模型上定義一個 BranchScope:

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class BranchScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (auth()->check()) {
            $builder->where('branch_id', auth()->user()->branch_id);
        }
    }
}

再讓 Order 模型在啟動時掛上這個 Scope:

namespace App\Models;

use App\Models\Scopes\BranchScope;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new BranchScope());
    }
}

Customer 模型用同樣的方式掛上 BranchScope。掛上之後,任何地方對 Order::query() 或 Customer::query() 發出的查詢,不管是 Filament Resource 的列表、表單的關聯下拉選單,還是未來可能新增的統計語句,都會自動被限制在目前登入使用者的據點範圍內,Resource 本身不需要多寫一行條件判斷。這跟 Day 15 建立 Policy 整合時「判斷邏輯集中管理」的精神一致,只是這次集中管理的對象從「能不能操作」換成「查得到多少資料」,任何未來新增的模組,只要補上 branch_id 欄位並掛上同一套 Scope,就能自動享有這層隔離,不需要每個開發者各自記得手動加條件。

訂單明細不需要另外處理。訂單明細本身不記錄所屬據點,因為它依附在訂單之下,只要訂單已經被 BranchScope 限制在正確的範圍內,透過訂單關聯取出的訂單明細,自然跟著被限制在同一個範圍,不需要重複設計一套一樣的邏輯。

Global Scope 掛載後的資料流向示意圖

驗證隔離成果,換上不同據點的帳號看看訂單列表

實作完成後,準備兩組帳號,一組 branch_id 指向台北據點,一組指向高雄據點,角色都設成業務,確保這次驗證看到的差異純粹來自據點隔離,不是角色矩陣造成的。

用台北據點的業務帳號登入,打開訂單列表,畫面上只剩下台北據點的訂單,原本混在一起的高雄訂單消失了。切到客戶列表,同樣只看得到台北據點的客戶。登出換成高雄據點的業務帳號,訂單列表換成一份完全不同的清單,只有高雄據點的訂單,客戶列表也只剩高雄據點的客戶,兩邊互不重疊。

這一刻值得停下來說清楚它的意義。這是全系列第一次,讓後台依登入者所屬的組織範圍,呈現出真正不同的資料樣貌,不再是所有據點的人打開畫面都看到同一份全域資料。這正好兌現了這個階段一開始設定的目標,讓後台依組織範圍隔離資料,今天算是把它具體做出來、也親眼驗證過了。

範圍隔開了,但跟角色矩陣疊在一起還沒驗證過

多租戶隔離今天正式定案並實作完成,客戶、訂單、訂單明細這幾個模組,現在會依登入使用者所屬的經銷據點,自動限制查詢範圍,不同據點的人看到的資料確實不一樣了。

但有個新的落差得誠實點出來。角色矩陣跟多租戶隔離這兩層機制,現在都各自存在,各自也都驗證過能正常運作,可是兩者疊加在一起運作時是否真的正確,還沒有人實際確認過。舉例來說,高雄據點的倉管角色實際操作一次,結果會不會意外看到不屬於自己據點的資料,或者反過來,被據點隔離連帶擋掉本來角色矩陣該賦予的權限,這件事今天完全沒有測試過。

權限與租戶疊加時,驗證兩者不互相打架,是接下來要處理的內容。


上一篇
Day 16:設計一份貼近真實團隊的角色權限矩陣
下一篇
Day 18:權限與租戶疊加時,驗證兩者不互相打架
系列文
Laravel Filament 從入門到實戰 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言