- 后端
- 数据库
- GraphQL
【免费下载链接】prisma1
💾 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL & MongoDB) [deprecated]
本指南讲解如何将原有 Graphcool Framework 服务中的认证(Authentication)与授权(Authorization)功能迁移到 Prisma 架构:Prisma 不再把用户认证与权限系统绑定,而是提供基于 JWT(Service Token)的简单令牌机制,用户注册/登录与数据访问权限(如"只有作者才能更新自己的 Post")全部下沉到 GraphQL 服务器的应用层实现。读完本文,你将掌握签名与校验 Prisma 服务令牌、用prisma-binding的exists函数替代 Graphcool 权限查询(permission queries),以及用graphql-yoga+bcryptjs+jsonwebtoken完整落地 signup / login / 权限校验的实战方案。
背景:Graphcool Framework 的认证与授权方式
在 Graphcool Framework 中,认证功能通过 resolver functions(解析器函数)实现:对 API 的 GraphQL schema 做扩展,追加提供注册(signup)和登录(login)能力的专用 mutation。整个过程通常包含三个步骤:
- 在 GraphQL schema 的
Mutation类型上以 schema extension 的形式定义signup/loginresolver 函数; - 直接在 Graphcool Framework 中以 JavaScript 提供这些 resolver 函数的实现,或使用 webhook 调用你自行托管的函数;
- 在 service 定义文件中把 mutation 定义与实现连接起来。
而在数据访问的安全控制方面,Graphcool Framework 使用permission queries(权限查询)概念:每个 API 操作都可以关联一条(或多条)权限规则,在执行操作前先进行校验。
这种把认证与权限都耦合进框架的方式,带来的架构问题是认证逻辑、业务逻辑与数据层界限模糊。Prisma 对此做了彻底调整。
Prisma 的认证概念:为什么功能被移入应用层
Prisma 的 authentication 概念与 Graphcool Framework 截然不同:
- Prisma 不提供用户级认证。它只提供一个简单的、基于令牌(可以理解为API Key)的系统用于访问 Prisma API——即 service secret 与 service token 机制;
- 用户认证与权限规则移入应用层,即你的 GraphQL 服务器(
graphql-yoga等)负责实现; prisma-binding包提供的exists函数在应用层扮演与 Graphcool Framework 中权限查询类似的角色;- JWT 令牌改由你自己生成,而不再由
graphcool-lib代为生成。
这一设计对应迁移指南 Overview 中描述的两层架构:Prisma 提供数据库层(自动生成、面向 CRUD 的 GraphQL API),你的 GraphQL 服务器构成应用层(面向客户端、承载业务逻辑、认证与权限)。prisma-binding负责把应用层的请求转发给 Prisma API。
理解 Prisma 的服务密钥与令牌机制
在动手迁移之前,先理解 Prisma 自身的认证基础。Prisma 服务的 GraphQL API 默认由 service secret 保护,它定义在prisma.yml中:
service: my-service stage: ${env:PRISMA_STAGE} cluster: ${env:PRISMA_CLUSTER} datamodel: database/datamodel.graphql secret: ${env:PRISMA_SECRET}secret的约束:必须是 UTF-8 编码、不能包含空格、最长 256 字符;也可以在单个字符串中以逗号分隔多个 secret(空格会被忽略),从而实现无缝密钥轮换:
secret: myFirstSecret, SECRET_NUMBER_2,3rd-secret如果设置disableAuth: true,则任何人都拥有数据库的完整读写权限(默认是false,即默认启用认证)。
secret用于签发 JWT 服务令牌,令牌放在 HTTP 请求的Authorization头中:
Authorization: Bearer __TOKEN__JWT 载荷(payload)包含两类必须的 claims:
{ "exp": 1300819380, "service": "my-service@prod" }exp:令牌过期时间;service:服务的名称与 stage。
Prisma 校验请求时会检查:令牌必须用服务配置的 secret 签名、exp必须指向未来、serviceclaim 必须与当前请求的服务及 stage 匹配。prisma.yml的完整字段定义可参考 YAML-Structure。
在 Node 端,可以基于jsonwebtoken库自行签发这样的服务令牌(这里的PRISMA_SECRET/PRISMA_STAGE来自环境变量):
var jwt = require('jsonwebtoken') jwt.sign( { data: { service: 'my-service@' + process.env.PRISMA_STAGE, }, }, process.env.PRISMA_SECRET, { expiresIn: '1h', } )也可以直接用 CLI 命令prisma token获取当前 Prisma 服务的新签名 JWT,支持--copy复制到剪贴板与--env-file指定环境变量文件,详见 prisma-token:
prisma token prisma token --copy在源码层面,prisma-client-lib的 Client 构造函数会在传入secret时自动用它签发空载荷令牌sign({}, secret),并把它作为Authorization: Bearer <token>头附加到对 Prisma API 的所有请求(包括订阅的connectionParams)。也就是说,应用层服务器与 Prisma 之间的服务级认证是自动完成的,你在应用层要操心的是你的用户与你的 GraphQL 服务器之间的用户认证。
迁移前提
以下迁移步骤假设:
- 你使用
graphql-yoga作为 GraphQL 服务器; - 你的 schema 定义使用 SDL(Schema Definition Language)编写。
想直接看现成实现,可参考本仓库
examples/目录下的示例(仓库中examples目录目前只包含说明文件,完整的 auth / permissions 示例在 Prisma 官方示例仓库中)。另有配套教程 Permissions 详述权限规则的实现。
迁移 Step 1:迁移 Schema 定义
假设你的 Graphcool Framework 服务中曾定义过如下 schema extension:
type Mutation { signupUser(email: String!, password: String!): SignupUserPayload authenticateUser(email: String!, password: String!): AuthenticateUserPayload } type SignupUserPayload{ userId: ID! token: String! } type AuthenticateUserPayload { token: String! }其中signupUser与authenticateUsermutation 用于注册和登录。返回的token是由graphcool-lib生成的 JSON Web Token,客户端把它放进AuthorizationHTTP 头即可对 API 发起认证请求。
迁移时,把这些定义挪进你的graphql-yoga服务器的 schema 定义即可。这里还有个额外的好处:Graphcool 时期由于 resolver 函数无法返回模型类型(model types)而被迫做的 workaround 也可以一并去掉。例如新定义可以简化为:
type Mutation { signup(email: String!, password: String!): AuthPayload login(email: String!, password: String!): AuthPayload } type AuthPayload { token: String! user: User! }AuthPayload直接返回User对象,schema 更干净,也更能表达业务语义。
迁移 Step 2:迁移 Resolver 函数
接下来为上面定义的signup和loginmutation 实现 resolver。核心变化是:JWT 由你自己用jsonwebtoken生成,密码哈希用bcryptjs;signupresolver 中还负责创建新的User节点。
auth.js:
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 }几点说明:
signup中ctx.db.mutation.createUser是经由prisma-binding生成的 Prisma mutation,data: { ...args, password }中args包含email与password,其中密码已替换为 bcrypt 哈希后的值——绝不能把明文密码写进数据库;login用ctx.db.query.user({ where: { email } })按唯一字段email查询用户,然后bcrypt.compare校验密码;- 两个 resolver 都用
jwt.sign({ userId: user.id }, process.env.JWT_SECRET)生成用户令牌,这里的JWT_SECRET是应用层的密钥,与 Prisma 的 service secret 不是一回事——它只用于签名"你是谁"的用户令牌。
AuthPayload.js(解析user字段,避免把整棵User对象直接暴露):
const AuthPayload = { user: async ({ user: { id } }, args, ctx, info) => { return ctx.db.query.user({ where: { id } }, info) }, } module.exports = { AuthPayload }把token与user解耦后,可以确保只查询客户端真正请求的User字段(info会被透传给 Prisma)。
如何识别当前请求用户
在上面login之后,所有受保护的 resolver 都要先从请求上下文恢复userId。常见做法是封装一个getUserId(ctx)工具:从ctx.request的Authorization头中取出Bearer <token>,用jsonwebtoken.verify验签得到userId;验签失败则抛出"未认证"错误。这也是下方权限校验代码中getUserId(ctx)的语义基础——它在用户未认证时直接抛错。
迁移 Step 3:迁移权限规则
如前所述,prisma-binding的exists函数是迁移权限查询的首选工具。它生成的ctx.db.exists.<Type>(filter)会在数据库中检查是否存在满足条件的节点,返回布尔值,非常适合在 resolver 内做前置条件判断。
在源码层面,prisma-client-lib的 Client 类对外暴露$exists(即ctx.db.exists的底层实现),并在构造时通过this.buildExists()构建,按模型类型生成exists.Post(...)/exists.User(...)这样的方法。
场景:只允许作者更新自己的 Post
假设你的 Prisma 服务数据模型如下:
type User @model { id: ID! @unique name: String! posts: [Post!]! } type Post @model { id: ID! @unique title: String! author: User! }在 Graphcool Framework 中,要表达"只有Post的作者能更新它",你需要给updatePostmutation 关联如下 permission query:
query ($user_id: ID!, $post_id: ID!) { SomePostExists(filter: { id: $post_id author: { id: $user_id } }) }而在 Prisma 中,你需要在应用层的updatePostresolver 里完成同样的检查:
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', ) }注意对应关系:
| Graphcool Framework | Prisma 应用层 |
|---|---|
permission query(如SomePostExists) | ctx.db.exists.Post({ ... }) |
| 框架内自动执行权限检查 | resolver 内显式检查,通过后才执行ctx.db.mutation.* |
$user_id由框架注入 | getUserId(ctx)从请求令牌解析 |
| 检查失败由框架返回错误 | 检查失败抛Error,由 GraphQL 服务器统一返回 |
扩展:组合多个权限条件(作者或管理员)
exists不仅支持单条条件,还支持组合判断。比如"作者本人或ADMIN用户才能读取某篇 Post":
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.', ) }这里的关键是给User增加role字段(配合Role枚举:ADMIN/CUSTOMER,默认CUSTOMER),且role与password一样不要暴露在应用层 schema中,从而对客户端隐藏。更完整的可运行示例(drafts、publish、deletePost等 resolver 的逐步改造)参见 Permissions 教程。
在 GraphQL Playground 中验证认证与权限
完成迁移后,可以用 GraphQL Playground 验证流程:
- 启动服务器(
yarn start),打开http://localhost:4000; - 发送
signupmutation 创建用户并返回token:mutation { signup( email: "sarah@graph.cool" password: "graphql" name: "Sarah" ) { token } } - 复制返回的
token,在 Playground 左下角的 HTTP Headers 中以 JSON 形式设置为Authorization头(把__TOKEN__替换为真实令牌):{ "Authorization": "__TOKEN__" } - 之后所有请求都代表该用户发出,可以验证:用另一个用户登录后尝试发布 Sarah 的 draft,会得到
Post not found or you're not the author之类的错误。
小结:从框架到 Prisma 的认证/授权心智模型
迁移完成后,你的架构变成了清晰的"双 GraphQL 层":
- Prisma(数据库层):只认 service token(JWT,由 service secret 签发),提供纯粹的 CRUD 数据访问能力,不做任何用户级权限判断;
- 你的 GraphQL 服务器(应用层):负责用户注册/登录、生成用户 JWT、解析当前请求用户(
getUserId)、用ctx.db.exists实现细粒度权限校验,再把合法请求转发给 Prisma。
这套模式的收益是认证与授权逻辑完全由你掌控,schema 更简洁(AuthPayload直接返回user),权限规则用普通 JavaScript 表达、可组合可测试,同时 Prisma 侧的 API 可以保持通用、复用和可预测。
- 后端
- 数据库
- GraphQL
【免费下载链接】prisma1
💾 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL & MongoDB) [deprecated]
相关推荐
Prisma 迁移指南:从 Graphcool Framework 到 Prisma 的认证与授权(Authentication & Authorization)
Prisma 迁移指南:从 Graphcool Framework 到 Prisma 的认证与授权(Authentication & Authorization
后端数据库GraphQL从 Graphcool Framework 到 Prisma:认证与授权(Authentication & Authorization)迁移完整指南
从 Graphcool Framework 到 Prisma:认证与授权(Authentication & Authorization)迁移完整指南 本篇指南以
后端数据库GraphQLPrisma 认证与授权迁移指南:从 Graphcool Framework 到应用层 JWT 实现
Prisma 认证与授权迁移指南:从 Graphcool Framework 到应用层 JWT 实现 本文基于 Prisma 官方迁移指南(对应仓库文档 doc
后端数据库GraphQL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考