news 2026/9/7 16:17:58

Next.js 结合 Apollo Server 的 GraphQL 鉴权实战:api-routes-apollo-server-and-client-auth 示例源码全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Next.js 结合 Apollo Server 的 GraphQL 鉴权实战:api-routes-apollo-server-and-client-auth 示例源码全解析

Next.js 结合 Apollo Server 的 GraphQL 鉴权实战:api-routes-apollo-server-and-client-auth 示例源码全解析

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

本篇技术指南基于 Next.js 仓库中的官方示例examples/api-routes-apollo-server-and-client-auth,完整讲解如何在 Next.js API Routes 中接入 Apollo Server 4、通过 GraphQL 的 Query 与 Mutation 实现用户注册/登录/登出,以及客户端如何用同构 Apollo Client 在 SSR 与浏览器中无缝消费同一套查询。读完后你将掌握:基于 API Route 的 GraphQL 端点搭建、Iron 加密会话 Cookie 的鉴权实现、SchemaLink/HttpLink 同构数据获取链路,以及密码哈希存储等配套安全细节。

一、示例定位:Next.js 数据获取方法 + Apollo 鉴权一体化

该示例的核心思路是:将 Apollo 与 Next.js 的数据获取方法无缝集成——在服务端执行 GraphQL 查询,再把结果水合(hydrate)到浏览器中。README 指出,Apollo 作为 GraphQL 客户端能精确查询所需数据,并根据查询及其结果构建客户端侧缓存,且缓存会随后续查询与变更持续更新。Next 与 Apollo Server 的集成通过社区包apollo-server-integration-next实现,在本仓库当前的示例实现中对应的依赖是@as-integrations/next(见 package.json,"@as-integrations/next": "^1.1.0")。

示例的关键依赖版本(以 package.json 为准):

依赖版本作用
@apollo/server^4.1.1Apollo Server 4 服务端核心
@apollo/client^3.7.1Apollo Client(含 SchemaLink/HttpLink)
@as-integrations/next^1.1.0Apollo Server 与 Next.js API Routes 的集成层
@graphql-tools/schema^9.0.9由 typeDefs + resolvers 组装可执行 Schema
graphql^16.6.0GraphQL 协议实现
@hapi/iron6.0.0加密/签名会话 Token
cookie^0.4.1序列化与解析 Cookie
deepmerge4.2.2合并 SSR 与客户端的 Apollo 缓存

示例的完整目录结构如下(对应仓库实际文件):

examples/api-routes-apollo-server-and-client-auth/ ├── apollo/ │ ├── client.tsx # Apollo Client 同构实例(SchemaLink/HttpLink) │ ├── resolvers.ts # Query.viewer 与 auth Mutation 的实现 │ ├── schema.ts # makeExecutableSchema 组装 │ └── type-defs.ts # GraphQL 类型定义(SDL) ├── components/ │ └── field.tsx # 表单输入框组件 ├── lib/ │ ├── auth-cookies.ts # token Cookie 的写入/删除/解析 │ ├── auth.ts # 会话 seal/unseal(Iron) │ ├── form.ts # GraphQL 错误消息提取 │ └── user.ts # 内存用户存储 + pbkdf2 密码哈希 ├── pages/ │ ├── api/graphql.ts # API Route:Apollo Server 入口 │ ├── _app.tsx # ApolloProvider 注入 │ ├── index.tsx # 查看 viewer,未登录跳转 /signin │ ├── signin.tsx / signup.tsx / signout.tsx ├── README.md └── package.json

二、如何运行示例(How to use)

按照 README 的说明,使用create-next-app配合--example参数即可一键拉取本示例,支持 npm、Yarn 与 pnpm 三种方式:

# npm / npx npx create-next-app --example api-routes-apollo-server-and-client-auth api-routes-apollo-server-and-client-auth-app
# Yarn yarn create next-app --example api-routes-apollo-server-and-client-auth api-routes-apollo-server-and-client-auth-app
# pnpm pnpm create next-app --example api-routes-apollo-server-and-client-auth api-routes-apollo-server-and-client-auth-app

示例的脚本配置为标准的next命令:devbuildstart(见 package.json)。需要特别注意的一个前置条件:会话加密依赖环境变量TOKEN_SECRET,lib/auth.ts 中直接读取process.env.TOKEN_SECRET,运行前必须设置该变量,否则 Iron 的 seal/unseal 会失败。

三、GraphQL 层:类型定义、Schema 组装与 Resolver

3.1 类型定义(typeDefs)

apollo/type-defs.ts 以 SDL 形式定义了本示例的完整契约:

type User { id: ID! email: String! createdAt: Int! } input SignUpInput { email: String! password: String! } input SignInInput { email: String! password: String! } type SignUpPayload { user: User! } type SignInPayload { user: User! } type Query { user(id: ID!): User! users: [User]! viewer: User } type Mutation { signUp(input:SignUpInput!): SignUpPayload! signIn(input:SignInInput!): SignInPayload! signOut: Boolean! }

值得注意的设计点:viewer: User是可空的(非!),因为它代表"当前登录者",未登录时应当返回null而不是报错——这正是 pages/index.tsx 中shouldRedirect = !(loading || error || viewer)判断能成立的前提。

3.2 Schema 组装

apollo/schema.ts 使用@graphql-tools/schemamakeExecutableSchema将 typeDefs 与 resolvers 合并为可执行 Schema:

import { makeExecutableSchema } from "@graphql-tools/schema"; import { typeDefs } from "./type-defs"; import { resolvers } from "./resolvers"; export const schema = makeExecutableSchema({ typeDefs, resolvers, });

这个schema会被同时用于服务端 API Route 与客户端的SchemaLink,是整套同构数据获取的枢纽(见第五节)。

3.3 Resolvers:viewer 查询与认证 Mutation

apollo/resolvers.ts 实现了核心的鉴权逻辑,逐条分析:

  • Query.viewer:通过getLoginSession(context.req)从请求中解析会话。若会话有效,用session.email查出用户返回;若抛错(如会话过期、Token 无效),则转换为带extensions.code: "UNAUTHENTICATED"GraphQLError,消息为 "Authentication token is invalid, please log in"。这是 GraphQL 规范推荐的错误扩展写法,便于客户端按 code 分流处理。
  • Mutation.signUp:调用createUser(args.input)创建用户并返回{ user }
  • Mutation.signIn:先用findUser({ email })查找用户,再validatePassword校验密码;通过后构造{ id, email }会话对象并调用setLoginSession(context.res, session)写入加密 Cookie,最后返回{ user }。凭证不符时抛出 "Invalid email and password combination" 的GraphQLError
  • Mutation.signOut:调用removeTokenCookie(context.res)清空 Cookie,返回true

可以看到,Resolver 通过context.req/context.res直接操作 Node 的 HTTP 对象来完成 Cookie 读写——这正是 API Route 模式与自定义 Server 模式在 Apollo 集成中的典型差异。

四、API Route 入口:Apollo Server 4 如何挂载到 Next.js

整个服务端只有一个文件 pages/api/graphql.ts:

import { ApolloServer } from "@apollo/server"; import { startServerAndCreateNextHandler } from "@as-integrations/next"; import { NextApiRequest, NextApiResponse } from "next"; import { schema } from "../../apollo/schema"; type ExampleContext = { req: NextApiRequest; res: NextApiResponse; }; const apolloServer = new ApolloServer<ExampleContext>({ schema }); export default startServerAndCreateNextHandler(apolloServer, { context: async (req, res) => ({ req, res }), });

这里体现的是 Apollo Server 4 的无框架(framework-agnostic)设计:ApolloServer本身不绑定任何 Web 框架,@as-integrations/next提供的startServerAndCreateNextHandler把它"适配"成 Next.js API Route 默认导出的(req, res)处理函数。context回调把原始的req/res透传进 Resolver,第三节中getLoginSession(context.req)setLoginSession(context.res, session)依赖的正是这条透传链路。

该端点即 README 所述"在服务端获取查询、在浏览器中水合"的传输层:客户端所有 GraphQL 请求都发往/api/graphql(见客户端HttpLinkuri配置)。

五、会话鉴权实现:Iron 加密 Token + 安全 Cookie

这是示例中最有实战价值的部分,分为三层。

5.1 用户存储与密码哈希(lib/user.ts)

lib/user.ts 用内存数组模拟用户表(源码注释明确说明真实应用必须使用数据库)。安全要点在于密码处理:

const salt = crypto.randomBytes(16).toString("hex"); const hash = crypto .pbkdf2Sync(password, salt, 1000, 64, "sha512") .toString("hex");
  • 每个用户生成 16 字节随机盐(hex 编码),避免彩虹表;
  • 使用 PBKDF2-SHA512,1000 次迭代,输出 64 字节哈希;
  • validatePassword用用户自己的salt对输入密码重新做 PBKDF2 后再与存储的hash比对,绝不存明文。

findUser({ email })validatePassword(user, inputPassword)被 resolvers 直接复用,职责边界清晰。

5.2 会话 seal/unseal(lib/auth.ts)

lib/auth.ts 使用@hapi/iron实现"自包含、可验证"的会话:

  • setLoginSession(res, session):在会话对象上附加createdAtmaxAge,用Iron.seal(obj, TOKEN_SECRET, Iron.defaults)加密并签名后写入 Cookie;
  • getLoginSession(req):读取 Cookie 中的 Token 后Iron.unseal还原会话,并按createdAt + maxAge * 1000校验过期时间,过期则抛出"Session expired"——该异常会被Query.viewer捕获并转换为UNAUTHENTICATED错误。

由于 Token 本身被加密+签名,服务端无需维护内存 session 表,天然适配多实例部署。

5.3 Cookie 参数细节(lib/auth-cookies.ts)

lib/auth-cookies.ts 中的setTokenCookie展示了完整的安全 Cookie 配置:

const cookie = serialize(TOKEN_NAME, token, { maxAge: MAX_AGE, // 8 小时 expires: new Date(Date.now() + MAX_AGE * 1000), httpOnly: true, // 禁止 JS 读取 secure: process.env.NODE_ENV === "production", // 生产环境仅 HTTPS path: "/", sameSite: "lax", }); res.setHeader("Set-Cookie", cookie);
参数取值含义
TOKEN_NAMEtokenCookie 名称
MAX_AGE60 * 60 * 8(秒)会话有效期 8 小时
httpOnlytrue防止 XSS 窃取 Token
secure生产环境为true仅通过 HTTPS 传输
sameSitelax缓解 CSRF
path/全站可用

另外两个工具函数:removeTokenCookie通过写入maxAge: -1的空值 Cookie 实现登出删除;parseCookies做了 API Routes 与页面两条路径的兼容——API Route 的req.cookies已由 Next.js 解析好,而页面侧需要从req.headers.cookie手动parsegetTokenCookie(req)则统一从 Cookie 中取token

六、客户端:同构 Apollo Client 与 SSR 缓存水合

apollo/client.tsx 是"服务端查询、浏览器水合"这一 README 核心主张的具体落地。

6.1 同构 Link

function createIsomorphLink() { if (typeof window === "undefined") { return new SchemaLink({ schema }); // 服务端:直接调用本地 Schema } else { return new HttpLink({ uri: "/api/graphql", // 浏览器:走 HTTP 请求 credentials: "same-origin", // 关键:携带 Cookie }); } }

服务端渲染时不经过网络,SchemaLink直接用同一个schema对象在进程内执行 GraphQL 请求;浏览器中则通过HttpLink访问第四节的/api/graphql端点,且credentials: "same-origin"确保携带tokenCookie,使viewer查询在客户端也能识别登录态。

6.2 initializeApollo 与缓存合并

initializeApollo(initialState)的处理逻辑:

  1. 复用模块级单例apolloClient,没有则创建;创建时设置ssrMode: typeof window === "undefined"InMemoryCache全新实例;
  2. 若传入initialState(来自页面的getStaticProps/getServerSideProps),先extract()取出客户端已有缓存,用deepmerge将服务端初始状态合并进已有缓存,再cache.restore(data)恢复;
  3. 服务端每次请求都返回新客户端(避免跨请求串缓存),客户端则创建一次后长期复用。

配合useApollo(initialState)(内部是useMemo(() => initializeApollo(initialState), [initialState]))和 _app.tsx:

export default function App({ Component, pageProps }) { const apolloClient = useApollo(pageProps.initialApolloState); return ( <ApolloProvider client={apolloClient}> <Component {...pageProps} /> </ApolloProvider> ); }

从而每个页面组件都可以通过useQuery/useMutation消费同一份 Apollo Client。

七、页面层:viewer 驱动的登录门与错误处理

7.1 首页登录门(pages/index.tsx)

pages/index.tsx 演示了viewer查询的典型用法:

const ViewerQuery = gql` query ViewerQuery { viewer { id email } } `; const { data, loading, error } = useQuery(ViewerQuery); const viewer = data?.viewer; const shouldRedirect = !(loading || error || viewer); useEffect(() => { if (shouldRedirect) router.push("/signin"); }, [shouldRedirect]);
  • 查询中(loading)显示 "Loading...";
  • 查询出错显示error.message
  • 成功且viewer非空则展示 "You're signed in as {viewer.email}",并提供前往/about/signout的链接;
  • 三者皆不满足(即未登录)则useEffect中跳转/signin

7.2 登录/注册 Mutation 与错误消息提取

pages/signin.tsx 的提交流程值得注意:先执行await client.resetStore()清空旧缓存(防止上一个用户的viewer数据残留),再执行SignInMutation,成功后router.push("/")回到首页;出错时由 lib/form.ts 的getErrorMessage提取展示文案——它会优先查找extensions.code === "BAD_USER_INPUT"graphQLErrors并返回其message,否则回退到error.message。注册页signup.tsx与登出页signout.tsx遵循同一模式(useMutation+getErrorMessage+ 表单组件 components/field.tsx)。

八、从源码结构看的安全边界与适用前提

结合源码可以确认并推断出以下使用前提,移植到生产环境前需要补齐:

  1. 无数据持久化:lib/user.ts 使用内存数组存储用户,源码注释明确提示真实应用需替换为 MongoDB、Fauna、SQL 等数据库——重启即丢数据;
  2. 必须设置TOKEN_SECRET:lib/auth.ts 直接依赖该环境变量,且它是 Iron 加密/签名的唯一密钥,生产环境需使用高熵随机值并保持机密;
  3. Cookie 安全性依赖部署环境secure仅在NODE_ENV === "production"时生效(lib/auth-cookies.ts),因此生产必须运行在 HTTPS 之下;
  4. 会话有效期 8 小时:由MAX_AGE常量与 Iron Token 内嵌的maxAge双重控制(Cookie 过期与getLoginSession的时间校验一致),需要更长/更短会话时两处应同步调整;
  5. 错误语义约定:服务端通过GraphQLError.extensions.code(如UNAUTHENTICATEDBAD_USER_INPUT)与客户端 lib/form.ts 形成约定,扩展业务错误时应沿用这一模式;
  6. 版本前提:示例基于 Pages Router(pages/目录)、@apollo/server4.x 与@as-integrations/next1.x;README 中提到的apollo-server-integration-next是该集成在旧版本的包名。

九、小结与延伸阅读

本示例用极小的代码量串起了完整的 GraphQL 鉴权链路:pages/api/graphql.ts提供 Apollo Server 4 端点并透传req/res上下文;apollo/resolvers.ts在该上下文中完成会话读取与 Cookie 写入;lib/auth.ts+lib/auth-cookies.ts构成 Iron 加密会话层;apollo/client.tsx以 SchemaLink/HttpLink 同构实现保证 SSR 与浏览器行为一致,并通过deepmerge完成缓存水合。相关文档可继续阅读仓库中的 API Routes 文档 与 数据获取文档,以理解 Next.js 侧的 API Route 与数据获取基础机制。

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 16:17:16

学术写作新助手:好写作AI从选题到润色的全流程辅助指南

你是不是也有这样的时刻&#xff1a;导师丢来一句“回去把综述写一下”&#xff0c;你坐在电脑前三个小时&#xff0c;文档里还只有标题和两行孤零零的字。想写&#xff0c;不知道怎么开头&#xff1b;写出来了&#xff0c;又觉得不像学术语言&#xff1b;就算硬凑出一版&#…

作者头像 李华
网站建设 2026/9/7 16:17:02

空气源热泵热水器:从原理、能效到选型安装全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:16:09

WorkBuddy双模型限免实测:Hy4 preview与Hy3选型与配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:15:34

Python构建电影推荐系统:KNN协同过滤与API接口实战

作为一个靠Python吃饭的人&#xff0c;我太清楚“算法”和“API”这两个词对新手意味着什么了。很多人学完基础语法、爬虫、数据分析那一套之后&#xff0c;会觉得“我啥都会了&#xff0c;但啥也做不出来”。而电影推荐系统这个项目&#xff0c;恰好就是打通“理论”到“实战”…

作者头像 李华
网站建设 2026/9/7 16:14:45

Deepseek相关技术应用与行业发展趋势解析

谁懂啊&#xff0c;2026届硕博新生们&#xff01; 刚入学、刚转博&#xff0c;最崩溃的瞬间&#xff0c;一定是面对开题报告的那一刻&#xff1a; 方向没定&#xff0c;文献没读&#xff0c;框架搭不出来&#xff0c;导师一问三不知&#xff1b;好不容易憋出一版&#xff0c;…

作者头像 李华