跳到主要内容

实践中的 RBAC:为你的应用实现安全的授权 (Authorization)

你是否在为你的应用实现一个安全且可扩展的授权 (Authorization) 系统而苦恼?基于角色的访问控制 (RBAC) 是管理用户权限的行业标准,但正确实现它可能具有挑战性。本教程将通过一个真实的内容管理系统 (CMS) 示例,教你如何构建一个健壮的 RBAC 系统。

通过本指南,你将学到:

  • ✨ 如何设计和实现细粒度权限,实现精确控制
  • 🔒 如何将权限有序地组织到有意义的角色中
  • 👤 有效处理资源所有权的技巧
  • 🚀 让你的授权 (Authorization) 系统具备可扩展性和可维护性的方法
  • 💡 通过真实 CMS 示例的实践实现

本教程的完整源码可在 GitHub 获取。

理解 RBAC 基础

基于角色的访问控制 (RBAC) 不仅仅是为用户分配权限。它是关于创建一种结构化的授权 (Authorization) 方法,在安全性和可维护性之间取得平衡。

你可以在 Auth Wiki 了解更多 什么是 RBAC

以下是我们在实现中遵循的关键原则:

细粒度权限设计

细粒度权限让你可以精确控制用户在系统中的操作。与“管理员”或“用户”这类宽泛的访问级别不同,我们定义用户可以对资源执行的具体操作。例如:

  • read:articles - 查看系统中的任意文章
  • create:articles - 创建新文章
  • update:articles - 修改已有文章
  • publish:articles - 更改文章的发布状态

资源所有权与访问控制

资源所有权是我们 CMS 授权 (Authorization) 设计中的一个基本概念。RBAC 定义了不同角色可以执行的操作,而所有权为访问控制增加了个人维度:

  • 作者自动拥有他们创建的文章的访问权限
  • 这种自然的所有权模型意味着作者始终可以查看和编辑自己的内容
  • 系统在处理文章操作时会同时检查角色权限或所有权
  • 例如,即使没有 update:articles 权限,作者仍然可以编辑自己的文章
  • 这种设计减少了额外角色权限的需求,同时保证了安全性

这种双层结构(角色 + 所有权)让系统更直观且更安全。发布者和管理员仍然可以通过其角色权限管理所有内容,而作者则对自己的作品拥有控制权。

设计安全的 API

让我们从通过 API 端点设计 CMS 的核心功能开始:

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 控制台的“应用程序”中创建一个新的 React 应用

CMS React 应用

配置 API 资源和权限

  1. 在 Logto 控制台的“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 资源详情

创建角色

在 Logto 控制台的“角色”中为 CMS 创建以下角色

  • 管理员 (Admin)
    • 拥有所有权限
  • 发布者 (Publisher)
    • 拥有 read:articleslist:articlespublish:articles
  • 作者 (Author)
    • 拥有 create:articles

管理员角色

发布者角色

作者角色

分配角色给用户

在 Logto 控制台的“用户管理”部分创建用户。

在用户详情的“角色”标签页中,你可以为用户分配角色。

在我们的示例中,我们创建了 3 个用户并分配如下角色:

  • Alex:管理员 (Admin)
  • Bob:发布者 (Publisher)
  • Charlie:作者 (Author)

用户管理

用户详情 - Alex

备注:

为了演示,我们通过 Logto 控制台创建这些资源和配置。在实际项目中,你可以通过 Logto 提供的 Management API 以编程方式创建这些资源和配置。

前端集成 Logto RBAC

现在,我们已经在 Logto 中配置好了 RBAC,可以开始将其集成到前端。

首先,按照 Logto 快速上手 将 Logto 集成到你的应用中。

在我们的示例中,我们使用 React 进行演示。

在你的应用中配置好 Logto 后,我们需要为 Logto 添加 RBAC 配置以使其生效。

// frontend/src/App.tsx

const logtoConfig: LogtoConfig = {
appId: LOGTO_APP_ID, // 你在 Logto 控制台创建的应用 ID
endpoint: LOGTO_ENDPOINT, // 你在 Logto 控制台创建的 endpoint
resources: [API_RESOURCE], // 你在 Logto 控制台创建的 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 获取访问令牌中的权限 (Scopes)。

// 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,
};

如你所见,在这个中间件中,我们会验证前端请求是否包含有效的访问令牌 (Access token),并检查访问令牌的受众是否与我们在 Logto 控制台创建的 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 控制台的“登录与账户 > 注册和登录”并启用你喜欢的认证 (Authentication) 方式(如邮箱 + 密码或用户名 + 密码)。

首先,分别以 Alex 和 Charles 登录并创建一些文章。

由于 Alex 拥有管理员 (Admin) 角色,他可以创建、删除、更新、发布和查看所有文章。

CMS 仪表盘 - Alex

Charles 拥有作者 (Author) 角色,只能创建自己的文章,并且只能查看、更新和删除自己拥有的文章。

CMS 仪表盘 - Charles - 文章列表

Bob 拥有发布者 (Publisher) 角色,可以查看和发布所有文章,但不能创建、更新或删除文章。

CMS 仪表盘 - Bob

总结

恭喜你!你已经学会了如何在应用中实现一个健壮的 RBAC 系统。

对于更复杂的场景,比如构建多租户应用,Logto 提供了完善的组织 (Organization) 支持。你可以查看我们的指南 构建多租户 SaaS 应用:从设计到实现的完整指南 了解如何实现组织范围的访问控制。

祝你编码愉快!🚀