跳至主要內容

實務上的角色型存取控制 (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 實作存取控制

對每個端點,我們需考慮兩個存取控制面向:

  1. 資源擁有權 —— 使用者是否擁有該資源?
  2. 角色型權限 —— 使用者的角色是否允許此操作?

以下是各端點的存取邏輯:

端點存取控制邏輯
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 為例。

建立應用程式

  1. 前往 Logto Console 的「應用程式」建立新 React 應用程式

CMS React application

設定 API 資源與權限

  1. 前往 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

CMS API resource details

建立角色

前往 Logto Console 的「角色」建立下列 CMS 角色

  • 管理員 (Admin)
    • 擁有所有權限
  • 發佈者 (Publisher)
    • 擁有 read:articleslist:articlespublish:articles
  • 作者 (Author)
    • 擁有 create:articles

Admin role

Publisher role

Author role

指派角色給使用者

前往 Logto Console 的「使用者管理」建立使用者。

在使用者詳細資料的「角色」分頁中,可指派角色給該使用者。

本範例建立 3 位使用者及其角色:

  • Alex:管理員 (Admin)
  • Bob:發佈者 (Publisher)
  • Charlie:作者 (Author)

User management

User details - Alex

備註:

為方便展示,我們透過 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) 角色,可建立、刪除、更新、發佈並檢視所有文章。

CMS dashboard - Alex

Charles 擁有作者 (Author) 角色,只能建立自己的文章,且僅能檢視、更新、刪除自己擁有的文章。

CMS dashboard - Charles - Article list

Bob 擁有發佈者 (Publisher) 角色,可檢視與發佈所有文章,但無法建立、更新或刪除文章。

CMS dashboard - Bob

結論

恭喜你!你已學會如何在應用程式中實作健全的 RBAC 系統。

若需更複雜的情境(如建構多租戶應用程式),Logto 提供完整的組織 (Organization) 支援。歡迎參考我們的指南 打造多租戶 SaaS 應用程式:從設計到實作的完整指南,深入瞭解組織層級的存取控制。

祝你寫程式愉快!🚀