Graphcool 迁移至 Prisma 实战Authentication 与 Authorization 架构升级指南【免费下载链接】prisma1 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL MongoDB) [deprecated]项目地址: https://gitcode.com/gh_mirrors/pr/prisma1本篇指南基于 Prisma 开源仓库gh_mirrors/pr/prisma1中的官方升级文档系统讲解如何将 Graphcool Framework 的注册/登录Authentication与权限规则Authorization迁移到 Prisma 的应用层架构中。你将学会把基于 resolver 与 permission query 的旧鉴权体系重构为基于 JWT prisma-binding/Prisma Client 的新体系并在graphql-yoga服务器中落地 signup/login resolver 与基于exists函数的权限检查最终完成从框架内置鉴权到应用层自治鉴权的完整范式切换。迁移背景Graphcool 与 Prisma 的鉴权哲学差异在深入迁移步骤之前有必要先理解两个平台在鉴权设计上的根本分歧这正是整个升级工作的出发点。Graphcool Framework 的旧鉴权模型在 Graphcool Framework 时代认证Authentication是通过 resolver 函数schema extension实现的。其工作链路包含三个固定步骤定义 resolver在 GraphQL schema 中以Mutation类型扩展的形式声明 signup 与 login 的 resolver 函数提供实现直接在 Graphcool Framework 中用 JavaScript 提供 resolver 实现或通过 webhook 调用自托管函数连接二者通过调整服务定义文件service definition file把 mutation 定义与实现绑定起来。与此同时数据访问的安全由permission queries权限查询概念支撑——API 上的每个操作都可以关联一条或多条权限规则操作执行前会先校验这些规则。这种认证 权限耦合进框架的模式导致 Graphcool 服务同时承担了业务逻辑与安全逻辑的双重职责。Prisma 的新鉴权模型Prisma 采用了完全不同的理念官方升级文档明确指出Prisma 提供的是一个基于 token可理解为 API Key的简单系统用于访问 Prisma API而非像 Graphcool 那样把认证绑定到权限系统上。这带来两个关键变化用户认证与权限规则下沉到应用层由你的 GraphQL 服务器例如graphql-yoga自己实现JWT 令牌由你自行生成不再由graphcool-lib代为签发。这一设计的直接收益是更清晰的架构与更好的关注点分离separation of concernPrisma 只负责数据层的存取鉴权逻辑完全由业务应用掌控。前提与准备官方指南假定你正在使用以下技术栈请确认你的环境与之匹配graphql-yoga作为 GraphQL 服务器schema 以 SDLSchema Definition Language编写数据模型已迁移至 Prisma 服务模型定义见下文 Step 3。Step 1迁移 Schema 定义Graphcool 时代的 schema extension在 Graphcool Framework 服务中你通常会有如下形式的 schema extension通过signupUser与authenticateUser两个 mutation 提供注册与登录能力返回的token是graphcool-lib生成的 JWT客户端需将其放入AuthorizationHTTP 头来认证请求type Mutation { signupUser(email: String!, password: String!): SignupUserPayload authenticateUser(email: String!, password: String!): AuthenticateUserPayload } type SignupUserPayload{ userId: ID! token: String! } type AuthenticateUserPayload { token: String! }迁移到 graphql-yoga 后的新定义这些定义可以整体搬入graphql-yoga服务器的 schema 中。官方文档特别提醒迁移过程中你可以去掉一个历史 workaround——旧框架下 resolver 函数无法返回模型类型model types因此不得不定义独立的SignupUserPayload/AuthenticateUserPayload包装类型现在直接返回User即可。官方建议的新定义如下type Mutation { signup(email: String!, password: String!): AuthPayload login(email: String!, password: String!): AuthPayload } type AuthPayload { token: String! user: User! }注意两个细节变化authenticateUser更名为更常见的login且AuthPayload直接携带user: User!字段签名更简洁、对客户端更友好。Step 2迁移 Resolver 函数接下来需要在应用层实现signup与login的 resolver。核心职责是在 resolver 内自行生成 JWT并返回给用户在signupresolver 中同时创建User类型的新节点。官方文档给出了auth.js的参考实现请先安装bcryptjs与jsonwebtoken依赖const bcrypt require(bcryptjs) const jwt require(jsonwebtoken) const auth { async signup(parent, args, ctx, info) { const password await bcrypt.hash(args.password, 10) const user await ctx.db.mutation.createUser({ data: { ...args, password }, }) return { token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET), user, } }, async login(parent, { email, password }, ctx, info) { const user await ctx.db.query.user({ where: { email } }) if (!user) { throw new Error(No such user found for email: ${email}) } const valid await bcrypt.compare(password, user.password) if (!valid) { throw new Error(Invalid password) } return { token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET), user, } }, } module.exports { auth }逐段解读其中的关键实现细节密码哈希bcrypt.hash(args.password, 10)使用 10 个 salt rounds 对明文密码加盐哈希绝不允许明文入库创建用户ctx.db.mutation.createUser({ data: { ...args, password } })将原始参数与哈希后的密码一并写入这里的ctx.db正是 Prisma Client或prisma-binding的Prisma实例JWT 签发jwt.sign({ userId: user.id }, process.env.JWT_SECRET)在服务端用自己的密钥建议通过环境变量注入签名令牌payload 中只需携带userId登录校验先按email查出用户并判空再用bcrypt.compare校验密码两者任一失败都抛出明确错误避免泄露用户是否存在这类信息。AuthPayload 的字段解析由于signup直接返回了user对象通常无需额外 resolver。但官方文档仍给出了AuthPayload.js的示例展示如何在需要时按 id 二次查询完整用户const AuthPayload { user: async ({ user: { id } }, args, ctx, info) { return ctx.db.query.user({ where: { id } }, info) }, } module.exports { AuthPayload }这里user作为AuthPayload的字段 resolver 被单独拆分它从父级 payload 解构出user.id再委托给ctx.db.query.user并透传infoselection set确保按客户端实际请求的字段返回。Step 3迁移权限规则Permission Rules权限查询到exists的映射官方指南的核心结论是prisma-binding包中的exists函数是迁移权限查询permission queries的首选工具它扮演了与 Graphcool 中权限查询相似的角色。先看官方文档给出的 Prisma 数据模型示例type User model { id: ID! unique name: String! posts: [Post!]! } type Post model { id: ID! unique title: String! author: User! }在 Graphcool Framework 中若要表达只有Post的作者才能更新它需要把下面的 permission query 关联到updatePostmutation 上query ($user_id: ID!, $post_id: ID!) { SomePostExists(filter: { id: $post_id author: { id: $user_id } }) }在 Prisma 架构下这一条件检查被搬进了应用层updatePostresolver 中用ctx.db.exists.Post(...)表达完全相同的语义async function updatePost(parent, { id, title, text }, ctx, info) { // getUserId throws an error if the requesting user is not authenticated const userId getUserId(ctx) // this expresses the same condition as the permission query above const requestingUserIsAuthor await ctx.db.exists.Post({ id, author: { id: userId, }, }) // only if the condition is true, the post is actually updated if (requestingUserIsAuthor) { return await ctx.db.mutation.updatePost({ where: { id }, data: { title, text }, }, info) } throw new Error( Invalid permissions, you must be an admin or the author of a post to update it, ) }这一段的迁移模式具有普遍性可归纳为三步鉴权getUserId(ctx)先从请求中解析出当前用户未认证则直接抛错授权判定ctx.db.exists.Post({ id, author: { id: userId } })以布尔值表达该 Post 是否存在且作者为当前用户条件执行仅当条件成立才真正调用updatePostmutation否则抛出权限错误。关于权限规则的更多实现细节官方文档指引参考 this tutorial。exists的底层实现原理exists并不是黑魔法——从源码中可以确认其底层机制。在 cli/packages/prisma-client-lib/src/Client.ts 中buildExists()方法遍历 schema 的查询类型为每个模型类型生成形如exists.Post(args)的函数。每个函数的实现是先以where条件查询对应的列表字段再检查结果长度是否大于 0最终返回布尔值private 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.Post({ id, author: { id: userId } })在底层等价于一次posts(where: {...})列表查询通过结果非空来判断节点是否存在。这解释了为什么exists能无缝承接 Graphcool permission query 的过滤语义——两者都是基于where条件的谓词判断。JWT 解析与getUserId辅助函数官方文档同时给出了getUserId的标准实现它负责从 HTTP 请求头中提取并校验 JWTfunction getUserId(ctx) { const Authorization ctx.request.get(Authorization) if (Authorization) { const token Authorization.replace(Bearer , ) const { userId } jwt.verify(token, process.env.APP_SECRET) return userId } throw new AuthError() } class AuthError extends Error { constructor() { super(Not authorized) } }其工作流程为读取请求头中的Authorization字段形如Bearer token→ 剥掉Bearer前缀得到原始 JWT → 用jwt.verify校验签名并解出userId若请求头缺失或令牌无效则抛出AuthError。令牌的传输与服务端处理迁移完成后客户端与服务端之间形成如下鉴权闭环客户端注册/登录调用signup/loginmutation服务端返回{ token, user }客户端携带令牌后续请求在 HTTP 头中携带Authorization: Bearer jwt服务端校验应用层 resolver 通过getUserId(ctx)解析出当前用户身份授权决策结合ctx.db.exists与业务逻辑决定是否放行。在 Prisma 服务这一侧prisma-binding的Prisma客户端在实例化时若配置了secret会自动用该 secret 签发 token 并在每次请求中附带Authorization: Bearer token头。相关逻辑同样位于 cli/packages/prisma-client-lib/src/Client.ts构造函数中const token secret ? sign({}, secret!) : undefined随后在BatchedGraphQLClient的请求头与 WebSocket 订阅的connectionParams中统一注入。这说明应用层的 JWT 与应用访问 Prisma API 的服务级 token 是两套并行的凭证体系——前者代表用户后者代表服务。常见注意事项环境变量JWT_SECRET/APP_SECRET应通过环境变量注入切勿硬编码进源码或提交到版本库密钥一致性签发jwt.sign与校验jwt.verify必须使用同一密钥否则getUserId会因签名不匹配而持续抛错密码安全始终使用bcryptjs等加盐哈希登录比对用bcrypt.compare不要自行实现哈希或明文存储权限检查的位置Prisma 不再替你做业务级授权任何敏感 mutation/query 都必须在应用层 resolver 中显式完成身份确认与权限判定exists的语义exists返回布尔值适合作为前置条件判断但不提供返回数据的能力需要数据时仍应走ctx.db.query/ctx.db.mutation。迁移前后对比小结维度Graphcool FrameworkPrisma迁移后认证实现位置resolver 函数schema extension应用层 resolvergraphql-yogaJWT 生成graphcool-lib代为生成应用层用jsonwebtoken自行生成权限规则permission query 关联 API 操作应用层ctx.db.exists条件判断返回类型限制resolver 不能返回模型类型需包装 payload可直接返回User等模型类型职责划分认证与权限耦合进框架关注点分离Prisma 专注数据层完成上述三个 Step 后你的服务将从框架内置鉴权平滑过渡到应用层自治鉴权同时获得更简洁的 schema、更灵活的权限控制与更清晰的架构边界。官方文档还提供了完整的实践示例auth 与 permissions 两个示例工程可结合 Prisma Bindings API 参考 与 客户端源码实现 进一步深入验证迁移效果。【免费下载链接】prisma1 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL MongoDB) [deprecated]项目地址: https://gitcode.com/gh_mirrors/pr/prisma1创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考