實務上的角色型存取控制 (RBAC in practice):為你的應用程式實作安全的授權 (Authorization)
你是否在為應用程式實作安全且可擴展的授權 (Authorization) 系統而苦惱?角色型存取控制 (RBAC, Role-based Access Control) 是管理使用者權限的業界標準,但正確實作並不容易。本教學將以實際的內容管理系統(CMS, Content Management System)為例,帶你建立一套健全的 RBAC 系統。
跟著本指南,你將學會:
- ✨ 如何設計與實作細緻的權限,精確掌控存取行為
- 🔒 將權限有意義地組織成角色的最佳實踐
- 👤 有效處理資源擁有權的技巧
- 🚀 讓你的授權 (Authorization) 系統具備可擴展性與可維護性的方法
- 💡 以真實 CMS 範例實作
本教學完整原始碼可在 GitHub 取得。
理解 RBAC 基礎
角色型存取控制 (RBAC) 不僅僅是將權限分配給使用者,更是建立一套結構化的授權 (Authorization) 方法,兼顧安全性與可維護性。
你可以在 Auth Wiki 進一步瞭解 什麼是 RBAC。
以下是我們實作時遵循的關鍵原則:
細緻權限設計
細緻的權限讓你能精確控制使用者在系統中的操作。與其僅用「admin」或「user」這類廣泛權限,我們會定義使用者可對資源執行的具體動作。例如:
read:articles
- 檢視系統中任一文章create:articles
- 建立新文章update:articles
- 修改現有文章publish:articles
- 變更文章發佈狀態
資源擁有權與存取控制
資源擁有權是我們 CMS 授權 (Authorization) 設計的核心概念。RBAC 定義不同角色可執行的動作,而擁有權則為存取控制增添個人層面:
- 作者自動擁有自己創建的文章的存取權
- 這種自然的擁有權模型讓作者永遠能檢視與編輯自己的內容
- 系統在處理文章操作時會同時檢查角色權限或擁有權
- 例如,即使沒有
update:articles
權限,作者仍可編輯自己的文章 - 這種設計減少了額外角色權限的需求,同時維持安全性
這種雙層(角色 + 擁有權)設計讓系統更直覺且安全。發佈者與管理員可透過角色權限管理所有內容,而作者則掌控自己的作品。
設計安全的 API
我們先從設計 CMS 核心功能的 API 端點開始:
GET /api/articles # 列出所有文章
GET /api/articles/:id # 取得特定文章
POST /api/articles # 建立新文章
PATCH /api/articles/:id # 更新文章
DELETE /api/articles/:id # 刪除文章
PATCH /api/articles/:id/published # 變更發佈狀態
為 API 實作存取控制
對每個端點,我們需考慮兩個存取控制面向:
- 資源擁有權 —— 使用者是否擁有該資源?
- 角色型權限 —— 使用者的角色是否允許此操作?
以下是各端點的存取邏輯:
端點 | 存取控制邏輯 |
---|---|
GET /api/articles | - 具備 list:articles 權限者,或作者可見自己文章 |
GET /api/articles/:id | - 具備 read:articles 權限者,或該文章作者 |
POST /api/articles | - 具備 create:articles 權限者 |
PATCH /api/articles/:id | - 具備 update:articles 權限者,或該文章作者 |
DELETE /api/articles/:id | - 具備 delete:articles 權限者,或該文章作者 |
PATCH /api/articles/:id/published | - 僅限具備 publish:articles 權限者 |
建立可擴展的權限系統
根據 API 存取需求,我們可定義以下權限:
權限 | 說明 |
---|---|
list:articles | 檢視系統中所有文章列表 |
read:articles | 閱讀任一文章完整內容 |
create:articles | 建立新文章 |
update:articles | 修改任一文章 |
delete:articles | 刪除任一文章 |
publish:articles | 變更發佈狀態 |
注意,這些權限僅在存取非自己擁有的資源時才需要。文章擁有者自動擁有:
- 檢視自己文章(不需
read:articles
) - 編輯自己文章(不需
update:articles
) - 刪除自己文章(不需
delete:articles
)
建立有效的角色
API 與權限定義好後,我們可建立邏輯分組的角色:
權限 / 角色 | 👑 管理員 (Admin) | 📝 發佈者 (Publisher) | ✍️ 作者 (Author) |
---|---|---|---|
說明 | 完整內容管理權限 | 可檢視所有文章並控管發佈狀態 | 可在系統中建立新文章 |
list:articles | ✅ | ✅ | ❌ |
read:articles | ✅ | ✅ | ❌ |
create:articles | ✅ | ❌ | ✅ |
update:articles | ✅ | ❌ | ❌ |
delete:articles | ✅ | ❌ | ❌ |
publish:articles | ✅ | ✅ | ❌ |
注意:作者自動擁有自己文章的讀 / 編輯 / 刪除權限,無論角色權限為何。
每個角色皆針對特定職責設計:
- 管理員 (Admin):全面掌控 CMS,包括所有文章操作
- 發佈者 (Publisher):專注於內容審核與發佈管理
- 作者 (Author):專職內容創作
這樣的角色結構明確分工:
- 作者專注於內容創作
- 發佈者管理內容品質與可見性
- 管理員維護整體系統
在 Logto 設定 RBAC
開始前,你需要在 Logto Cloud 建立帳號,或使用 Logto OSS 版本 自行架設 Logto。
本教學為簡化流程,將以 Logto Cloud 為例。
建立應用程式
- 前往 Logto Console 的「應用程式」建立新 React 應用程式
- 應用程式名稱:Content Management System
- 應用程式類型:傳統網頁應用程式
- Redirect URIs: http://localhost:5173/callback
設定 API 資源與權限
- 前往 Logto Console 的「API 資源」建立新 API 資源
- API 名稱:CMS API
- API 識別碼:https://api.cms.com
- 新增權限至 API 資源
list:articles
read:articles
create:articles
update:articles
publish:articles
delete:articles
建立角色
前往 Logto Console 的「角色」建立下列 CMS 角色
- 管理員 (Admin)
- 擁有所有權限
- 發佈者 (Publisher)
- 擁有
read:articles
、list:articles
、publish:articles
- 擁有
- 作者 (Author)
- 擁有
create:articles
- 擁有
指派角色給使用者
前往 Logto Console 的「使用者管理」建立使用者。
在使用者詳細資料的「角色」分頁中,可指派角色給該使用者。
本範例建立 3 位使用者及其角色:
- Alex:管理員 (Admin)
- Bob:發佈者 (Publisher)
- Charlie:作者 (Author)
為方便展示,我們透過 Logto Console 建立這些資源與設定。實際專案中,你也可以透過 Logto 提供的 Management API 程式化建立這些資源與設定。
前端整合 Logto RBAC
現在我們已在 Logto 設定好 RBAC,可以開始整合到前端。
首先,依照 Logto 快速上手 將 Logto 整合進你的應用程式。
本範例以 React 示範。
完成 Logto 設定後,需新增 RBAC 設定讓 Logto 正常運作。
// frontend/src/App.tsx
const logtoConfig: LogtoConfig = {
appId: LOGTO_APP_ID, // 你在 Logto Console 建立的 app ID
endpoint: LOGTO_ENDPOINT, // 你在 Logto Console 建立的 endpoint
resources: [API_RESOURCE], // 你在 Logto Console 建立的 API 資源識別碼,例如 https://api.cms.com
// 前端可能會向 API 資源請求的所有權限範圍 (scopes)
scopes: [
'list:articles',
'create:articles',
'read:articles',
'update:articles',
'delete:articles',
'publish:articles',
],
};
如果你已登入,記得登出再登入讓設定生效。
當使用者透過 Logto 登入並請求上述 API 資源的存取權杖 (Access token) 時,Logto 會將與使用者角色相關的權限範圍 (scopes) 加入存取權杖。
你可以透過 useLogto
hook 的 getAccessTokenClaims
取得存取權杖中的權限範圍。
// frontend/src/hooks/use-user-data.ts
import { useLogto } from '@logto/react';
import { API_RESOURCE } from '../config';
import { useState, useEffect } from 'react';
export const useUserData = () => {
const { getAccessTokenClaims } = useLogto();
const [userScopes, setUserScopes] = useState<string[]>([]);
const [userId, setUserId] = useState<string>();
useEffect(() => {
const fetchScopes = async () => {
const token = await getAccessTokenClaims(API_RESOURCE);
setUserScopes(token?.scope?.split(' ') ?? []);
setUserId(token?.sub);
};
fetchScopes();
}, [getAccessTokenClaims]);
return { userId, userScopes };
};
你可以利用 userScopes
判斷使用者是否有權存取資源。
// frontend/src/pages/Dashboard.tsx
const Dashboard = () => {
const { userId, userScopes } = useUserData();
// ...
return (
<div>
{/* ... */}
{(userScopes.includes('delete:articles') || article.ownerId === userId) && (
<button
onClick={() => handleDelete(article.id)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
)}
</div>
);
};
後端整合 Logto RBAC
接下來,將 Logto RBAC 整合進後端。
後端授權 (Authorization) 中介軟體
首先,在後端新增中介軟體,檢查使用者權限、驗證是否登入,以及是否有權存取特定 API。
// backend/src/middleware/auth.js
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 hasScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};
const verifyJwt = async (token) => {
const JWKS = createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL));
const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.LOGTO_ISSUER,
audience: process.env.LOGTO_API_RESOURCE,
});
return payload;
};
const requireAuth = (requiredScopes = []) => {
return async (req, res, next) => {
try {
// 取得權杖
const token = getTokenFromHeader(req.headers);
// 驗證權杖
const payload = await verifyJwt(token);
// 將使用者資訊加入請求
req.user = {
id: payload.sub,
scopes: payload.scope?.split(' ') || [],
};
// 驗證所需權限
if (!hasScopes(req.user.scopes, requiredScopes)) {
throw new Error('Insufficient permissions');
}
next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
};
};
module.exports = {
requireAuth,
hasScopes,
};
如上所示,此中介軟體會驗證前端請求是否帶有有效的存取權杖,並檢查該權杖的受眾 (audience) 是否與 Logto Console 建立的 API 資源相符。
驗證 API 資源的原因在於,API 資源實際上代表我們 CMS 後端的資源,所有 CMS 權限都與此 API 資源綁定。
既然此 API 資源代表 Logto 中的 CMS 資源,前端在呼叫後端 API 時會帶上對應的存取權杖 (Access token):
// frontend/src/hooks/use-api.ts
export const useApi = () => {
const { getAccessToken } = useLogto();
return useMemo(
() =>
async (endpoint: string, options: RequestInit = {}) => {
try {
// 取得 API 資源的存取權杖
const token = await getAccessToken(API_RESOURCE);
if (!token) {
throw new ApiRequestError('Failed to get access token');
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
// 將存取權杖加入請求標頭
Authorization: `Bearer ${token}`,
...options.headers,
},
});
// ... 處理回應
return await response.json();
} catch (error) {
// ... 錯誤處理
}
},
[getAccessToken]
);
};
現在可以用 requireAuth
中介軟體保護 API 端點。
保護 API 端點
對於僅允許特定權限使用者存取的 API,可直接在中介軟體中加上限制。例如,建立文章 API 僅允許具備 create:articles
權限的使用者:
// backend/src/routes/articles.js
const { requireAuth } = require('../middleware/auth');
router.post('/articles', requireAuth(['create:articles']), async (req, res) => {
// ...
});
對於需同時檢查權限與資源擁有權的 API,可用 hasScopes
函式。例如,文章列表 API,具備 list:articles
權限者可存取所有文章,否則僅能存取自己創建的文章:
// backend/src/routes/articles.js
const { requireAuth, hasScopes } = require('../middleware/auth');
router.get('/articles', requireAuth(), async (req, res) => {
try {
// 有 list:articles 權限則回傳所有文章
if (hasScopes(req.user.scopes, ['list:articles'])) {
const articles = await articleDB.list();
return res.json(articles);
}
// 否則僅回傳使用者自己的文章
const articles = await articleDB.listByOwner(req.user.id);
res.json(articles);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch articles' });
}
});
至此,我們已完成 RBAC 實作。你可以參考 完整原始碼 了解全部細節。
測試 CMS RBAC 實作
現在,讓我們用剛剛建立的三位使用者測試 CMS RBAC 實作。
如果你發現無法用「使用者管理」建立的帳號登入,請先啟用對應的登入方式。前往 Logto Console 的「登入體驗 (Sign-in Experience)」並啟用你偏好的驗證 (Authentication) 方式(如電子郵件 + 密碼或使用者名稱 + 密碼)。
首先,分別以 Alex 與 Charles 登入並建立一些文章。
Alex 擁有管理員 (Admin) 角色,可建立、刪除、更新、發佈並檢視所有文章。
Charles 擁有作者 (Author) 角色,只能建立自己的文章,且僅能檢視、更新、刪除自己擁有的文章。
Bob 擁有發佈者 (Publisher) 角色,可檢視與發佈所有文章,但無法建立、更新或刪除文章。
結論
恭喜你!你已學會如何在應用程式中實作健全的 RBAC 系統。
若需更複雜的情境(如建構多租戶應用程式),Logto 提供完整的組織 (Organization) 支援。歡迎參考我們的指南 打造多租戶 SaaS 應用程式:從設計到實作的完整指南,深入瞭解組織層級的存取控制。
祝你寫程式愉快!🚀