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 concern):Prisma 只负责数据层的存取,鉴权逻辑完全由业务应用掌控。
前提与准备
官方指南假定你正在使用以下技术栈,请确认你的环境与之匹配:
graphql-yoga作为 GraphQL 服务器;- schema 以 SDL(Schema 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并透传info(selection 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 请求头中提取并校验 JWT:
function 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 Framework | Prisma(迁移后) |
|---|---|---|
| 认证实现位置 | resolver 函数(schema extension) | 应用层 resolver(graphql-yoga) |
| JWT 生成 | 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),仅供参考