使用CASL在Expressjs中进行访问管理



在支持身份验证的现代应用程序中,我们经常希望根据用户的角色更改用户可见的内容。 例如,访客用户可以看到文章,但是只有注册用户或管理员看到删除该文章的按钮。


随着角色的增加,管理这种可见性可能是一场噩梦。 您可能已经编写或看到了如下代码:


if (user.role === ADMIN || user.auth && post.author === user.id) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) } 

这样的代码分布在整个应用程序中,通常在客户更改需求或要求添加其他角色时成为一个大问题。 最后,您需要检查所有此类if-s并添加其他检查。


在本文中,我将展示一种使用称为CASL的库在Expressjs API中实现权限管理的替代方法。 它极大地简化了访问控制,并允许您将前面的示例重写为如下所示:


 if (req.ability.can('read', post)) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) } 

CASL的新手? 我建议阅读什么是CASL?


注意:本文最初发布于Medium。


演示应用


作为测试应用程序,我为Blog制作了一个相当简单的REST API 。 该应用程序包含3个实体( UserPostComment )和4个模块(每个实体一个模块,另一个用于授权验证的模块)。 所有模块都可以在src/modules文件夹中找到。


该应用程序使用猫鼬模型, passportjs身份验证和基于CASL的授权(或访问控制)。 使用API​​,用户可以:


  • 阅读所有文章和评论
  • 创建一个用户(即注册)
  • 如果授权,则管理您自己的文章(创建,编辑,删除)
  • 如果授权,则更新个人信息
  • 管理自己的评论(如果获得授权)

要安装此应用程序,只需从github克隆它,然后运行npm installnpm start 。 您还需要启动MongoDB服务器,该应用程序将连接到mongodb://localhost:27017/blog 。 一切准备就绪后,您可以进行一些尝试,并使其更加有趣,请从db/文件夹导入基本数据:


 mongorestore ./db 

或者,您可以按照README项目文件中的说明进行操作,也可以使用我的Postman收藏


诀窍是什么?


首先,CASL的巨大优势在于,它允许您在一个位置为所有用户确定访问权限! 其次,CASL并不关注用户是谁,而是关注他可以做什么,即 在其功能上。 这使您可以将这些功能分配给不同的角色或用户组,而无需进行不必要的工作。 这意味着我们可以为授权用户和未授权用户注册访问权限:


 const { AbilityBuilder, Ability } = require('casl') function defineAbilitiesFor(user) { const { rules, can } = AbilityBuilder.extract() can('read', ['Post', 'Comment']) can('create', 'User') if (user) { can(['update', 'delete', 'create'], ['Post', 'Comment'], { author: user._id }) can(['read', 'update'], 'User', { _id: user._id }) } return new Ability(rules) } const ANONYMOUS_ABILITY = defineAbilitiesFor(null) module.exports = function createAbilities(req, res, next) { req.ability = req.user.email ? defineAbilitiesFor(req.user) : ANONYMOUS_ABILITY next() } 

现在让我们解析上面编写的代码。 在defineAbilitiesFor(user) AbilityBuilder创建了一个AbilityBuilder -a实例,其提取方法将该对象分为两个cancannotrules数组( cannot在此代码中使用)的两个简单函数。 接下来,使用对can函数的调用,我们确定用户可以执行的操作:第一个参数传递动作(或动作数组),第二个参数是执行动作(或类型数组)的对象的类型,条件对象可以作为第三个可选参数传递。 在检查类实例的访问权限时使用条件对象,即 它检查post对象和user._idauthor属性是否相等,如果相等,则返回true ,否则返回false 。 为了清楚起见,我将举一个例子:


 // Post is a mongoose model const post = await Post.findOne() const user = await User.findOne() const ability = defineAbilitiesFor(user) console.log(ability.can('update', post)) //  post.author === user._id,   true 

接下来,使用if (user)我们确定授权用户的访问权限(如果未授权用户,则不知道他是谁,并且我们没有包含有关用户信息的对象)。 最后,我们返回Ability类的实例,借助它我们将检查访问权限。


接下来,我们创建ANONYMOUS_ABILITY常量,它是未授权用户的Ability类的实例。 最后,我们导出快速中间件,该中间件负责为特定用户创建Ability实例。


测试API


让我们测试一下Postman发生了什么。 首先,您需要获取accessToken,为此发送请求:


 POST /session { "session": { "email": "casl@medium.com", "password": "password" } } 

您得到这样的响应:


 { "accessToken": "...." } 

该令牌必须插入到Authorization header并与所有后续请求一起发送。


现在,让我们尝试更新文章。


 PATCH http://localhost:3030/posts/597649a88679237e6f411ae6 { "post": { "title": "[UPDATED] my post title" } } 200 Ok { "post": { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } } 

一切正常。 但是,如果我们更新别人的文章怎么办?


 PATCH http://localhost:3030/posts/59761ba80203fb638e9bd85c { "post": { "title": "[EVIL ACTION] my post title" } } 403 { "status": "forbidden", "message": "Cannot execute \"update\" on \"Post\"" } 

出错了! 如预期的那样:)


现在,让我们想象一下,对于我们博客的作者,我们想要创建一个页面,使他们可以看到所有可以更新的帖子。 从特定逻辑的角度来看,这并不困难,您只需要选择author等于user._id所有文章user._id 。 但是我们已经在CASL的帮助下注册了这样的逻辑,从数据库中获取所有此类文章而无需编写额外的请求将非常方便,并且如果权限发生更改,则您将不得不更改请求-额外的工作:)。


幸运的是,CASL还有一个npm软件包- @ casl / mongoose 。 这个包允许您根据特定的权限从MongoDB查询记录! 对于猫鼬,此软件包提供了一个插件,该插件向模型添加了accessibleBy(ability, action)方法。 使用此方法,我们还将从数据库请求记录(有关详细信息,请参阅CASL文档README软件包文件 )。


这正是实现/posts handler的方式(我还添加了指定需要检查权限的操作的功能):


 Post.accessibleBy(req.ability, req.query.action) 

因此,为了解决前面描述的问题,只需添加参数action=update


 GET http://localhost:3030/posts?action=update 200 Ok { "posts": [ { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } ] } 

总结


多亏了CASL,我们才有了管理访问权限的好方法。 我非常确定类型构造


 if (ability.can('read', post)) ... 

比起更清晰,更容易


 if (user.role === ADMIN || user.auth && todo.author === user.id) ... 

使用CASL,我们可以更清楚地了解代码的作用。 另外,这种检查肯定会在我们的应用程序中的其他地方使用,而CASL正是在这里帮助避免代码重复。


希望您和我对创建CASL一样感兴趣,对阅读CASL感兴趣。 CASL有很好的文档 ,您肯定会在其中找到很多有用的信息,但是如果在聊天中随意询问并在github上添加星号;)

Source: https://habr.com/ru/post/zh-CN414951/


All Articles