跳到主要内容

实践中的基于角色的访问控制 (RBAC):为你的应用实现安全的授权 (Authorization)

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

通过本指南,你将学到:

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

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

理解 RBAC 基础

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

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

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

细粒度权限 (Permissions) 设计

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

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

资源所有权与访问控制

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

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

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

设计安全的 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. 基于角色 (Roles) 的权限 (Permissions)——用户的角色 (Role) 是否允许此操作?

以下是我们对每个端点的访问控制处理方式:

端点访问控制逻辑
GET /api/articles- 任何拥有 list:articles 权限 (Permission) 的用户,或作者可见自己的文章
GET /api/articles/:id- 任何拥有 read:articles 权限 (Permission) 的用户,或文章作者
POST /api/articles- 任何拥有 create:articles 权限 (Permission) 的用户
PATCH /api/articles/:id- 任何拥有 update:articles 权限 (Permission) 的用户,或文章作者
DELETE /api/articles/:id- 任何拥有 delete:articles 权限 (Permission) 的用户,或文章作者
PATCH /api/articles/:id/published- 仅拥有 publish:articles 权限 (Permission) 的用户

构建可扩展的权限 (Permissions) 系统

根据我们的 API 访问需求,我们可以定义如下权限 (Permissions):

权限 (Permission)描述
list:articles查看系统中所有文章的列表
read:articles阅读任意文章的完整内容
create:articles创建新文章
update:articles修改任意文章
delete:articles删除任意文章
publish:articles更改发布状态

注意,这些权限 (Permissions) 仅在访问你不拥有的资源时需要。文章所有者可以自动:

  • 查看自己的文章(无需 read:articles 权限 (Permission))
  • 编辑自己的文章(无需 update:articles 权限 (Permission))
  • 删除自己的文章(无需 delete:articles 权限 (Permission))

构建有效的角色 (Roles)

现在我们已经定义了 API 和权限 (Permissions),可以创建逻辑分组这些权限 (Permissions) 的角色 (Roles):

权限 (Permission)/角色 (Role)👑 管理员 (Admin)📝 发布者 (Publisher)✍️ 作者 (Author)
描述拥有完整内容管理权限可查看所有文章并控制发布状态可在系统中创建新文章
list:articles
read:articles
create:articles
update:articles
delete:articles
publish:articles

注意:作者无论角色 (Role) 权限 (Permission) 如何,自动拥有对自己文章的读取 / 更新 / 删除权限 (Permissions)。

每个角色 (Role) 都有明确的职责:

  • 管理员 (Admin):对 CMS 拥有完全控制权,包括所有文章操作
  • 发布者 (Publisher):专注于内容审核和发布管理
  • 作者 (Author):专注于内容创作

这种角色 (Role) 结构实现了关注点的清晰分离:

  • 作者专注于内容创作
  • 发布者管理内容质量和可见性
  • 管理员维护整体系统控制

在 Logto 中配置 RBAC

在开始之前,你需要在 Logto Cloud 创建一个账号,或者你也可以通过 Logto OSS 版本 使用自托管的 Logto 实例。

但本教程将以 Logto Cloud 为例,操作更为简便。

设置你的应用

  1. 在 Logto 控制台的“应用程序”中创建一个新的 React 应用

CMS React 应用

配置 API 资源和权限 (Permissions)

  1. 在 Logto 控制台的“API 资源”中创建一个新的 API 资源
    • API 名称:CMS API
    • API 标识符:https://api.cms.com
    • 为 API 资源添加权限 (Permissions)
      • list:articles
      • read:articles
      • create:articles
      • update:articles
      • publish:articles
      • delete:articles

CMS API 资源详情

创建角色 (Roles)

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

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

管理员角色

发布者角色

作者角色

分配角色 (Roles) 给用户

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

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

在我们的示例中,我们创建了 3 个用户及其角色 (Roles):

  • 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 会将与用户角色 (Role) 相关的权限 (Scopes) 添加到访问令牌 (Access token) 中。

你可以通过 useLogtogetAccessTokenClaims 获取访问令牌 (Access token) 中的权限 (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"
>
删除
</button>
)}
</div>
);
};

后端集成 Logto RBAC

现在,是时候将 Logto RBAC 集成到你的后端了。

后端授权 (Authorization) 中间件

首先,我们需要在后端添加一个中间件,用于检查用户权限 (Permissions)、验证用户是否已登录,并判断其是否有权限访问某些 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(' ') || [],
};

// 验证所需权限 (Scopes)
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),并检查访问令牌 (Access token) 的受众是否与我们在 Logto 控制台创建的 API 资源一致。

验证 API 资源的原因在于,我们的 API 资源实际上代表了 CMS 后端的资源,所有 CMS 权限 (Permissions) 都与该 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 资源的访问令牌 (Access token)
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',
// 在请求头中添加访问令牌 (Access token)
Authorization: `Bearer ${token}`,
...options.headers,
},
});

// ... 处理响应

return await response.json();
} catch (error) {
// ... 错误处理
}
},
[getAccessToken]
);
};

现在我们可以使用 requireAuth 中间件来保护我们的 API 端点。

保护 API 端点

对于只允许特定权限 (Permissions) 用户访问的 API,可以直接在中间件中添加限制。例如,文章创建 API 只允许拥有 create:articles 权限 (Permission) 的用户访问:

// backend/src/routes/articles.js

const { requireAuth } = require('../middleware/auth');

router.post('/articles', requireAuth(['create:articles']), async (req, res) => {
// ...
});

对于需要同时检查权限 (Permissions) 和资源所有权的 API,可以使用 hasScopes 函数。例如,在文章列表 API 中,拥有 list:articles 权限 (Permission) 的用户可以访问所有文章,而作者只能访问自己创建的文章:

// backend/src/routes/articles.js

const { requireAuth, hasScopes } = require('../middleware/auth');

router.get('/articles', requireAuth(), async (req, res) => {
try {
// 如果用户有 list:articles 权限 (Scope),返回所有文章
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) 角色 (Role),他可以创建、删除、更新、发布并查看所有文章。

CMS 控制台 - Alex

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

CMS 控制台 - Charles - 文章列表

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

CMS 控制台 - Bob

总结

恭喜你!你已经学会了如何在应用中实现一个强大的基于角色的访问控制 (RBAC) 系统。

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

祝你编码愉快!🚀