多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于 Prisma 与 graphql-yoga 实现 GraphQL 权限控制:从数据模型到 Resolver 实战

基于 Prisma 与 graphql-yoga 实现 GraphQL 权限控制:从数据模型到 Resolver 实战 后端数据库GraphQL【免费下载链接】prisma1 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL MongoDB) [deprecated]项目地址https://gitcode.com/gh_mirrors/pr/prisma1点击查看免费下载本文是一篇面向 GraphQL 服务端开发者的实战指南讲解如何在使用 Prisma 与graphql-yoga构建 GraphQL 服务器时实现细粒度的权限控制。文章以node-advancedboilerplate 为起点逐步为数据模型引入ADMIN/CUSTOMER角色枚举并在各 resolver 中落地 数据访问检查data access check模式——通过ctx.db.exists校验请求者的身份与数据归属从而让feed、drafts、post、publish、deletePost五个操作具备各自独立的权限语义。读完本文你将掌握基于 Prisma 的 GraphQL API 权限设计范式、exists检查的底层原理以及用 GraphQL Playground Authorization请求头验证权限的完整流程。前置准备安装 GraphQL CLI 并创建项目在开始之前需要先安装 GraphQL CLI它是用来引导bootstrapGraphQL 项目的命令行工具npm install -g graphql-cli注意本教程不要求单独安装 Prisma CLI因为prisma已作为node-advancedboilerplate 的development dependency被列出可以通过yarn前缀调用其命令例如yarn prisma deploy或yarn prisma playground。如果你的机器上已经全局安装了prismanpm install -g prisma则可以省略yarn前缀。接着使用 GraphQL CLI 创建一个名为permissions-example的新服务boilerplate 指定为node-advancedgraphql create permissions-example --boilerplate node-advanced命令执行完成后输出中会给出一个公网 Prisma endpoint形如https://eu1.prisma.sh/public-example-123/permissions-example/dev。你也可以选择将 Prisma 服务部署到本地需要安装 Docker但为了简单直接本教程使用公网 demo cluster。注意node-advancedboilerplate 已内置开箱即用的身份认证authentication这为后续实现权限规则提供了getUserId(ctx)这类辅助函数基础。创建完成后permissions-example目录中包含两部分内容GraphQL 服务器源码基于graphql-yoga主要文件位于src/如src/index.js、src/schema.graphql、src/resolvers/Prisma 数据库服务配置包括prisma.yml服务配置与database/datamodel.graphql数据模型定义。该 GraphQL 服务器基于以下数据模型构建type Post { id: ID! unique createdAt: DateTime! updatedAt: DateTime! isPublished: Boolean! default(value: false) title: String! text: String! author: User! } type User { id: ID! unique email: String! unique password: String! name: String! posts: [Post!]! }这里unique、default都是 Prisma 数据模型指令datamodel directivesunique为该字段建立唯一约束default(value: false)为字段声明默认值。数据模型定义在database/datamodel.graphql中Prisma 服务的prisma.yml会通过datamodel字段指向它——关于 Prisma 服务配置中datamodel的完整说明可参考 Demo Server 设置教程 与 prisma.yml 相关文档若该文件不存在则以实际目录为准。为数据模型添加ADMIN角色本教程的场景中User要么是拥有特殊访问权限的管理员admin要么是普通客户customer。为了区分这两类用户需要修改数据模型引入一个枚举类型来定义角色。打开database/datamodel.graphql将User类型更新为如下形式并同时新增Role枚举type User { id: ID! unique email: String! unique password: String! name: String! posts: [Post!]! role: Role! default(value: CUSTOMER) } enum Role { ADMIN CUSTOMER }需要注意role字段与password字段一样不会通过 GraphQL 服务器的 API 暴露——因为应用 schemaapplication schema即src/schema.graphql中定义的User类型并没有包含该字段。应用 schema 最终决定了哪些数据会暴露给客户端应用。也就是说数据模型datamodel控制数据库层有什么应用 schema 控制API 层暴露什么两者是相互独立的。修改完成后需要重新部署数据库服务让 Prisma API 同步更新yarn prisma deploy部署成功后数据模型与 Prisma API 都会包含User类型的role字段。后续在 resolver 中就可以通过ctx.db.exists.User({ id: userId, role: ADMIN })来判定请求者是否为管理员。定义权限需求清单src/schema.graphql中的应用 schema 暴露了以下查询与变更type Query { feed: [Post!]! drafts: [Post!]! post(id: ID!): Post! me: User } type Mutation { signup(email: String!, password: String!, name: String!): AuthPayload! login(email: String!, password: String!): AuthPayload! createDraft(title: String!, text: String!): Post! publish(id: ID!): Post! deletePost(id: ID!): Post! }本文聚焦与Post类型相关的 resolver为它们定义如下权限需求操作权限需求feed无任何权限限制所有人包括未认证用户都可读取已发布的Post列表drafts每个用户只能访问自己的草稿即自己是该Post的authorpost只有Post的author或ADMIN用户可以通过post查询读取该节点publish只有Post的author可以发布它deletePost只有Post的author或ADMIN用户可以删除它这份清单将作为后续 resolver 改造的验收标准。实现权限规则resolver 中的数据访问检查用 Prisma 与graphql-yoga实现权限规则的基本思路是在每个 resolver 中实现一次 数据访问检查data access check。只有当检查通过时该操作query、mutation 或 subscription才会借助prisma-binding转发给 Prisma 服务执行检查失败则抛出错误。下面逐个改造相关 resolver。feed无需检查feed面向所有人开放因此不需要实现任何检查。drafts按 author 过滤drafts的权限需求是每个用户只能访问自己的草稿。当前draftsresolver 的实现如下drafts(parent, args, ctx, info) { const id getUserId(ctx) const where { isPublished: false, author: { id } } return ctx.db.query.posts({ where }, info) },这个实现其实已经满足了需求它通过getUserId(ctx)从请求上下文即Authorization头解析出的 JWT中取出当前登录用户的id并在where过滤条件中同时限定isPublished: false草稿和author.id归属。因此这里无需任何改动。注意这里向 Prisma 下发的where过滤体现了 Prisma 关系过滤relation filter的能力——author: { id }是嵌套在PostWhereInput中的关系查询条件由 Prisma 引擎在数据库层完成过滤而不是在内存中做权限判断。postauthor 或 ADMIN 双重校验post的权限需求是只有Post的author或ADMIN用户可以通过post查询读取该节点。当前实现非常简单post(parent, { id }, ctx, info) { return ctx.db.query.post({ where: { id } }, info) }它没有做任何身份校验。现在需要确保只有当请求者是该Post的author或是ADMIN用户时才返回Post节点。这里将使用prisma-binding提供的exists函数。按如下方式更新src/resolvers/Query.js中的实现async post(parent, { id }, ctx, info) { const userId getUserId(ctx) const requestingUserIsAuthor await ctx.db.exists.Post({ id, author: { id: userId, }, }) const requestingUserIsAdmin await ctx.db.exists.User({ id: userId, role: ADMIN, }) if (requestingUserIsAdmin || requestingUserIsAuthor) { return ctx.db.query.post({ where: { id } }, info) } throw new Error( Invalid permissions, you must be an admin or the author of this post to retrieve it., ) }这里的两次exists调用分别收集两类信息ctx.db.exists.Post({ id, author: { id: userId } })判断发送请求的User是否确实是所请求Post的authorctx.db.exists.User({ id: userId, role: ADMIN })判断发送请求的User是否为ADMIN。只要二者之一成立就正常返回Post否则抛出权限不足错误。注意exists返回的是Promiseboolean因此需要把 resolver 改成async并用await等待结果。publish仅 author 可发布publish变更的权限需求是只有Post的author可以发布它。该 resolver 位于src/resolvers/Mutation/post.js当前实现如下async publish(parent, { id }, ctx, info) { const userId getUserId(ctx) const postExists await ctx.db.exists.Post({ id, author: { id: userId }, }) if (!postExists) { throw new Error(Post not found or youre not the author) } return ctx.db.mutation.updatePost( { where: { id }, data: { isPublished: true }, }, info, ) },现有的exists检查已经确保请求者是待发布Post的author因此需求已经满足无需任何改动。注意这里的一个工程细节把不存在该 Post和你不是作者合并成同一个错误信息Post not found or youre not the author避免向外泄露资源存在性信息这是一种常见的安全实践。deletePostauthor 或 ADMIN 可删除deletePost变更的权限需求是只有Post的author或ADMIN用户可删除。当前 resolver 位于src/resolvers/Mutation/post.jsasync deletePost(parent, { id }, ctx, info) { const userId getUserId(ctx) const postExists await ctx.db.exists.Post({ id, author: { id: userId }, }) if (!postExists) { throw new Error(Post not found or youre not the author) } return ctx.db.mutation.deletePost({ where: { id } }) },现有exists检查只覆盖了请求者是作者这一种情况。如果请求者是ADMIN该Post也应该被删除。因此需要补充管理员校验async deletePost(parent, { id }, ctx, info) { const userId getUserId(ctx) const postExists await ctx.db.exists.Post({ id, author: { id: userId }, }) const requestingUserIsAdmin await ctx.db.exists.User({ id: userId, role: ADMIN, }) if (!postExists !requestingUserIsAdmin) { throw new Error(Post not found or you dont have access rights to delete it.) } return ctx.db.mutation.deletePost({ where: { id } }) },改造后的逻辑是只有当不是作者且不是管理员时才抛出错误其余情况作者或管理员均放行删除操作。至此五类操作的权限规则全部落地feed公开、drafts按作者过滤、post双条件校验、publish仅作者、deletePost作者或管理员。可以发现这套模式的核心就是ctx.db.exists这一轻量级存在性查询工具——下面深入它的实现。深入理解ctx.db.exists的底层实现在上面的 resolver 中ctx.db是node-advancedboilerplate 通过prisma-binding生成的 Prisma 数据库 API 客户端它暴露了query、mutation、$subscribe与exists四组能力。在本文对应的开源仓库中prisma-client-lib包提供了同源能力的参考实现。在 Client.ts 中Client类将$exists作为公开 API 暴露export class Client { query: any mutation: any $subscribe: any $graphql: any $exists: any // ... constructor({ typeDefs, endpoint, secret, debug, models }: ClientOptions) { // ... this.$graphql this.buildGraphQL() this.$exists this.buildExists() // ... } }buildExists的实现如下Client.tsprivate buildExists(): Exists { const queryType this._schema.getQueryType() if (!queryType) { return {} } if (queryType) { const types getTypesAndWhere(queryType) return types.reduce((acc, { type, pluralFieldName }) { const firstLetterLowercaseTypeName type[0].toLowerCase() type.slice(1) return { ...acc, [firstLetterLowercaseTypeName]: args { return thispluralFieldName.then(res { return res.length 0 }) }, } }, {}) } return {} }从源码可以看出两个关键实现事实exists是建立在列表查询之上的它扫描 Prisma schema 的 Query 根类型通过getTypesAndWhere提取出所有支持where参数并返回列表的字段如posts、users为每个模型生成一个xxx(args)函数该函数实际执行thispluralFieldName即query.posts({ where })这类列表查询然后通过res.length 0将结果归约为布尔值。因此exists并不会把数据取回客户端而是把存在性判断下推给 Prisma 引擎完成。exists的类型签名为(where?: WhereInput) Promiseboolean在 utils/index.ts 中getExistsTypes会为每个模型生成形如post: (where?: PostWhereInput) Promiseboolean的类型声明这正是 resolver 中await ctx.db.exists.Post({...})可用性的来源。此外在 Client.ts 的构造函数中可以看到客户端与 Prisma 服务端的认证握手方式当配置了secret时客户端会用jsonwebtoken的sign({}, secret)生成一个 token并在所有请求与订阅连接中以Authorization: Bearer token的形式发送。这与本文后面把用户 token 放进Authorization头的 Playground 测试方式同源——不同的是resolver 中的ctx.db携带的是服务级 secret用于与 Prisma 服务通信而 Playground 中携带的是用户级 JWT用于让 GraphQL 服务器识别以谁的身份执行操作。这两层认证在权限体系中各司其职。在 GraphQL Playground 中测试权限实现完权限规则后可以在 GraphQL Playground 中验证它们是否生效。整体流程如下在 Playground 中通过signup变更创建新User并在 selection set 中请求token如果此前已创建过User也可以改用login变更将服务器返回的token保存下来设置为 Playground 的Authorization请求头具体做法见下文此后所有请求都以该User的身份发送。1. 创建新User并获取 token首先需要启动 GraphQL 服务器yarn start服务器启动后运行在http://localhost:4000。在浏览器中打开该地址在默认 Playground 的app标签页中发送以下变更mutation { signup( email: sarahgraph.cool password: graphql name: Sarah ) { token } }2. 设置Authorization请求头复制返回的token在 Playground 左下角的 HTTP HEADERS 面板中按如下 JSON 格式设置请求头注意将__TOKEN__占位符替换为signup变更实际返回的认证 token{ Authorization: __TOKEN__ }设置完成后Playground 发出的所有请求都将以刚创建的User身份执行。说明graphql-yoga底层基于 Express在服务器启动时会创建默认的 GraphQL Playground。getUserId(ctx)之所以能从ctx中取出用户身份正是因为Authorization头中的 token 在服务器的请求中间件中被解析并注入到了ctx。3. 验证权限规则带着以上知识现在可以自由尝试可用的 queries/mutations验证权限规则是否正确工作。例如可以走一遍如下流程以Sarah刚创建的User的身份通过createDraft变更创建一个新草稿再用signup变更创建另一个User并为其获取新的token使用新用户的 token尝试发布 Sarah 的草稿调用publish——由于该用户既不是草稿的author也不是ADMIN服务器应返回错误Post not found or youre not the author。类似地还可以验证未携带Authorization头访问drafts或post会得到未认证/无效 token类错误取决于 boilerplate 中getUserId对缺失 token 的处理以ADMIN角色用户在数据库中把某用户role改为ADMIN或通过 Prisma 控制台/API 更新访问他人的post、删除他人的deletePost操作应被放行。小结本文完整演示了在 Prisma graphql-yoga架构中实现权限控制的推荐路径数据模型层通过Role枚举与default指令为User增加角色字段yarn prisma deploy同步到 Prisma 服务应用 schema 层控制 API 暴露面让role、password等敏感字段不出现在应用 schema 中Resolver 层采用数据访问检查模式利用ctx.db.exists底层是带where条件的列表查询归约为布尔值校验请求者身份 数据归属 角色检查通过才将操作转发给 Prisma 服务测试层通过 Playground 的signup获取 token、设置Authorization头以指定用户身份验证每条权限规则。这套模式的核心价值在于权限判断贴近数据本身归属、角色而非散落在路由或中间件中因而可以精确到某条记录能否被某个用户访问的粒度。若想继续深化可进一步阅读本系列中 使用 Prisma 构建 GraphQL 服务器 以及 Resolver 模式 等文档将权限逻辑与订阅、批量操作等场景结合。赞分享后端数据库GraphQL【免费下载链接】prisma1 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL MongoDB) [deprecated]项目地址https://gitcode.com/gh_mirrors/pr/prisma1点击查看免费下载相关推荐使用 Prisma 与 graphql-yoga 构建 GraphQL 权限系统基于 exists 的逐 Resolver 数据访问控制实战使用 Prisma 与 graphql yoga 构建 GraphQL 权限系统基于 exists 的逐 Resolver 数据访问控制实战 本文是一篇实战向后端数据库GraphQLPrisma 与 graphql-yoga 实现 GraphQL API 权限控制从角色建模到 resolver 级访问检查Prisma 与 graphql yoga 实现 GraphQL API 权限控制从角色建模到 resolver 级访问检查 导读 本教程以 Prisma 官后端数据库GraphQL基于 Prisma 与 graphql-yoga 的 GraphQL 服务端权限控制实战基于 Prisma 与 graphql yoga 的 GraphQL 服务端权限控制实战 导读 本篇教程以 docs/1.1/03 Tutorials/02 G后端数据库GraphQL上一篇DLSS Swapper革新性全攻略释放游戏画质潜能的开源解决方案下一篇智能米游社签到助手3步实现零门槛自动签到每月多领300原石创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表