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.1 | Apollo Server 4 服务端核心 |
@apollo/client | ^3.7.1 | Apollo Client(含 SchemaLink/HttpLink) |
@as-integrations/next | ^1.1.0 | Apollo Server 与 Next.js API Routes 的集成层 |
@graphql-tools/schema | ^9.0.9 | 由 typeDefs + resolvers 组装可执行 Schema |
graphql | ^16.6.0 | GraphQL 协议实现 |
@hapi/iron | 6.0.0 | 加密/签名会话 Token |
cookie | ^0.4.1 | 序列化与解析 Cookie |
deepmerge | 4.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命令:dev、build、start(见 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/schema的makeExecutableSchema将 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(见客户端HttpLink的uri配置)。
五、会话鉴权实现: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):在会话对象上附加createdAt与maxAge,用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_NAME | token | Cookie 名称 |
MAX_AGE | 60 * 60 * 8(秒) | 会话有效期 8 小时 |
httpOnly | true | 防止 XSS 窃取 Token |
secure | 生产环境为true | 仅通过 HTTPS 传输 |
sameSite | lax | 缓解 CSRF |
path | / | 全站可用 |
另外两个工具函数:removeTokenCookie通过写入maxAge: -1的空值 Cookie 实现登出删除;parseCookies做了 API Routes 与页面两条路径的兼容——API Route 的req.cookies已由 Next.js 解析好,而页面侧需要从req.headers.cookie手动parse。getTokenCookie(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)的处理逻辑:
- 复用模块级单例
apolloClient,没有则创建;创建时设置ssrMode: typeof window === "undefined",InMemoryCache全新实例; - 若传入
initialState(来自页面的getStaticProps/getServerSideProps),先extract()取出客户端已有缓存,用deepmerge将服务端初始状态合并进已有缓存,再cache.restore(data)恢复; - 服务端每次请求都返回新客户端(避免跨请求串缓存),客户端则创建一次后长期复用。
配合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)。
八、从源码结构看的安全边界与适用前提
结合源码可以确认并推断出以下使用前提,移植到生产环境前需要补齐:
- 无数据持久化:lib/user.ts 使用内存数组存储用户,源码注释明确提示真实应用需替换为 MongoDB、Fauna、SQL 等数据库——重启即丢数据;
- 必须设置
TOKEN_SECRET:lib/auth.ts 直接依赖该环境变量,且它是 Iron 加密/签名的唯一密钥,生产环境需使用高熵随机值并保持机密; - Cookie 安全性依赖部署环境:
secure仅在NODE_ENV === "production"时生效(lib/auth-cookies.ts),因此生产必须运行在 HTTPS 之下; - 会话有效期 8 小时:由
MAX_AGE常量与 Iron Token 内嵌的maxAge双重控制(Cookie 过期与getLoginSession的时间校验一致),需要更长/更短会话时两处应同步调整; - 错误语义约定:服务端通过
GraphQLError.extensions.code(如UNAUTHENTICATED、BAD_USER_INPUT)与客户端 lib/form.ts 形成约定,扩展业务错误时应沿用这一模式; - 版本前提:示例基于 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),仅供参考