GraphQL 认证实战:基于 Node.js、TypeScript、Fastify 与 Prisma 的 JWT 登录认证实现 【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载本篇指南基于 HowToGraphQL 教程中的 TypeScript 后端路线Node.js Fastify GraphQL-Helix Prisma完整讲解如何为 GraphQL 服务器实现用户注册signup、登录login与当前用户识别me的完整认证体系。读完本文你将掌握 JWT 签发与校验、密码的 bcrypt 哈希存储、GraphQL context 中注入认证用户以及通过 resolver 权限守卫保护敏感操作的全部实现细节最终得到一个可通过Authorization: Bearer头完成鉴权的全栈 GraphQL API。背景与整体思路在 HowToGraphQL 的 TypeScript 后端教程中项目基于以下技术栈构建Node.js 作为运行时、TypeScript 作为开发语言、fastify作为 HTTP 服务器、graphql-helix作为 GraphQL 请求处理库、Prisma配合 SQLite作为数据库访问层。在认证章节之前项目已完成 GraphQL 服务器的搭建POST /graphql端点 GraphiQL 界面和 Prisma Client 与数据库的连接context.prisma可用。认证章节要解决的核心问题是如何让用户对 GraphQL 服务器进行身份认证并让每个 resolver 都知道当前请求的用户是谁。整体方案分为五个部分在 Prisma 数据模型中新增User模型并建立User与Link的一对多关系在 GraphQL Schema 中新增signup、login两个 mutation 和AuthPayload、User类型使用jsonwebtoken签发/校验 JWT使用bcryptjs对密码做哈希实现两个 mutation 的 resolver通过 HTTPAuthorization头携带 token在构建 GraphQL context 时解析 token 并把currentUser注入所有 resolver用context.currentUser保护postmutation并实现Link.postedBy、User.links关系字段 resolver验证me查询。这套方案刻意选择了简单但完整的 JWT 方案无状态、不依赖服务端 sessiontoken 通过标准 HTTP 头传递而不污染 GraphQL 契约本身。第一步在 Prisma 数据模型中添加 User 模型首先需要在数据库层面表达用户数据。打开prisma/schema.prisma新增User模型并更新已有的Link模型以表达Link 由某个 User 发布这一关系model Link { id Int id default(autoincrement()) createdAt DateTime default(now()) description String url String postedBy User? relation(fields: [postedById], references: [id]) postedById Int? } model User { id Int id default(autoincrement()) name String email String unique password String links Link[] }理解关系字段relation fields这里有两个关键点值得展开Link模型上新增了一个关系字段postedBy类型为可空的User?并通过relation属性注解声明fields: [postedById]指定本表的外键列是postedByIdreferences: [id]指定它指向User表的id列。因此还需要一个显式的postedById Int?列来实际存储外键值。对熟悉 SQL 的读者来说这就是典型的一对多关系一个 User 发布多条 Link一条 Link 属于一个 User。postedById可空意味着系统中允许存在未关联发布者的历史链接。User模型上新增links Link[]字段这是关系的反向一对多的多侧表示该用户发布的所有链接列表。email字段带unique约束保证邮箱唯一——这正是后续loginresolver 使用findUnique({ where: { email } })精确查询的依据。Prisma 数据模型的价值在于数据模型与底层数据库表的映射关系被显式声明在 schema 中开发者无需手写 JOIN 或外键 SQL就能以贴近数据库语义的方式推理数据结构。执行迁移并重新生成 Prisma Client按照本教程在 数据库章节建立的固定工作流每次修改数据模型后都必须先迁移数据库再重新生成 Prisma Client。在项目根目录运行npx prisma migrate dev --name add-user-model该命令会生成第二份迁移脚本并放入prisma/migrations目录——这个目录随时间推移成为数据库演化的历史记录同时命令会实际执行迁移使新的User表就绪。迁移完成后Prisma Client 也自动重新生成暴露出针对User模型的全部 CRUD 方法user.create、user.findUnique等可以直接在 resolver 中使用。第二步扩展 GraphQL Schema沿用教程中的 schema-driven 开发方式先把新增操作写进 SDLSchema Definition Language再实现对应的 resolver。打开src/schema.graphql更新为type Query { info: String! feed: [Link!]! } type Mutation { post(url: String!, description: String!): Link! signup(email: String!, password: String!, name: String!): AuthPayload login(email: String!, password: String!): AuthPayload } type Link { id: ID! description: String! url: String! } type AuthPayload { token: String user: User } type User { id: ID! name: String! email: String! links: [Link!]! }几个设计细节signup和login的行为高度相似两者都返回正在注册或登录的User信息以及一个可用于后续请求认证的token这些信息被打包进AuthPayload类型统一返回。两个字段声明为可空token: String、user: User因为AuthPayload作为通用认证结果类型设计上允许字段缺省。User类型中没有暴露password字段——认证凭证不应通过 GraphQL 契约外泄这是 schema 层面就做出的安全决策。由于User与Link的关系是双向的还需要在Link类型中补上postedBy字段使 GraphQL 契约与 Prisma 模型的关系保持对称type Link { id: ID! description: String! url: String! postedBy: User }第三步实现认证基础设施安装依赖认证实现依赖两个库jsonwebtoken用于签发和校验 JWTbcryptjs用于密码哈希npm install --save jsonwebtoken bcryptjs为了获得完整的 TypeScript 类型支持还需要安装对应的类型声明包npm install --save-dev types/jsonwebtoken types/bcryptjs创建 src/auth.ts 与签名密钥新建src/auth.ts先放入一个签名密钥常量后续作为 JWT 签名与密码加密的基础export const APP_SECRET this is my secret;安全提示这里硬编码密钥仅适用于教程环境。生产环境中APP_SECRET应通过环境变量注入例如process.env.APP_SECRET且不应提交到版本库。实现 signup resolver打开src/schema.ts在Mutation下新增signupresolver// ... other imports ... import { APP_SECRET } from ./auth; import { hash } from bcryptjs; import { sign } from jsonwebtoken; const resolvers { // ... other resolvers ... Mutation: { signup: async ( parent: unknown, args: { email: string; password: string; name: string }, context: GraphQLContext ) { // 1. 对明文密码做 bcrypt 哈希 const password await hash(args.password, 10); // 2. 通过 PrismaClient 写入新的 User 记录 const user await context.prisma.user.create({ data: { ...args, password }, }); // 3. 用 APP_SECRET 签发 JWTpayload 中只携带 userId const token sign({ userId: user.id }, APP_SECRET); // 4. 按 AuthPayload 的形状返回 token 和 user return { token, user, }; }, } }逐步拆解这四步密码哈希bcryptjs的hash以 cost factor 10 对明文密码加盐哈希。数据库中永远只存哈希值即使数据库泄露也无法直接还原密码。注意hash是异步操作必须await。持久化用户通过context.prisma即 前一章挂在 GraphQL context 上的PrismaClient单例调用user.create插入新记录。data: { ...args, password }把email、name两个入参展开同时用哈希后的password覆盖原明文。签发 JWTjsonwebtoken的sign以{ userId: user.id }作为 payload、APP_SECRET作为密钥生成签名令牌。注意 payload 中只放用户 ID不放大段用户信息——这样用户改名等场景下 token 依然有效后续请求只需按 ID 查库。组装返回值返回的{ token, user }对象与 schema 中AuthPayload类型一一对应GraphQL 引擎会按选择集裁剪字段后返回给客户端。验证 signup启动服务器npm run dev后打开http://localhost:3000/graphql的 GraphiQL执行mutation { signup(email: testmail.com, name: Dotan Simha, password: 123456) { token user { id name email } } }执行成功后会返回一个 JWT 字符串与用户信息。请务必保存这个 token后续验证me查询时会用到。实现 login resolver在signup下方继续添加loginresolver// ... other imports ... import { hash, compare } from bcryptjs; const resolvers { // ... other resolvers ... Mutation: { login: async ( parent: unknown, args: { email: string; password: string }, context: GraphQLContext ) { // 1. 按邮箱查找用户不存在则报错 const user await context.prisma.user.findUnique({ where: { email: args.email }, }); if (!user) { throw new Error(No such user found); } // 2. 将入参明文密码与库中哈希比对不一致则报错 const valid await compare(args.password, user.password); if (!valid) { throw new Error(Invalid password); } const token sign({ userId: user.id }, APP_SECRET); // 3. 同样以 AuthPayload 形状返回 return { token, user, }; }, } }与signup的对照不创建新记录而是用findUnique按email精确查找现有用户依赖User.email上的unique约束。查不到即抛出No such user found。用bcryptjs的compare把客户端提交的明文密码与数据库中存储的哈希进行恒定时间比对不匹配则抛出Invalid password。校验通过后签发与signup完全相同结构的 JWT并返回{ token, user }。两个错误分支都用throw new Error表达GraphQL 引擎会把 resolver 抛出的异常收集进响应的errors字段data中对应字段则为null——这是 GraphQL 标准的错误传播机制客户端能明确感知登录失败。在 GraphiQL 中用刚注册的账号验证登录mutation { login(email: testmail.com, password: 123456) { token user { id name email } } }第四步通过 HTTP 头识别当前用户有了签发 token 的能力下一步是识别每次请求的发起者是谁。关键设计决策是不走 GraphQL schema 传 token比如不把它设计成 mutation 的参数而是使用标准 HTTP 头避免认证流程污染 GraphQL 契约Authorization: Bearer MY_TOKEN_HERE为此服务器需要能访问原始 HTTP 请求、验证 token、解析出当前用户并把该用户注入 GraphQLcontext——这样每个 resolver 都能通过第三个参数读到context.currentUser。authenticateUser 函数在src/auth.ts中新增认证函数import { PrismaClient, User } from prisma/client; import { FastifyRequest } from fastify; import { JwtPayload, verify } from jsonwebtoken; export const APP_SECRET this is my secret; export async function authenticateUser(prisma: PrismaClient, request: FastifyRequest): PromiseUser | null { if (request?.headers?.authorization) { // 1. 从 Authorization 头中拆出 Bearer 后的 token const token request.headers.authorization.split( )[1]; // 2. 用 jsonwebtoken 的 verify 校验签名解析出 payload const tokenPayload verify(token, APP_SECRET) as JwtPayload; // 3. 取出 payload 中的 userId const userId tokenPayload.userId; // 4. 按 ID 从数据库查出用户 return await prisma.user.findUnique({ where: { id: userId } }); } return null; }流程梳理读取入站 HTTP 请求头中的Authorization值按空格切分取第二段得到 token第一段是Bearer前缀。verify用APP_SECRET校验签名与时效失败会直接抛出异常成功则得到 payload从中取出userId。用 Prisma 按id查库返回完整的User记录而不是仅凭 token payload 构造用户保证数据实时准确。请求头缺失或 token 无效时返回null让上层 resolver 自行决定是拒绝访问还是按匿名处理。改造 contextFactory修改src/context.ts让 context 构建阶段调用上述函数import { PrismaClient, User } from prisma/client; import { FastifyRequest } from fastify; import { authenticateUser } from ./auth; const prisma new PrismaClient(); export type GraphQLContext { prisma: PrismaClient; currentUser: User | null; }; export async function contextFactory( request: FastifyRequest ): PromiseGraphQLContext { return { prisma, currentUser: await authenticateUser(prisma, request), }; }GraphQLContext类型新增了currentUser: User | null字段——类型层面的声明让所有 resolver 都能获得currentUser的自动补全与严格空值检查。同时必须确保contextFactory能拿到入站 HTTP 请求。在src/index.ts的 GraphQL 处理器中把 Fastify 的req传进去const result await processRequest({ request, schema, operationName, contextFactory: () contextFactory(req), query, variables, });注意这里contextFactory从 第七章中直接传函数引用改成了每次调用时执行() contextFactory(req)的闭包以便把当前请求req注入进去。这一步正是识别当前用户能落地的关键graphql-helix的processRequest在执行每个 GraphQL 请求前调用contextFactory构建 context认证逻辑就嵌入了请求处理管线。至此每个携带有效 token 的 GraphQL 请求都会在context.currentUser中拿到认证用户没有 token 或 token 无效时context.currentUser为null。添加 me 查询验证为验证 context 注入生效在 schema 中新增Query.me字段type Query { info: String! feed: [Link!]! me: User! }并实现其 resolverconst resolvers { Query: { me: (parent: unknown, args: {}, context: GraphQLContext) { if (context.currentUser null) { throw new Error(Unauthenticated!); } return context.currentUser; }, } }resolver 的权限检查模式很简单直白context.currentUser为null时直接抛错。在 GraphiQL 中执行查询query { me { id name } }并在 GraphiQL 的HEADERS区域填入之前保存的 token{ Authorization: Bearer YOUR_TOKEN_HERE }执行后服务器即可基于 token 识别并返回当前用户信息——认证闭环打通。第五步把认证接入其余 resolver保护 post mutation此前postmutation 对所有人开放。现在要求只有认证用户才能发布链接并且发布时自动关联当前用户。修改Mutation.postconst resolvers { Mutation: { post: async (parent: unknown, args: { url: string; description: string }, context: GraphQLContext) { if (context.currentUser null) { throw new Error(Unauthenticated!); } const newLink await context.prisma.link.create({ data: { url: args.url, description: args.description, postedBy: { connect: { id: context.currentUser.id } }, }, }); return newLink; }, } }两处变化函数开头做认证守卫context.currentUser null时抛Unauthenticated!。没有携带 token 或 token 失效的客户端从此无法再调用post。创建Link时通过 Prisma 的关系连接语法postedBy: { connect: { id: context.currentUser.id } }把新链接关联到当前用户。connect是 Prisma 在写操作时建立外键关系的标准方式等价于在插入时填充postedById外键列无需二次更新。在 GraphiQL 中携带Authorization头再次执行mutation { post(url: www.graphqlconf.org, description: An awesome GraphQL conference) { id } }实现关系字段 resolver还有最后一块拼图让User与Link之间新增的关系字段真正可查询。Link.postedByresolver——在src/schema.ts的 resolvers 中为Link类型补充const resolvers { Link: { id: (parent: Link) parent.id, description: (parent: Link) parent.description, url: (parent: Link) parent.url, postedBy: async (parent: Link, args: {}, context: GraphQLContext) { if (!parent.postedById) { return null; } return context.prisma.link .findUnique({ where: { id: parent.id } }) .postedBy(); }, }, }这里利用了 Prisma Client 的关系查询 API先findUnique拿到Link的记录类型上带有关系方法再链式调用.postedBy()加载其发布者的User。开头对parent.postedById判空是因为postedById在数据模型中可空——历史数据允许存在无发布者的链接。注意 resolver 名必须与 GraphQL 类型定义中的字段名postedBy一致这是 GraphQL 字段级 resolver 的命名约定。User.linksresolver——同理实现反向关系// ... other imports ... import { Link, User } from prisma/client; // ... other resolvers ... const resolvers { User: { links: (parent: User, args: {}, context: GraphQLContext) context.prisma.user.findUnique({ where: { id: parent.id } }).links(), }, }两个方向都通过 Prisma Client 生成的关系方法.postedBy()/.links()解析避免了手写关联查询。端到端验证现在所有字段都已有 resolver可以在 GraphiQL 中运行组合查询一次验证 feed 列表、关系解析与认证状态query { feed { id description url postedBy { id name } } }返回结果中每条Link都会带上其发布者的User信息postedBy为null的则是认证功能上线前创建的链接。方案总结与安全要点把本章节的实现串起来完整的认证调用链是写路径signup/loginmutation 校验身份 → 签发 JWTpayload 只含userId→ 随AuthPayload返回客户端读路径客户端每个请求在Authorization: Bearer token头中携带 token →contextFactory调authenticateUser解析并查库 →context.currentUser注入所有 resolver →me、post等 resolver 据此授权与取数。几个值得记住的工程要点密钥管理APP_SECRET硬编码仅适合学习生产环境务必改用环境变量且 JWT 密钥一旦泄露应立即轮换。无状态与时效JWT 是无状态的verify只校验签名。sign未设置过期时间意味着 token 永久有效生产实现应通过expiresIn加上合理的有效期。错误语义认证失败统一用throw new Error表达经 GraphQL 的errors字段返回客户端逻辑与 REST 的 401 语义不同需要在客户端做相应处理。密码安全明文密码只存在于signup/login的入参中数据库仅存 bcrypt 哈希GraphQL 契约User类型从不暴露password字段。授权粒度本教程演示的是认证你是谁与最基础的授权post只允许登录用户。更细粒度的权限例如只有发布者能编辑自己的链接可以在 resolver 中基于context.currentUser进一步判断。至此HackerNews 克隆的 GraphQL 服务器具备了完整的用户认证能力注册、登录、识别当前用户、保护写操作、解析用户与内容的双向关系。后续章节订阅与过滤、分页、排序将在此基础上继续扩展 API 能力。赞分享【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载相关推荐HowToGraphQL TypeScript Apollo Server 实战用 JWT、bcrypt 与 Prisma 实现 GraphQL 用户认证HowToGraphQL TypeScript Apollo Server 实战用 JWT、bcrypt 与 Prisma 实现 GraphQL 用户认证RedisInsight批量操作深度解析5个提升Redis管理效率的关键技巧RedisInsight批量操作深度解析5个提升Redis管理效率的关键技巧 RedisInsight作为Redis官方推出的图形化管理工具其批量操作功能是数据库客户端桌面应用后端前端数据可视化Forge中的上下文压缩处理长对话的高效方法Forge中的上下文压缩处理长对话的高效方法 在构建自托管LLM应用时长对话管理是开发者面临的核心挑战之一。 Forge 作为专注于本地部署LLM工具调用和人工智能LLM 网关工具调用本地部署上一篇探索Claude Code Hooks Mastery的持续学习能力AI助手自我提升下一篇掌握AlignedCollectionViewFlowLayout构建响应式iOS应用界面的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考