我一直在GraphQL研讨会上工作 ,这是一次很棒的学习经历。 我在GraphQL中必须处理的最棘手的事情之一就是处理复杂的用户权限。 直到一个朋友将我的注意力吸引到Hasura之前,这一直是一个麻烦。
Hasura是一个开源引擎,可连接到您的数据库和微服务,并立即为您提供可用于生产的GraphQL API。
最简单形式的权限
假设您具有以下模型……
![](https://i-blog.csdnimg.cn/blog_migrate/5a07ac9b01a410c8b4ca7b354a62ffdc.png)
并且要求您确保以下几点:
- 只有登录用户才能创建帖子
- 只有管理员用户可以删除帖子
这些都是可行的-您将需要确保GraphQL解析器能够处理逻辑。 可以从您的解析器调用的函数如下所示:
function createPost ( post ) {
const isAuthenticated = await isAuthenticated()
if (!isAuthenticated) {
// return 401
}
return await Post.create(post)
}
为了确保只有管理员用户才能删除帖子,您必须检查用户的角色,授予或权限是否具有管理员值:
function deletePost ( postId, loggedInUserId ) {
const isAuthenticated = await isAuthenticated()
const isAdmin = await isAdmin(loggedInUserId)
if (!isAuthenticated && !isAdmin) {
// return 401
}
return await Post.delete(postId)
}
function isAdmin ( userId ) {
const userRoles = await getUserRoles(userId);
return userRoles.includes( 'admin' )
}
假设您将此示例投放到了生产环境中,并且您的论坛或博客甚至黑客新闻克隆都处于活动状态-祝贺您。 您明天回来,就会意识到用户正在更新其他用户的帖子-令人讨厌。
实现这一目标也不是那么困难。 您将需要检查登录用户的ID,并将其与所需帖子上的外键user_id进行比较。 如果它们匹配,则用户可以编辑,如果不匹配,则应返回401:
function updatePost ( post, loggedInUserId ) {
if (post.user_id !== loggedInUserId) {
// return 401
}
return Post.update(post)
}
我们刚刚看到了日常的CRUD许可策略-很难实现这种授权。 但是,当您开始引入独特的业务逻辑时,事情就会变得棘手。
当权限变得棘手时
几周后,您想鼓励您参与正在进行的任何社交/协作项目。 实现此目的的一种好方法是添加“赞”,“投票”或“拍手”功能。
![](https://i-blog.csdnimg.cn/blog_migrate/63c9b156f0be803368fa600af2cf4df8.png)
添加投票功能不只是添加一张具有正确关系的表格。 您需要考虑以下标准的投票规则:
- 只有登录的用户可以投票
- 帖子创建者无法对他们创建的帖子进行投票
- 选民只能在给定帖子中投票一次
这是这种逻辑的样子:
function vote ( post, loggedInUserId ) {
// 1. Must be logged in
const isAuthenticated = await isAuthenticated()
if (!isAuthenticated) {
// return 401
}
// 2. Disallow votes for post creators
if (post.user_id === loggedInUserId) {
// return 401
}
// 3. Disallow multiple votes per voter
const votes = await Vote.all({
where : {
post_id : post.id,
user_id : loggedInUserId
}
})
if (votes.length > 0 ) {
// return 401
}
// Finally, vote
return await Vote.create({
post_id : post.id,
user_id : loggedInUserId
})
}
看看随着我们的用户需求不断增加,这种情况变得越来越长。 错误有更多的机会潜入。使情况更糟的是,如果我们不断收到这样的要求,那么维护就变得很痛苦。
想象一下,我们决定执行Medium的功能并添加一个鼓掌功能。 请注意,拍手与投票不同,因为允许用户拍手不止一次,但不能永远拍手:
function clap ( post, loggedInUserId ) {
// 1. Must be logged in
// 2. Disallow claps for post creators
// 3. Disallow more than 20 claps per clapper
const claps = await Clap.all({
where : {
post_id : post.id,
user_id : loggedInUserId
}
})
if (claps.length > 20 ) {
// return 401
}
// Finally, clap (again)
return await Clap.create({
post_id : post.id,
user_id : loggedInUserId
})
}
您可以吹嘘一个可以正常工作的应用程序,但是请告诉我,当越来越多的功能请求不断涌入时,您如何管理这几年的工作。
与Hasura一体
首先,首先-删除所有这些解析器。
我喜欢将Hasura称为“后端即服务”。 它处理数据管理,关系,模式,授权/权限,并允许您通过其可扩展性功能专注于自定义业务逻辑。
对于开发人员来说,这意味着您将花费更多的时间在网络上建立用户体验,而将更少的时间用于处理诸如CRUD,角色等重复性任务。
要查看运行中的Hasura并将其用于复杂的权限,您需要进行一些设置。 我不会在本文中详细介绍它们,因为我不会做出正式的文档正义:
- 使用Docker在本地 部署或运行Hasura。
- 创建上面模型图中的表 。
- 如图所示,使用外键将表相互关联
用户→发布关系应如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/28d9d703a539cdbe0fb6ef52edd18f05.png)
使用Hasura获得许可
在我们看到Hasura中的权限是什么样之前,您需要记住一件事-HTTP是无状态的。 因此,对于发送到Hasura后端的每个查询或变异,都需要确保会话变量或JWT可用。
您可以设置身份验证服务器或使用Auth0之类的东西。 无论您选择哪种选择,都有一个不错的文档可以指导您。 我们不会仅仅设置身份验证,而是让我们可以使用会话变量或JWT。 我们可以模拟它们并将它们用于测试您设置的API。
很高兴知道Hasura权限允许您定义行和列的访问。 例如:
- 用户A只能编辑用户A的帖子(行规则)
- 用户A无法更改创建日期(列规则)
到目前为止,这是我们从本文开始就拥有的所有许可规则:
- 只有登录用户才能创建帖子
- 只有管理员用户可以删除帖子
- 只有登录的用户可以投票
- 帖子创建者无法对他们创建的帖子进行投票
- 选民只能在给定帖子中投票一次
1.只有登录用户才能创建帖子
这简直太简单了-我们需要告诉Hasura,在尝试编写/创建帖子之前,它应该检查一个具有用户ID的会话变量,以确保对用户进行身份验证。
如果您所有的数据都向公众开放,则身份验证是没有用的。 因此,在创建身份验证和规则之前,您应该做的第一件事就是将Hasura API设为私有 。
重新加载您的本地主机,应该要求您提供用于将Hasura私有化的秘密:
![](https://i-blog.csdnimg.cn/blog_migrate/c33dcdbaaa92af2426a1a233c30dd6cf.png)
要创建权限,请在导航栏中单击“数据”菜单,然后从左侧选择要向其添加权限的表。 由于我们要确保只有登录的用户才能创建帖子,因此我们应该选择帖子表。
单击“权限”选项卡,然后按照下面的屏幕快照创建插入权限:
![](https://i-blog.csdnimg.cn/blog_migrate/c58df65dd598cf1daf92e28b88257a78.png)
我们暂时不需要对用户进行自定义检查。 我们关心的只是在允许插入之前用户是否已登录。 我们不在乎它是哪个用户。
我们刚刚定义了行权限,但是我们还需要设置用户可以在其中插入值的列。我们不希望用户编辑id
或外键user_id
。
![](https://i-blog.csdnimg.cn/blog_migrate/1c9844a7d1dfa2797f81774e073c6367.png)
保存权限并从GraphQL菜单创建用户。 我们需要该用户的ID来创建帖子。
![](https://i-blog.csdnimg.cn/blog_migrate/d949c09c031d0dd1b782423884cfd4e9.png)
将x-hasura-role
标头设置为user
,这是迄今为止除管理员以外唯一的权限。 请注意,一旦设置了该值,您可以运行的操作将仅限于insert_post
突变:
2.只有管理员用户可以删除帖子
删除就像将x-hasura-role
更新为admin
一样简单:
![](https://i-blog.csdnimg.cn/blog_migrate/279464a89e2489c108bc817ed6e41ed5.png)
3.只有登录的用户可以投票
许可就像我们创建帖子一样:
![](https://i-blog.csdnimg.cn/blog_migrate/f9d81b18b7e38a4e1b95f13c4e7f1a68.png)
现在,只要将x-hasura-role
设置为user
就可以将插入投票作为一项操作:
![](https://i-blog.csdnimg.cn/blog_migrate/722606b12d1960991b840b64f0999725.png)
4.帖子创建者无法对他们创建的帖子进行投票
此权限看起来会稍有不同。 我们必须更新在上一步中创建的用户角色,该角色允许任何用户投票。 我们需要对其进行更新以使用自定义检查,以确保发布表上的user_id列不等于X-Hasura-User-Id
标头值。 X-Hasura-User-Id
是已认证的user
。
![](https://i-blog.csdnimg.cn/blog_migrate/d3dab537a02afd318ad5b72df9dc4d53.png)
为了使投票表属性在投票权限规则中可用,您需要确保该关系不仅止于添加外键 。 您还需要添加一个对象关系 。
保存权限并尝试以创建帖子的用户的身份进行投票:
mutation MyMutation {
insert _vote( objects : { post_id : 1, user_id : 1}) {
affected_rows
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/77bb3dc83554a4cf9fa23bfce8ae2131.png)
您将获得权限错误,但是当您尝试以其他登录用户身份登录时,将可以投票:
![](https://i-blog.csdnimg.cn/blog_migrate/b1e1a6dd52f4ad92041bd2d4e917dff1.png)
(请记住添加一个用户ID值为2的用户)
5.选民只能在给定职位上投票一次
为确保我们不允许用户在特定帖子上投票超过一次,我们需要能够计算出到目前为止该用户对该帖子拥有多少票。 我们有两种选择:
- 使用事件触发器编写自定义验证逻辑。 使用此自定义逻辑,您可以查询和计数。
- 使用Postgres视图,该视图根据票数汇总行。 然后,您可以向该视图添加手动关系,并使用它来创建权限。
我通常选择选项2,因为我需要编写的代码越少,向项目引入错误的机会就越少。
从创建视图开始。
转到“ 数据”菜单,然后单击“ SQL” 。 粘贴以下SQL以创建视图:
CREATE OR REPLACE VIEW "public" . "vote_count" AS
SELECT count (vote.id) AS count ,
vote.user_id,
vote.post_id
FROM vote
GROUP BY vote.user_id, vote.post_id;
这是一个指导您的屏幕截图:
![](https://i-blog.csdnimg.cn/blog_migrate/6e468f314223edb7e2441b02dc3d3022.png)
您应该看到,这会在表列表中添加一个新的表(视图),称为vote_count
:
![](https://i-blog.csdnimg.cn/blog_migrate/133b1fd9ba6b3b8a1d2916b1b715d867.png)
为确保其正常运行,请两次运行以下GraphQL查询:
mutation MyMutation {
insert _vote( objects : { user_id : 2, post_id : 1}) {
affected_rows
}
}
就像它出现在这里:
![](https://i-blog.csdnimg.cn/blog_migrate/f00b0fdc99604e3253920634c74d5aa6.png)
现在,让我们看一下vote
表,您应该看到我们有2票关联于同一用户和同一帖子:
![](https://i-blog.csdnimg.cn/blog_migrate/8e8f026f111fd4c2976d49c1f08f2288.png)
但是有趣的是,当您查看vote_count
视图时:
![](https://i-blog.csdnimg.cn/blog_migrate/ebd4f55de83916388c19a6cb229a2ec5.png)
现在,让我们在vote
表上添加一个关系,以指向vote_count
视图。 由于Views从技术上讲不是表,因此我们必须在Hasura上使用手动关系。 手动关系告诉Hasura将这些实体视为关联,即使它们不是两个表也是如此。
要创建手动关系,请转到投票表,选择“关系”选项卡,然后单击“ 配置” 。 接下来,使用以下值填写表单:
![](https://i-blog.csdnimg.cn/blog_migrate/5d915476d583b029cef47b75dc903a7b.png)
使用此视图设置,您可以使用它来确定某人是否可以投票,方法是确保该人尚未投票。
由于我们已经拥有检查该帖子是否不属于该用户的第一条权限规则,因此我们必须使用运算符来添加多个规则。 此运算符是_and
,在编程语言中的行为类似于和运算符。
![](https://i-blog.csdnimg.cn/blog_migrate/6035de04fec0dc598005b1d70f02657a.png)
如框1所示, _and
运算符是一个数组。 在可以授予权限之前,此数组中的每个项目都必须解析为true。 这是有道理的,因为我们希望在用户投票之前满足以下条件:
- 尝试投票的用户未创建该帖子(已实现)
- 用户只能投票一次
第二条规则是我们将在框2中突出显示的字段中实现的规则。在此之前,我们需要讨论可能的情况:
- 如果视图中的计数字段小于1,则用户可以投票。
- 如果用户不在视图中,则该用户可以投票。
似乎我们在说同样的话,但事实并非如此。 如果您熟悉JavaScript,它就像一个变量,值为0
vs undefined
。 情况1的值为0; 情况2是未定义的值。 如果仅占1,则将出现未处理的边缘情况,这将导致错误。 我们需要确保,如果发生以上任何一种情况,用户都应该能够投票。
这是编程语言中的经典或操作员用例。 我们也可以在Hasura中使用此运算符:
![](https://i-blog.csdnimg.cn/blog_migrate/9a635ceadd735b978921ce29c628a001.png)
这是这里发生的事情的完整分解:
一个 and
经营者的检查:
- 选民不是该职位的所有者
-
or
运算符解析为true
该 or
任一,如果任何属实将解析为真正的运营商检查:
- 用户的投票数小于1(所以0)
- 或该行为空(_not)
使用此设置,清除投票表,然后尝试使用GraphQL查询窗口以不同于创建帖子的用户的用户身份进行投票。 请注意,投票是第一次进行。 现在再试一次,您将得到一个权限错误。
您只需在GraphQL中实现一些功能强大的权限策略,而无需一行代码。 我并不感到意外; 是2020年!
这只是使用Hasura可以使用权限进行操作的冰山一角。 您可以看一下有关可能的文档 。 您也可以探索这个Github规模的权限场景。
先前发布在https://dev.to/codebeast/handling-complex-user-permissions-in-graphql-538p
From: https://hackernoon.com/how-to-handle-complex-user-permissions-in-graphql-kke53yfg