像专业人士一样授权用户:帮助你使用 Node.js 实现访问控制的库
雷穆特
ZenStack
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
在Web应用程序中实施安全措施虽然不如开发炫酷的新功能那样令人兴奋,但却至关重要。你往往不会因为做好安全工作而得到经理的认可。但是,无论你如何出色地解决用户问题,糟糕的安全措施都可能对优秀的产品造成毁灭性的打击。
本文将探讨安全的一个重要方面:授权。我将介绍几个库,帮助你更轻松地在 Node.js 应用中实现更可靠的授权层。
背景
身份验证与授权
身份验证和授权是安全应用程序的两大支柱。它们相关但又有所不同,人们常常将它们混淆,并在不应该的情况下互换使用这两个术语。首先,让我们澄清几点:
身份验证的核心在于验证用户的身份。它将一些凭证转换为系统能够识别的身份信息。这些凭证可以很简单,例如邮箱/密码或邮箱/一次性 密码 (OTP) 。现代应用程序通常倾向于使用基于 OAuth 的 身份验证,它将身份验证委托给受信任的第三方,从而避免在自己的系统中保存敏感凭证。
另一方面,授权控制着“谁可以对哪个资产执行什么操作”。假设用户的身份已经通过身份验证,那么授权在概念上可以理解为如下功能:
请求(身份、操作、资产)→ 允许 | 拒绝
操作通常被定义为 CRUD——“创建”、“读取”、“更新”和“删除”;但也可以根据实施者的需要自由定义。
授权和“访问控制”本质上是一回事。
基于角色的访问控制
基于角色的访问控制 (RBAC) 是一种传统的授权建模方式。简而言之,就是定义角色,将用户分配到各个角色,然后按角色而非单个用户来控制资产访问权限。用户角色可以是任何内容,例如权限、部门、职责等等。
例如:
管理员用户可以删除任何内容。
销售部门可以查看收入报告。
基于属性的访问控制
与基于角色的访问控制 (RBAC) 不同,基于属性的访问控制 (ABAC) 根据用户及其尝试访问的资产的属性来授予权限。属性可以是任何内容,例如博客文章的作者、待办事项的完成状态、CRM 中交易的当前阶段等等。
例如:
博客文章只能由作者本人删除。
交易阶段结束后,交易就无法更新了。
RBAC 和 ABAC 并非互斥。在实践中,您经常需要将它们结合使用:使用 RBAC 在资产类型级别进行粗粒度控制,而使用 ABAC 实现更动态的行为。
想了解更多详情, 请查看 这篇文章。
示例场景
为了便于说明,在本文中,我将使用以下 Blogger 应用程序作为示例场景。
Blogger 应用授权要求:
用户角色:成员和管理员
资产:帖子
资产属性:
帖子所有者:用户
Post.published:布尔值
操作:CRUD
规则:
管理员拥有所有帖子的完整访问权限
所有者对其拥有的帖子拥有完全访问权限。
所有用户都拥有对所有已发布帖子的“阅读”权限
所有其他请求均被拒绝
背景设置完毕,让我们深入了解一下库。
访问控制
家
介绍
AccessControl 是一个成熟的授权库,自 2016 年推出以来一直广受欢迎,最新版本发布于 2018 年。它提供简洁流畅的 API,允许您通过代码构建访问策略。这些 API 既简单又灵活。
该库的方法非常简单。设置策略时,您需要引入角色和资源,并指定 CRUD 操作规则。操作分为两类:“自身”和“任意”。通常,“自身”控制对当前用户拥有的资源的 CRUD 操作,而“任意”控制对所有资源的 CRUD 操作。然而,这种理解仅仅是一种约定俗成,开发者可以自行决定如何解释这些规则。设置完成后,您可以提出诸如“角色 X 能否更新他自己的资源 Y?”或“角色 X 能否读取任何资源 Y?”之类的问题。
我们的示例场景可以按如下方式实现:
// setting up access policies
import { AccessControl } from ' accesscontrol ' ;
const ac = new AccessControl ();
ac . grant ( ' member ' )
. createOwn ( ' post ' )
. readOwn ( ' post ' )
. updateOwn ( ' post ' )
. deleteOwn ( ' post ' );
ac . grant ( ' admin ' )
. createAny ( ' post ' )
. readAny ( ' post ' )
. updateAny ( ' post ' )
. deleteAny ( ' post ' );
// freeze our policy
ac . lock ();
// make queries
// -> false
console . log ( ac . can ( ' member ' ). updateAny ( ' post ' ). granted );
// -> false
console . log ( ac . can ( ' member ' ). updateOwn ( ' post ' ). granted );
// -> true
console . log ( ac . can ( ' admin ' ). deleteAny ( ' post ' ). granted );
Enter fullscreen mode
Exit fullscreen mode
对于新用户来说,可能会非常惊讶的是,尽管该库提供了“own”和“any”的概念,但它实际上根本不会检查这些概念。在查询策略引擎之前,您有责任确认用户是否“拥有”某个资源。换句话说,当您调用“readOwn”时,引擎会假定您已经验证了所有权。这类似于授权和身份验证之间的关系;虽然授权依赖于用户的身份,但它并不负责验证身份。
在 Web 后端中正确使用它的方式如下所示(这里以 Express.js 为例):
app . put ( ' /post/:id ' , async ( req , res ) => {
const post = await loadPost ( req . params . id );
if ( ! post ) {
res . status ( 404 ). send ();
return ;
}
let permission = ac . can ( req . user . role ). updateAny ( ' post ' );
if ( ! permission . granted ) {
if ( post . ownerId === req . user . id ) {
permission = ac . can ( req . user . role ). readOwn ( ' post ' );
}
if ( permission . granted ) {
// do post update here
} else {
// resource is forbidden for this user/role
res . status ( 403 ). end ();
}
});
Enter fullscreen mode
Exit fullscreen mode
细心的读者可能已经注意到,我们的政策定义并未涵盖其中一项要求:
❌ 所有用户都拥有对所有已发布帖子的“阅读”权限
你说得对;很遗憾,这无法通过访问控制来实现。不过,我们可以通过“灵活运用”“拥有”的概念来解决这个问题。正如前面所说,“拥有”的含义由你决定,而不是库本身。所以,在我们的例子中,如果帖子被发布,你的代码可以在执行“读取”操作时将其视为任何用户的“拥有”。问题解决了。
从本质上讲, 访问控制 就是一个权限推断系统。对于像我们示例这样简单的场景来说,这看起来可能有点小题大做,但当你的应用发展成拥有复杂的多层角色层级和多种资源时,拥有一个可以声明式定义访问策略的中心位置,而无需编写推断代码,就能带来巨大的好处。它能提高可维护性,并降低安全漏洞的发生概率。
优点
易于使用的流畅 API。
概念简单(虽然一开始有点绕),灵活性好。
与框架和存储无关。
支持字段可见性控制(演示中未显示)。
缺点
作为开发人员,您必须处理更多事情:确定用户角色、检查资源所有权等等。
如果 ABAC 能得到更自然的支持(而不是像我们这样对“自身”概念进行修改),那就太好了。
未与存储集成。例如,如果您需要向用户返回“可读”帖子列表,则必须从数据库中获取所有帖子,然后使用 访问控制 进行筛选。
已停止维护。
最佳匹配
如果您想要一个简洁的授权库,并且不希望它干扰您的技术栈选择,那么 AccessControl 凭借其优雅的模型和易于使用的 API,将是一个不错的选择。您可以使用它来维护关键的授权规范,并保持添加自定义逻辑的自由。
雷穆特
家
全栈 CRUD 简化版,采用 SSOT TypeScript 实体
Remult是什么?
Remult 使用 TypeScript 实体 作为以下方面的单一数据源:✅ CRUD + 实时 API,✅ 前端类型安全的 API 客户端,以及 ✅ 后端 ORM。
⚡ 零样板代码的 CRUD + 实时 API, 支持分页、排序和筛选
👌 为 API 查询、变更和 RPC 提供 全栈类型安全,无需代码生成
✨ 输入验证 只需定义一次 ,即可在后端和前端同时运行,从而实现最佳用户体验。
🔒 细粒度的 基于代码的 API 授权
😌 可逐步采用
Remult 支持所有主流数据库 ,包括:PostgreSQL、MySQL、SQLite、MongoDB、MSSQL 和 Oracle。
Remult 与前端和后端框架无关 ,并带有 Express、Fastify、Next.js、Nuxt、SvelteKit、SolidStart、Nest、Koa、Hapi 和 Hono 的适配器。
想亲身体验Remult吗? 试试我们的互动式在线教程吧 。
Remult 为前端和后端都提供了一致的查询语法 ……
介绍
Remult 是一个用于实现 CRUD 应用的工具包。它提供了一种代码优先的方式来定义应用实体的模式,并允许您将基于角色的访问控制 (RBAC) 策略附加到该模式。然后,它会动态生成一个 RESTful API,该 API 公开受策略规则保护的 CRUD 操作。之后,您可以将该 API 挂载到 Express 或 Next.js 等服务器,并使用它构建前端功能。
让我们看看如何使用 Remult 来表达我们的示例场景。首先, Post实体可以定义为一个 TypeScript 类。请注意,实体带有表示访问策略的注解。
import { Allow , Entity , Fields } from ' remult ' ;
import { Roles } from ' ./Roles ' ;
@ Entity < Post > ( ' post ' , {
allowApiRead : Allow . authenticated ,
allowApiInsert : Allow . authenticated ,
// a post can be updated by admin or its owner
allowApiUpdate : ( remult , post ) =>
remult . authenticated () &&
( remult . user ! . roles ! . includes ( Roles . admin ) ||
post ! . ownerId === remult . user ! . id ),
// a post can be deleted by admin or its owner
allowApiDelete : ( remult , post ) =>
remult . authenticated () &&
( remult . user ! . roles ! . includes ( Roles . admin ) ||
post ! . ownerId === remult . user ! . id ),
})
export class Post {
@ Fields . uuid ()
id ! : string ;
@ Fields . string ()
title = '' ;
@ Fields . boolean ()
published = false ;
@ Fields . string ()
ownerId ! : string ;
}
Enter fullscreen mode
Exit fullscreen mode
然后您可以使用存储库 API(在前端和后端代码中)来访问实体。
// the code works in both frontend and backend
const posts = remult . repo ( Post ). find ();
...
await remult . repo ( Post ). update ( id , { title : " 'my title'}); "
Enter fullscreen mode
Exit fullscreen mode
在底层,存储库 API 调用生成的后端服务,这些服务受到授权策略的保护。
您可能已经注意到,我尚未实现以下要求:
❌ 所有用户都拥有对所有已发布帖子的“阅读”权限
遗憾的是,我们遇到了 Remult 的一个限制,即在“读取”策略中无法访问资产的属性(此处 ownerId指 Post属性值)。为了解决这个问题,您需要实现一些后端方法来支持上述要求,例如:
import { BackendMethod , remult } from " remult " ;
export class PostsController {
@ BackendMethod ({ Allow . authenticated })
static async find () {
// implement your custom authorization here
}
}
Enter fullscreen mode
Exit fullscreen mode
对于非管理员用户,您需要调用此后端 API。虽然这种方法可行,但遗憾的是,由于信息现在分散在多个位置,因此很大程度上抵消了将访问策略附加到实体所带来的好处。
优点
访问策略与数据模型位于同一位置。
带有授权的 CRUD 服务会自动生成。
与用户界面框架无关。
积极开发中。
缺点
Remult 本身也是一个 ORM,所以如果您已经决定使用另一个 ORM(如 TypeORM 或 Prisma),就会发生冲突。
准入政策的表达能力有限。
最佳匹配
如果你的应用授权策略比较简单,并且你想快速搭建应用,那么 Remult 可能是一个不错的选择。它可以为你生成后端服务和前端库。如果你喜欢代码优先的 ORM 工具(例如 TypeORM ),那么你也会发现它很容易上手。
然而,更复杂的应用程序很容易超出 Remult 的舒适区,你最终可能会编写大量的后端方法,这与实现正式的后端并没有太大的区别。
ZenStack
家
全栈 TypeScript 工具包,通过灵活的授权层增强 Prisma ORM,支持 RBAC/ABAC/PBAC/ReBAC,提供自动生成的类型安全 API 和前端钩子。
ZenStack
<a href="https://www.npmjs.com/package/zenstack" rel="nofollow">
<img src="https://camo.githubusercontent.com/26ec5be4f7fb0d640c7b8f04f2fa6aeb0e764f39a7f7c152d7df7472246664bd/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f7a656e737461636b" data-canonical-src="https://img.shields.io/npm/v/zenstack" style="max-width: 100%;">
</a>
<a href="https://www.npmjs.com/package/zenstack" rel="nofollow">
<img src="https://camo.githubusercontent.com/198bdca61f5551a813edfd4713bf13da4b79ef80d9b0f4fb8bd0ed331c436bd7/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f646d2f7a656e737461636b" data-canonical-src="https://img.shields.io/npm/dm/zenstack" style="max-width: 100%;">
</a>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/zenstackhq/zenstack/actions/workflows/build-test.yml/badge.svg"><img src="https://github.com/zenstackhq/zenstack/actions/workflows/build-test.yml/badge.svg" style="max-width: 100%;"></a>
<a href="https://twitter.com/zenstackhq" rel="nofollow">
<img src="https://camo.githubusercontent.com/3d4bea69690cceee139872f76970b79bf15ceacd2ea395b265aaf2f39c3d8dec/68747470733a2f2f696d672e736869656c64732e696f2f747769747465722f75726c3f7374796c653d736f6369616c2675726c3d68747470732533412532462532466769746875622e636f6d2532467a656e737461636b68712532467a656e737461636b" data-canonical-src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Fzenstackhq%2Fzenstack" style="max-width: 100%;">
</a>
<a href="https://discord.gg/Ykhr738dUe" rel="nofollow">
<img src="https://camo.githubusercontent.com/39f8529d1c1792c76c747600955d6a5d66e95c6d184a08671fafedf22963f079/68747470733a2f2f696d672e736869656c64732e696f2f646973636f72642f31303335353338303536313436353935393631" data-canonical-src="https://img.shields.io/discord/1035538056146595961" style="max-width: 100%;">
</a>
<a href="https://github.com/zenstackhq/zenstack/blob/main/LICENSE">
<img src="https://camo.githubusercontent.com/4c8edb73fd95ab52c2eceb5635ead9af07567d17386848ed80d79358fe48b72a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e" data-canonical-src="https://img.shields.io/badge/license-MIT-green" style="max-width: 100%;">
</a>
Enter fullscreen mode
Exit fullscreen mode
它是什么
ZenStack 是一个 Node.js/TypeScript 工具包,可以简化 Web 应用后端的开发。它通过灵活的授权层和自动生成的类型安全的 API/钩子增强了 Prisma ORM ,从而充分发挥了其在全栈开发中的潜力。
我们的目标是让您节省编写样板代码的时间,专注于构建真正的功能!
工作原理
请访问👉🏻 zenstack.dev 阅读完整文档。欢迎加入 Discord 群组 ,提出反馈意见和问题。
ZenStack 通过以下四个层逐步扩展 Prisma 的功能:
1. ZModel——一种扩展的 Prisma 模式语言
ZenStack引入了一种名为“ZModel”的数据建模语言——它是Prisma模式语言的超集。它通过自定义属性和函数扩展了Prisma模式,并在此基础上围绕Prisma实现了一个灵活的访问控制层。
// base.zmodel
abstract model Base {
id String @id
author User @relation ( fields : [ authorId ] , references : [ id …
Enter fullscreen mode
Exit fullscreen mode
介绍
免责声明:我是 ZenStack 的创建者。
ZenStack 是一个用于简化使用 Next.js 构建安全 CRUD 应用的工具包。它与 Remult 有一些相似之处:都基于声明式访问策略模型生成受保护的后端服务和前端库。但它们在几个重要方面也存在差异:
ZenStack 采用模式优先的架构。
它基于现有的 ORM( Prisma ),而不是试图替换现有的 ORM。
它的访问控制策略引擎既直观又强大。
要使用 ZenStack 进行授权,您需要在现有的 Prisma schema 中添加额外的访问策略声明。让我们通过示例场景来看一下:
model Post {
id String @ id @ default ( cuid ())
title String
published Boolean @ default ( false )
owner User ? @ relation ( fields : [ authorId ], references : [ id ])
ownerId String ?
// must signin to access any post
@@ deny ( ' all ' , auth () == null )
// allow full CRUD by owner or admin
@@ allow ( ' all ' , owner == auth () || auth (). role == ' Admin ' )
// published posts are readable to everyone (logged in)
@@ allow ( ' read ' , published == true )
}
Enter fullscreen mode
Exit fullscreen mode
现在我们已经无需任何破解手段就涵盖了所有四条授权规则。
ZenStack 的核心是实现了 Prisma schema 的超集,并引入了两个新的注解:`@ User` @@allow和 @@deny`@Entity`,用于表达访问策略。策略规则可以访问当前用户和当前实体,并结合这两个信息做出判断。它实际上能够使用关系字段和集合字段来表达复杂的规则,例如:
✅ 帖子可以由帖子编辑成员用户进行更新。
model Post {
...
// a relation field storing editors of this post
editors User []
...
// use a Collection Predicate to check if the current user
// matches any entity in editors field
@@ allow ( ' update ' , editors ?[ id == auth (). id ])
}
Enter fullscreen mode
Exit fullscreen mode
根据架构,生成用于 CRUD 操作的安全 RESTful 服务,以及用于使用这些服务的客户端 React hooks。
// sample front-end code
const { find , update } = usePost ();
const { data : publishedPosts } = find ({ where : { published : true }});
...
await update ( postId , { title : " newTitle }); "
Enter fullscreen mode
Exit fullscreen mode
如果策略语言不足以表达您的规则,您可以随时实现自定义的 Next.js API 端点,增强策略行为,或者完全绕过它并直接访问数据库。
优点
访问策略与数据模型位于同一位置。
它是基于优秀的 ORM 系统(Prisma)构建的扩展。
无缝结合了基于角色的访问控制 (RBAC) 和基于属性的访问控制 (ABAC)。
授权与存储深度集成,因此策略检查完全下推到数据库,以实现最佳性能和可扩展性。
具有极高的访问策略定义灵活性。
积极开发中。
缺点
目前仅限 Next.js。
必须使用 Prisma ORM。
最佳匹配
如果您正考虑使用 Prisma 作为 ORM,那么 ZenStack 可能非常适合您,因为它的语言语法与 Prisma 的模式语言基本兼容。如果您预计会采用较为复杂的授权策略,同时又希望将策略规则与数据放在一起以确保单一数据源,那么 ZenStack 也是不错的选择。
包起来
构建安全的 Web 应用充满挑战,而实现正确的授权机制是其中至关重要的一环。希望您阅读愉快,也希望文中介绍的工具能对您有所帮助。如果您发现了其他未提及的实用工具,请留言告诉我,我会在以后的文章中进行介绍。
玩得开心!
文章来源:https://dev.to/zenstack/authorize-users-like-a-pro-libraries-that-help-you-implement-access-control-with-nodejs-5109