如何进行授权——决策框架:第一部分
大多数应用程序的安全性取决于两大支柱:身份验证 (AuthN) 和授权 (AuthZ)。前者用于验证用户身份是否与其声称的身份相符,后者则控制用户在系统中可以执行的操作。
身份验证是一个已被广泛理解的问题。经过多年的发展,业界已经总结出清晰的模式和最佳实践供您参考。无论您使用凭证、魔法链接、一次性密码 (OTP)、多因素身份验证 (MFA) 还是 OAuth,您都能找到适用于所有主流编程语言的成熟库和服务。一旦您确定了解决方案,它通常都很稳定,并且随着时间的推移很少需要更改。
相反,授权则完全是另一回事。像基于角色的访问控制(RBAC)和基于属性的访问控制(ABAC)这样的概念广为人知且被广泛接受。然而,如何实现这些概念却定义模糊,这给开发人员留下了很大的发挥空间,但也容易导致错误。授权是一个难题,因为它通常与应用程序的独特特性紧密相关,因此很难找到一种通用的解决方案。
本文旨在通过两个二分轴建立一个决策框架,用于指导如何在应用程序中实现授权:
- 它是否应该与数据库耦合?
- 应该将其嵌入到应用程序中,还是作为一项单独的服务?
我会解释坐标轴两侧的优缺点,并探讨符合该类别的现有解决方案。然后,您可以尝试将自己的需求放到相应的象限中,看看哪个选择最明智。
数据模型与授权模型
授权之所以复杂,主要原因之一在于它通常与业务模型紧密交织。例如,在内容管理系统 (CMS) 中,可能存在多种模型User,例如数据模型Group、内容管理模型、数据模型、数据处理模型、数据访问模型、数据管理模型、数据访问模型、数据访问Image模型等等,而授权很可能需要涉及所有这些模型。我们可以通过分析授权和数据存储之间的关系,将解决方案分为两类。VideoArticlePage
与数据库结合
因此,一个直观的解决方案是将授权机制与数据模型叠加起来。以我们的 CMS 示例为例,如果我们要实现一个简单的基于 ACL 的授权机制,我们可以引入一个模型,使其与其他模型建立关联,并将授权信息作为应用程序数据的一部分进行存储。然后,我们可以使用纯 SQL 查询(假设使用 SQL 数据库)来询问诸如“给我当前用户可读的AccessControlList列表”之类的权限问题,例如:Article
SELECT DISTINCT Article.*
FROM Article
JOIN AccessControlList ON AccessControlList.resource_id = Article.id
LEFT JOIN USER ON AccessControlList.user_id = User.id
LEFT JOIN GROUP ON AccessControlList.group_id = Group.id
WHERE (User.id = CURRENT_USER_ID
OR Group.id IN
(SELECT group_id
FROM UserGroup
WHERE user_id = CURRENT_USER_ID))
AND AccessControlList.permission = 'read';
在了解各种高级授权方案之前,我们都曾像上面那样解决授权问题:用命令式代码编写查询语句。虽然概念上很简单,但这种方法容易出错,而且即使对于稍微复杂一些的应用来说,也很难扩展。幸运的是,现在有一些优秀的解决方案可以帮助你在数据库层面以更具可扩展性的方式建模授权。
1. Google Firebase
Google Firebase是率先将访问策略集成到数据库中的先驱之一。它是一个 NoSQL 数据存储,以集合的形式存储文档,并允许您定义对象访问的安全规则。在执行查询和修改操作时,Firebase 会检查请求是否操作用户有权访问的对象,如果无权访问,则会拒绝该请求。
service cloud.firestore {
match /databases/{database}/documents {
// Matches any document in the 'articles' collection
match /articles/{articleId} {
// Allows read access if an ACL entry exists that grants read access to this user or a group they are part of
allow read: if isUserAuthorizedToRead(articleId);
}
// Helper function to check if a user is authorized to read an article
function isUserAuthorizedToRead(articleId) {
// Check if there's an ACL entry for this user and article with read access
let userAcl = exists(/databases/$(database)/documents/accessControlList/$(request.auth.uid) + '_' + articleId);
if (userAcl) return true;
// If not, check each group the user is part of to see if any have read access
let groups = get(/databases/$(database)/documents/users/$(request.auth.uid)).data.groups;
for (let group in groups) {
let groupAcl = exists(/databases/$(database)/documents/accessControlList/$(group) + '_' + articleId);
if (groupAcl) return true;
}
return false;
}
}
}
2. PostgreSQL 行级安全性
PostgreSQL 提供了一项名为“行级安全性”的功能,允许您使用 SQL 定义细粒度的访问策略。在执行 CRUD 操作期间,数据库引擎会自动在当前用户的上下文中强制执行这些策略。
CREATE POLICY user_access_policy ON Article
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM AccessControlList
WHERE
AccessControlList.resource_id = Article.id AND
AccessControlList.user_id = current_user AND
AccessControlList.permission_type = 'read'
)
);
CREATE POLICY group_access_policy ON Article
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM AccessControlList
JOIN user_groups ON AccessControlList.group_id = user_groups.group_id
WHERE
AccessControlList.resource_id = Article.id AND
user_groups.user_id = current_user AND
AccessControlList.permission_type = 'read'
)
);
Supabase、PostgREST和PostGraphile等一系列产品利用 PostgreSQL 的行级安全功能提供完全授权的数据访问。
3. ZenStack
ZenStack采用了一种独特的方法,从更高的层面——对象关系映射(ORM)——来解决问题。它基于Prisma ORM实现,并支持多种数据库。它扩展了Prisma,允许在数据模式内部建模访问策略,并通过将其注入Prisma查询在运行时强制执行这些策略。
model User {
...
groups Group[]
}
model Group {
...
members User[]
}
model AccessControlList {
user User?
userId Int?
group Group?
groupId Int?
article Article?
articleId Int?
}
model Article {
accessList AccessControlList[]
@@allow('read',
accessList?[
// user access
user == auth()
// group access
|| accessList?[user.groups?[members?[id == auth().id]]]]
)
}
优点和缺点
将授权与数据库相结合有几个明显的优势:
-
单一数据源:避免在授权端重复构建数据模型。数据库是数据及其访问规则的唯一数据源。
-
性能:性能良好,因为权限检查和数据获取合并在一起。不会因读取后丢弃而造成性能损失。
缺点也很明显:
-
数据库锁定:Firebase 和 PostgreSQL 的功能都绑定到特定的数据存储类型上。ZenStack 在数据库选择方面更加灵活,但仍然仅限于 Prisma 支持的数据库。
-
部署:Firebase 和 Postgres RLS 都需要将策略“部署”到数据存储中——这给 CI/CD 带来了额外的负担。
与数据库解耦
轴的这一侧提供了更多选择,包括简单的库和独立服务。这些解决方案提供 API,使您可以将授权策略与数据模型分开建模。它们通常定义涉及 `Private` Subject、Action`PrivateBy`、Condition`PrivateCall` 等的建模原语。使用这些原语构建策略后,您可以询问是否应该允许用户请求。
让我们来看几个属于这一类的例子。
1. 图书馆
根据你使用的编程语言,你可以找到许多专门用于授权的库。例如,CASL是一个 JavaScript 库,它利用一组声明式 API 帮助你构建灵活的授权方案。“给我当前用户可读的列表Article”这个问题可以建模并按如下方式查询:
function isReadable(acl, user) {
return acl.some(permission => {
return (
(permission.userId === user.id || user.groups.includes(permission.groupId))
&& permission.action === 'read'
);
});
}
const articleAbility = (user, article) => defineAbility((can) => {
if (isReadable(article.acl, user)) {
can('read', 'Article', { id: article.id });
}
});
const ability = articleAbility(user1, article1);
ability.can('read', 'Article', article1);
该库并不关心您如何获取策略规则。您可以选择像上面那样将它们硬编码到代码中,也可以从数据库加载,甚至可以设计一个领域特定语言 (DSL) 来编写它们。
2. 授权即服务
更高级的解耦授权形式是独立服务。顾名思义,它与应用程序的数据库解耦,并完全管理自身的状态。授权即服务(Authorization-as-a-Service)的概念由谷歌的 Zanzibar 论文推广开来,该论文旨在为数十亿用户授权数百个相互关联的服务。
我们以warranty.dev为例。该系统提供了一组 REST API,供您定义对象类型和访问策略(称为授权)。一般流程是首先使用 HTTP POST 创建对象类型:
{
"type": "article",
"relations": {
"viewer": {}
}
}
然后创建搜查令:
{
"objectType": "article",
"objectId": "1",
"relation": "viewer",
"subject": {
"objectType": "user",
"objectId": "d6ed6474-784e-407e-a1ea-42a91d4c52b9"
}
}
然后您可以发出授权验证请求:
{
"warrants": [
{
"objectType": "article",
"objectId": "1",
"relation": "viewer",
"subject": {
"objectType": "user",
"objectId": "d6ed6474-784e-407e-a1ea-42a91d4c52b9"
}
}
]
}
优点和缺点
将授权与应用程序的数据模型解耦有以下几个优点:
-
关注点分离:您可以让这两件事相对独立地发展,并且可以在不影响另一件事的情况下替换其中一个实现。
-
同构授权:像 CASL 这样的库允许你在后端和前端以相同的方式建模授权。虽然你不能完全信任前端对访问控制的判断,但它对于有条件地渲染 UI 组件非常有用。另一个场景是授权访问第一方(数据库)和第三方(API)数据。使用与数据库无关的授权机制可以实现同构的实现。
使用解耦授权是要付出代价的:
-
同步:无论你使用库还是服务,都需要从数据库中提取信息并将其同步到授权端,这引入了额外的复杂性。
-
性能:虽然授权库和服务可以针对性能进行高度优化,但在许多情况下,由于策略过滤,您无法避免从数据库中读取数据然后将其丢弃。
决策因素
耦合授权和解耦授权之间最大的区别在于是否可以利用数据库的功能来评估访问策略。
选择解耦的迹象:
- 评估策略并不需要从数据库中加载大量行数据。
- 您的授权要求过于复杂,无法转换为数据库查询。
- 您使用的数据库没有可用的授权解决方案。
考虑是否已婚的迹象:
- 访问策略评估需要大量的数据库交互,因此最好在数据库/ORM级别进行。
- 您的数据模型会快速演变,与之相关的授权规则也会随之变化。将它们紧密耦合起来,可以确保它们始终保持一致。
象限
我们已经了解了耦合式和解耦式授权解决方案的优缺点。为了帮助您更好地了解每个类别中的选择,我在下面汇总了一些值得关注的产品/项目。请注意,这里列出的一些产品,例如 Hasura,其功能远不止授权。但由于 AuthZ 是支撑其其他功能的核心,因此我也将其列入其中。您可以在本文末尾找到产品目录。
在下一部分中,我们将重点关注象限的另一个轴:授权是嵌入在应用程序中还是作为单独的服务。
文章来源:https://dev.to/zenstack/how-to-do-authorization-a-decision-framework-part-1-4ado

