建立多租戶 SaaS 應用程式:從設計到實作的完整指南
像 Notion、Slack 或 Figma 這樣的應用程式是怎麼打造的?這些多租戶 SaaS 應用程式看起來簡單易用,但自己動手做?那又是另一回事了。
當我第一次想要打造這種複雜的系統時,腦袋直接爆炸:
- 使用者需要多種登入選項(Email、Google、GitHub)
- 每個使用者可以建立並屬於多個組織 (Organizations)
- 每個組織內有不同的權限等級
- 企業組織需要特定網域自動加入
- 敏感操作需要 MFA
- 還有更多...
「老闆,產品設計兩週後再談,我現在卡住了。」
但當我真的開始動手時,我發現其實沒那麼可怕。
我只花了很少的力氣,就打造出一套具備所有這些功能的系統!



我會帶你從零開始設計並實作這樣的系統——你會驚訝於 2025 年用現代工具和正確架構方法,這一切其實很簡單。
完整原始碼已開源於這個 Github Repo。讓我們開始吧!
我們將以一個 AI 文件 SaaS 產品 DocuMind 為例。
DocuMind 是一款以多租戶模型設計的 AI 文件 SaaS 產品,支援個人、小型企業與大型企業。
這個平台為組織內的文件管理提供強大的 AI 能力,包括自動摘要產生、重點提取與智慧內容推薦。
SaaS 驗證 (Authentication) 與授權 (Authorization) 需要哪些功能?
首先,讓我們回顧一下必要需求。你需要哪些功能?
多租戶架構
要實現多租戶架構,你需要一個稱為 組織 (Organization) 的實體層。想像有一個共用的使用者池,這些使用者可以存取多個工作區。每個組織代表一個工作區,使用者只需一組身分即可根據分配的角色存取不同工作區(組織)。
這是驗證 (Authentication) 服務商常見的功能。身分管理系統中的組織對應到你的 SaaS 應用程式的工作區、專案或租戶。

成員資格 (Membership)
成員是一個暫時性的概念,用來表示某個身分在組織中的成員狀態。
例如,Sarah 用她的 email sarah@gmail.com 註冊你的應用程式。她可以屬於不同的工作區。如果 Sarah 屬於 Workspace A,但不屬於 Workspace B,那她就是 Workspace A 的成員,而不是 Workspace B 的成員。
角色 (Role) 與權限 (Permission) 設計
在多租戶架構下,使用者需要有特定 角色 (Role) 與 權限 (Permission) 才能存取租戶資源。
權限是細緻的存取控制,定義了具體的操作,例如 read: order 或 write: order,決定能對特定資源執行哪些動作。
角色則是一組權限,分配給多租戶環境中的成員。
你需要定義這些角色與權限,然後將角色分配給使用者,有時還會包含自動化流程。例如:
- 加入組織的使用者自動獲得 member 角色。
- 第一位建立工作區的使用者自動獲得 admin 角色。
註冊與登入流程
確保註冊與驗證 (Authentication) 流程既友善又安全,包括基本的登入與註冊選項:
- Email 與密碼登入:傳統的 email / 密碼登入方式。
- 無密碼登入:使用 email 驗證碼,簡單又安全。
- 帳號管理:帳號中心,讓使用者更新 email、密碼等資訊。
- 社交登入:如 Google、GitHub 等快速登入選項。
- 多重要素驗證 (MFA):支援如 Duo 等驗證器 app 增強安全性。
租戶建立與邀請
在多租戶 SaaS 應用程式中,使用者流程的一大差異是需要支援租戶建立與成員邀請。這個流程對產品啟動與成長至關重要,需要仔細規劃與實作。
以下是你需要考慮的典型使用流程:
| 使用者類型 | 進入點 |
|---|---|
| 新帳號 | 從登入 / 註冊頁建立新租戶 |
| 已有帳號 | 在產品內建立另一個租戶 |
| 已有帳號收到新租戶邀請 | 從登入 / 註冊頁進入 |
| 已有帳號收到新租戶邀請 | 從邀請 email 進入 |
| 新帳號收到新租戶邀請 | 從登入 / 註冊頁進入 |
| 新帳號收到新租戶邀請 | 從邀請 email 進入 |
這些都是幾乎每個 SaaS 應用程式都會遇到的常見場景。你可以參考這些流程,啟發你的產品與設計團隊,也可以根據需求自訂流程。






技術架構與系統設計
了解所有產品需求後,讓我們進入實作階段。
定義驗證 (Authentication) 策略
驗證 (Authentication) 看起來很可怕。使用者需要:
- Email / 密碼註冊 / 登入
- Google / Github 一鍵登入
- 忘記密碼時重設
- 企業客戶團隊級登入
- ...
光是這些基本功能就可能要開發好幾週。
但現在,我們完全不用自己實作這些!
現代驗證 (Authentication) 服務商(這次我選擇 Logto)已經把這些功能都包裝好了。驗證流程非常直觀:
從數週開發縮短到 15 分鐘設定,Logto 幫我們處理所有複雜流程!後面實作章節會介紹整合步驟。現在我們可以專注於 DocuMind 核心功能!
建立多租戶架構
組織系統讓使用者可以建立並加入多個組織。來看看核心關係:
在這個系統中,每個使用者可以屬於多個組織,每個組織也可以有多個成員。
在多租戶應用中啟用存取控制
基於角色的存取控制 (RBAC, Role-Based Access Control) 對於多租戶 SaaS 應用的安全性與可擴展性至關重要。
在多租戶應用中,權限與角色設計通常是一致的,這來自於產品設計。例如多個工作區通常會有 admin 角色與 member 角色。Logto 作為驗證 (Authentication) 服務商,提供以下組織層級的 RBAC 設計:
- 統一權限定義:權限在系統層級定義,所有組織一致適用,確保權限管理可維護且一致
- 組織模板:透過組織模板預設角色與權限組合,簡化組織初始化
權限關係如下:
由於每個使用者在每個組織中都需要自己的角色,角色與組織的關係必須能反映每個使用者被分配的角色:
我們已經設計好組織系統與存取控制系統,現在可以開始打造產品!
技術選型
我選擇了新手友善、可攜式的技術組合:
- 前端:React(也很容易轉換到 Vue / Angular / Svelte)
- 後端:Express(簡單直觀的 API)
為什麼要前後端分離?因為架構清晰、易學易換技術棧。驗證 (Authentication) 服務商以 Logto 為例。
而且以下教學的模式適用於:任何前端、任何後端、任何驗證 (Authentication) 系統。
為你的應用程式新增基本驗證 (Authentication) 流程
這是最簡單的一步。我們只需將 Logto 整合進專案,然後在 Logto Console 根據需求設定登入 / 註冊方式。
安裝 Logto 到你的應用程式
首先登入 Logto Cloud。沒有帳號可以免費註冊。建立一個 Development Tenant 進行測試。
在 Tenant Console 點擊左側「應用程式」按鈕,選擇 React 開始建立應用程式。
依照頁面指引操作,約 5 分鐘即可完成 Logto 整合!
以下是我的整合程式碼:
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
};
function App() {
return (
<LogtoProvider config={config}>
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100">
<Routes>
{/* 這個 callback 處理 Logto 登入導回 */}
<Route path="/callback" element={<Callback />} />
<Route path="/*" element={<AppContent />} />
</Routes>
</div>
</LogtoProvider>
);
}
function AppContent() {
const { isAuthenticated } = useLogto();
if (!isAuthenticated) {
// 未驗證 (Authentication) 使用者顯示首頁
return <Landing />;
}
// 驗證 (Authentication) 後顯示主應用
return (
<Routes>
{/* 儀表板顯示所有可用組織 */}
<Route path="/" element={<Dashboard />} />
{/* 點擊組織後進入組織頁 */}
<Route path="/:orgId" element={<Organization />} />
</Routes>
);
}

一個實用小技巧:我們的登入頁同時有「登入」與「註冊」按鈕。註冊按鈕會直接導向 Logto 的註冊頁。這是透過 Logto 的 first screen 功能實現的。它決定使用者進入驗證 (Authentication) 流程時先看到哪個步驟。
如果你的產品預期有大量新用戶,可以預設導向註冊頁。
function LandingPage() {
const { signIn } = useLogto();
return (
<div className="landing-container">
<div className="auth-buttons">
<button
className="sign-in-button"
onClick={() => {
signIn({
redirectUri: '<YOUR_APP_CALLBACK_URL>',
});
}}
>
登入
</button>
<button
className="register-button"
onClick={() => {
signIn({
redirectUri: '<YOUR_APP_CALLBACK_URL>',
firstScreen: 'register',
});
}}
>
註冊
</button>
</div>
</div>
);
}
點擊登入後會跳轉到 Logto 登入頁。登入(或註冊)成功後,恭喜!你的應用程式有了第一位使用者(你自己)!
當你需要登出時,呼叫 useLogto hook 的 signOut 函式即可。
function SignOutButton() {
const { signOut } = useLogto();
return <button onClick={() => signOut('<YOUR_POST_LOGOUT_REDIRECT_URL>')}>登出</button>;
}
自訂登入與註冊方式
在 Logto Console 點擊左側選單「Sign-in & account」,再點「Sign-up and sign-in」分頁。 依照頁面指示設定 Logto 的登入 / 註冊方式。

登入流程會長這樣:
啟用多重要素驗證 (MFA)
在 Logto 啟用 MFA 很簡單。只需在 Logto Console 點擊「Multi-factor auth」按鈕,然後在多重要素驗證頁啟用即可。

MFA 流程會長這樣:


一切都很簡單!我們只花幾分鐘就建立了一套複雜的使用者驗證 (Authentication) 系統!
新增多租戶組織體驗
現在我們有了第一位使用者!但這個使用者還不屬於任何組織,也還沒建立任何組織。
Logto 內建多租戶支援。你可以在 Logto 建立任意數量的組織,每個組織可有多位成員。
每個使用者都可以從 Logto 取得自己的組織資訊,實現多租戶支援。
取得使用者的組織資訊
要從 Logto 取得使用者的組織資訊,分兩步:
在 Logto Config 宣告組織資訊存取權。這是透過設定適當的 scopes 與 resources 完成的。
import { UserScope, ReservedResource } from "@logto/react";
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations], // 值:"urn:logto:scope:organizations"
resources: [ReservedResource.Organization], // 值:"urn:logto:resource:organizations"
};
使用 Logto 的 fetchUserInfo 方法取得使用者資訊(包含組織資料)。
function Dashboard() {
// 取得使用者資訊
const { fetchUserInfo } = useLogto();
const [organizations, setOrganizations] = useState<OrganizationData[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadOrganizations = async () => {
try {
setLoading(true);
// 取得使用者資訊
const userInfo = await fetchUserInfo();
// 取得使用者的組織資訊
const organizationData = userInfo?.organization_data || [];
setOrganizations(organizationData);
} catch (error) {
console.error('Failed to fetch organizations:', error);
} finally {
setLoading(false);
}
};
loadOrganizations();
}, [fetchUserInfo]);
if (loading) {
return <div>載入中...</div>;
}
if (organizations.length === 0) {
return <div>你還不是任何組織的成員</div>;
}
return <div>組織:{organizations.map(org => org.name).join(', ')}</div>;
}
完成這些步驟後,你需要先登出再登入。這是因為我們修改了請求的 scope 與 resource。
此時你還沒建立任何組織,使用者也還沒加入任何組織。儀表板會顯示「你還沒有任何組織」。

接下來,我們要為使用者建立一個組織並將其加入。
多虧有 Logto,我們不用自己實作複雜的組織關係。只需在 Logto 建立組織並將使用者加入即可。建立組織有兩種方式:
- 透過 Logto Console 手動建立組織
- 使用 Logto Management API 建立組織,特別適合設計讓使用者自行建立組織(工作區)的 SaaS 流程
在 Logto Console 建立組織
在 Logto Console 左側點選「Organizations」選單,建立一個組織。
現在你有了第一個組織。

接著,將使用者加入這個組織。
進入組織詳細頁,切換到 Members 分頁,點擊「+ Add member」按鈕,從左側清單選擇你的登入使用者,點右下角「Add members」按鈕。現在你已成功將使用者加入組織。

重新整理 APP 頁面,你會看到使用者已屬於某個組織!

實作自助式組織建立體驗
只在 Console 建立組織還不夠。你的 SaaS 應用需要讓終端使用者能輕鬆建立與管理自己的工作區。要實作這個功能,請使用 Logto Management API。
請參考 Interact with Management API 文件設置與 Logto 的 API 通訊。
理解組織驗證 (Authentication) 互動流程
以組織建立流程為例,流程如下:
這個流程有兩個關鍵驗證 (Authentication) 需求:
- 保護後端服務 API:
- 前端存取後端服務 API 需驗證 (Authentication)
- API 端點透過驗證 Logto 存取權杖 (Access token) 保護
- 確保只有驗證 (Authentication) 使用者能存取服務
- 存取 Logto Management API:
- 後端服務需安全呼叫 Logto Management API
- 依照 Interact with Management API 指南設置
- 使用機器對機器 (Machine-to-Machine) 驗證 (Authentication) 取得存取憑證
保護你的後端 API
首先,在後端服務建立一個建立組織的 API 端點。
app.post('/organizations', async (req, res) => {
// 使用 Logto Management API 實作
// ...
});
後端服務 API 只允許驗證 (Authentication) 使用者存取。我們需要用 Logto 保護 API,也要取得目前使用者資訊(如 user ID)。
在 Logto 與 OAuth 2.0 概念下,後端服務扮演資源伺服器(resource server)。使用者從前端帶著 Access token 存取 DocuMind 資源伺服器,資源伺服器驗證權杖,通過才回傳資源。
我們先建立一個 API Resource 來代表後端服務。
進入 Logto Console。
- 點擊右側「API resources」按鈕。
- 點「Create API resource」,彈窗選 Express。
- API 名稱填「DocuMind API」,API 識別碼填「https://api.documind.com」。
- 點擊建立。
不用擔心這個 API 識別碼 URL,它只是 Logto 內唯一識別你的 API,與實際後端服務 URL 無關。
你會看到一份 API resource 教學,可以照那份或照下方步驟。
我們來建立 requireAuth middleware 保護 POST /organizations 端點。
const { createRemoteJWKSet, jwtVerify } = require('jose');
const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';
if (!authorization) {
throw new Error('Authorization header missing');
}
if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization token type not supported');
}
return authorization.slice(bearerTokenIdentifier.length + 1);
};
const requireAuth = (resource) => {
if (!resource) {
throw new Error('Resource parameter is required for authentication');
}
return async (req, res, next) => {
try {
// 取得權杖
const token = getTokenFromHeader(req.headers);
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: resource,
}
);
// 將使用者資訊加到 request
req.user = {
id: payload.sub,
};
next();
} catch (error) {
console.error('Auth error:', error);
res.status(401).json({ error: 'Unauthorized' });
}
};
};
module.exports = {
requireAuth,
};
要用這個 middleware,需要這些環境變數:
- LOGTO_JWKS_URL
- LOGTO_ISSUER
這些變數可從 Logto tenant 的 OpenID Configuration 端點取得。造訪 https://<your-tenant-id>.logto.app/oidc/.well-known/openid-configuration,回傳的 JSON 會有:
{
"jwks_uri": "<https://tenant-id.logto.app/oidc/jwks>",
"issuer": "<https://tenant-id.logto.app/oidc>"
}
現在在 POST /organizations 端點使用 requireAuth middleware。
app.post('/organizations', requireAuth('<https://api.documind.com>'), async (req, res) => {
// 處理組織建立邏輯
// ...
});
這樣就保護了 POST /organizations 端點,只有持有有效 Logto 存取權杖的使用者才能存取。
我們現在可以在前端從 Logto 取得權杖,使用者可透過這個權杖經後端服務建立組織。middleware 也會給我們 user ID,方便加入組織。
在前端程式碼中,在 Logto config 宣告這個 API resource,將其識別碼加到 resources 陣列。
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations],
resources: [ReservedResource.Organization, "<https://api.documind.com>"], // 新增的 API resource 識別碼
};
如同之前,更新 Logto config 後,使用者需重新登入。
在 Dashboard 中,建立組織時取得 Logto 存取權杖,並用這個權杖存取後端服務 API。
// 取得 "DocuMind API" 的 access token
const token = await getAccessToken('<https://api.documind.com>');
// 用權杖存取後端服務 API
const response = await fetch('<http://localhost:3000/organizations>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: 'Organization A',
description: 'Organization A description',
}),
});
現在我們可以正確存取 DocuMind 後端服務 API。
呼叫 Logto Management API
我們來用 Logto Management API 實作組織建立。
如同前端請求後端,後端請求 Logto 也需要 Access token。
在 Logto,我們用機器對機器 (Machine-to-Machine) 驗證 (Authentication) 取得 access token。詳見 Interact with Management API。
進入 Logto Console 的 applications 頁,建立一個 Machine-to-Machine 應用程式,分配「Logto Management API access」角色,複製 Token endpoint、App ID、App Secret,這些會用來取得 access token。

現在我們可以透過這個 M2M 應用取得 Logto Management API 的 access token。
async function fetchLogtoManagementApiAccessToken() {
const response = await fetch(process.env.LOGTO_MANAGEMENT_API_TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(
`${process.env.LOGTO_MANAGEMENT_API_APPLICATION_ID}:${process.env.LOGTO_MANAGEMENT_API_APPLICATION_SECRET}`
).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
resource: process.env.LOGTO_MANAGEMENT_API_RESOURCE,
scope: 'all',
}).toString(),
});
const data = await response.json();
return data.access_token;
}
用這個 access token 呼叫 Logto Management API。
我們會用到這些 Management API:
POST /api/organizations:建立組織(參考:Create organization API reference)POST /api/organizations/{id}/users:將使用者加入組織(參考:Add users to organization API reference)
app.post('/organizations', requireAuth('<https://api.documind.com>'), async (req, res) => {
const accessToken = await fetchLogtoManagementApiAccessToken();
// 在 Logto 建立組織並將使用者加入
const response = await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
name: req.body.name,
description: req.body.description,
}),
});
const createdOrganization = await response.json();
await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
userIds: [req.user.id],
}),
});
res.json({ data: createdOrganization });
});
我們已經透過 Logto Management API 實作組織建立與成員加入。
來 Dashboard 測試這個功能。

點擊「Create Organization」

建立成功!
下一步是邀請使用者加入組織。這部分本教學暫不實作。你已經知道如何用 Management API。可參考 租戶建立與邀請 作為產品設計參考,並依照這篇部落格:How we implement user collaboration within a multi-tenant app 輕鬆實作。
為多租戶應用程式實作存取控制
現在來進行組織存取控制。
我們要達成:
- 使用者只能存取自己組織的資源:可透過 Logto 的
組織權杖 (Organization token)實現 - 使用者在組織內有特定角色(含不同權限)以執行授權操作:可透過 Logto 的組織模板功能實現
來看看如何實作這些功能。
使用 Logto 組織權杖 (Organization token)
類似前面提到的 Logto 存取權杖 (Access token),Logto 會針對特定資源發放 access token,使用者用這個權杖存取後端受保護資源。對應地,Logto 會針對特定組織發放組織權杖 (Organization token),使用者用這個權杖存取後端受保護的組織資源。
在前端應用中,可用 Logto 的 getOrganizationToken 方法取得存取特定組織的權杖。
const { getOrganizationToken } = useLogto();
const organizationToken = await getOrganizationToken(organizationId);
這裡的 organizationId 是使用者所屬組織的 id。
在使用 getOrganization 或任何組織功能前,需確保 Logto config 已包含 urn:logto:scope:organizations scope 與 urn:logto:resource:organization resource。前面已宣告過,這裡不再重複。
在組織頁面中,我們用組織權杖取得組織內文件。
function OrganizationPage() {
const { organizationId } = useParams();
const navigate = useNavigate();
const { signOut, getOrganizationToken } = useLogto();
const [error, setError] = useState<Error | null>(null);
const [documents, setDocuments] = useState([]);
const fetchDocuments = useCallback(async () => {
if (!organizationId) return;
try {
const organizationToken = await getOrganizationToken(organizationId);
const response = await fetch(`http://localhost:3000/documents`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${organizationToken}`,
},
});
const documents = await response.json();
setDocuments(documents);
} catch (error: unknown) {
if (error instanceof Error) {
setError(error);
} else {
setError(new Error(String(error)));
}
}
},[getOrganizationToken, organizationId]);
useEffect(() => {
void fetchDocuments();
}, [fetchDocuments]);
if (error) {
return <div>錯誤:{error.message}</div>;
}
return <div>
<h1>組織文件</h1>
<ul>
{documents.map((document) => (
<li key={document.id}>{document.name}</li>
))}
</ul>
</div>
}
這裡有兩個重點:
- 如果傳給
getOrganizationToken的organizationId並非目前使用者所屬組織 id,這個方法無法取得權杖,確保使用者只能存取自己的組織。 - 請求組織資源時,我們用組織權杖而非 access token,因為組織資源要用組織權限控管,而不是單一使用者權限(後面實作
GET /documentsAPI 時會更清楚)。
接下來在後端服務建立 GET /documents API。類似用 API resource 保護 POST /organizations,這裡用組織專屬資源標示符保護 GET /documents。
先建立 requireOrganizationAccess middleware 保護組織資源。
const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';
if (!authorization) {
throw new Error('Authorization header missing');
}
if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization token type not supported');
}
return authorization.slice(bearerTokenIdentifier.length + 1);
};
const extractOrganizationId = (aud) => {
if (!aud || typeof aud !== 'string' || !aud.startsWith('urn:logto:organization:')) {
throw new Error('Invalid organization token');
}
return aud.replace('urn:logto:organization:', '');
};
const decodeJwtPayload = (token) => {
try {
const [, payloadBase64] = token.split('.');
if (!payloadBase64) {
throw new Error('Invalid token format');
}
const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf-8');
return JSON.parse(payloadJson);
} catch (error) {
throw new Error('Failed to decode token payload');
}
};
const requireOrganizationAccess = () => {
return async (req, res, next) => {
try {
// 取得權杖
const token = getTokenFromHeader(req.headers);
// 動態取得 audience
const { aud } = decodeJwtPayload(token);
if (!aud) {
throw new Error('Missing audience in token');
}
// 驗證權杖與 audience
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: aud,
}
);
// 從 audience claim 取出組織 ID
const organizationId = extractOrganizationId(payload.aud);
// 將組織資訊加到 request
req.user = {
id: payload.sub,
organizationId,
};
next();
} catch (error) {
console.error('Organization auth error:', error);
res.status(401).json({ error: 'Unauthorized - Invalid organization access' });
}
};
};
然後用 requireOrganizationAccess middleware 保護 GET /documents API。
app.get('/documents', requireOrganizationAccess(), async (req, res) => {
// 可透過 req.user 取得目前使用者 id 與 organizationId
console.log('userId', req.user.id);
console.log('organizationId', req.user.organizationId);
// 依 organizationId 從資料庫取得文件
// ....
const documents = await getDocumentsByOrganizationId(req.user.organizationId);
res.json(documents);
});
這樣就實現了用組織權杖存取組織資源。後端可依組織 id 從資料庫撈出對應資源。
有些軟體需要組織間資料隔離,進一步討論與實作可參考:Multi-tenancy implementation with PostgreSQL: Learn through a simple real-world example。
實作組織層級 RBAC 設計
我們已經實作用組織權杖存取組織資源,接下來要用 RBAC 實作組織內使用者權限控管。
假設 DocuMind 有兩種角色:Admin 與 Collaborator。
Admin 可建立與存取文件,Collaborator 只能存取文件。
因此組織需有這兩個角色:Admin 與 Collaborator。
Admin 擁有 read:documents 與 create:documents 權限,Collaborator 只有 read:documents 權限。
- Admin
read:documentscreate:documents
- Collaborator
read:documents
這時就要用到 Logto 的組織模板功能。
組織模板是每個組織存取控制模型的藍圖:定義所有組織適用的角色與權限。
為什麼要組織模板?
因為可擴展性是 SaaS 產品最重要的需求之一。換句話說,對一個客戶有效的存取模型,對所有客戶都要有效。
進入 Logto Console > Organization Templates > Organization permissions,建立兩個權限:read:documents 與 create:documents。

再到組織角色分頁建立兩個角色:Admin 與 Collaborator,並分配對應權限。

這樣就為每個組織建立了 RBAC 權限模型。
接著到組織詳細頁為成員分配適當角色。

現在組織使用者有角色了! 你也可以透過 Logto Management API 完成這些步驟:
// 指派 'Admin' 角色給組織建立者
app.post('/organizations', requireAuth('https://api.documind.com'), async (req, res) => {
const accessToken = await fetchLogtoManagementApiAccessToken();
// 在 Logto 建立組織
// 既有程式碼...
// 在 Logto 加入使用者到組織
await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
userIds: [req.user.id],
}),
});
// 指派 `Admin` 角色給第一位使用者
const rolesResponse = await fetch(`${process.env.LOGTO_ENDPOINT}/api/organization-roles`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
});
const roles = await rolesResponse.json();
// 找到 `Admin` 角色
const adminRole = roles.find((role) => role.name === 'Admin');
// 指派 `Admin` 角色給第一位使用者
await fetch(
`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users/${req.user.id}/roles`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
organizationRoleIds: [adminRole.id],
}),
}
);
// 既有程式碼...
});
現在我們可以透過檢查權限實作使用者權限控管。
在程式碼中,需要讓使用者的組織權杖帶有權限資訊,然後在後端驗證這些權限。
在前端 Logto config 宣告組織內需要請求的權限,將 read:documents 與 create:documents 權限加到 scopes。
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations, "read:documents", "create:documents"],
resources: [ReservedResource.Organization, "<https://api.documind.com>"], // 新增的 API resource 識別碼
};
照慣例,請用戶重新登入讓設定生效。
然後在後端 requireOrganizationAccess middleware 加入權限驗證。
const hasRequiredScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};
const requireOrganizationAccess = ({ requiredScopes = [] } = {}) => {
return async (req, res, next) => {
try {
//...
// 驗證權杖與 audience
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: aud,
}
);
//...
// 從權杖取得 scopes
const scopes = payload.scope?.split(' ') || [];
// 驗證所需權限
if (!hasRequiredScopes(scopes, requiredScopes)) {
throw new Error('Insufficient permissions');
}
//...
next();
} catch (error) {
//...
}
};
};
然後建立 POST /documents API,並用 requireOrganizationAccess middleware 搭配 requiredScopes 保護這個 API 及前面的 GET /documents API。
// 建立文件 API
app.post(
'/documents',
requireOrganizationAccess({ requiredScopes: ['create:documents'] }),
async (req, res) => {
//...
}
);
// 取得文件 API
app.get(
'/documents',
requireOrganizationAccess({ requiredScopes: ['read:documents'] }),
async (req, res) => {
//...
}
);
這樣就透過檢查權限實作了使用者權限控管。
在前端可透過解碼組織權杖或呼叫 Logto 的 getOrganizationTokenClaims 方法取得使用者權限資訊。
const [scopes, setScopes] = useState([]);
const { getOrganizationTokenClaims } = useLogto();
const loadScopes = async () => {
const claims = await getOrganizationTokenClaims(organizationId);
setScopes(claims.scope.split(' '));
};
// ...
根據 claims 中的 scopes 控制頁面元素顯示。
新增更多多租戶應用功能
到目前為止,我們已經實作了多租戶 SaaS 系統的基本使用者與組織功能!但還有一些功能沒涵蓋,例如每個組織自訂登入頁品牌、特定網域 email 自動加入組織、整合企業級單一登入 (SSO) 等。
這些都是開箱即用的功能,更多資訊請參考 Logto 文件:
小結
還記得一開始那種壓力山大的感覺嗎?使用者、組織、權限、企業級功能……看起來像座爬不完的高山。
但看看我們完成了什麼:
- 一套完整的驗證 (Authentication) 系統,支援多種登入選項與 MFA
- 一個彈性的組織系統,支援多重成員資格
- 組織內的角色型存取控制 (RBAC)
最棒的是?我們不用重造輪子。善用像 Logto 這樣的現代工具,將原本要花數月開發的功能縮短到幾分鐘。
本教學完整原始碼請見:Multi-tenant SaaS Sample。
這就是 2025 年現代開發的威力——我們可以專注於打造獨特產品功能,而不是與基礎設施搏鬥。現在輪到你打造屬於自己的精彩產品!
探索 Logto 的所有功能,從 Logto Cloud 到 Logto OSS,請造訪 Logto 官網 或立即註冊 Logto cloud。