ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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.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.jsonas-integrations/next: ^1.1.0。示例的关键依赖版本以 package.json 为准依赖版本作用apollo/server^4.1.1Apollo Server 4 服务端核心apollo/client^3.7.1Apollo Client含 SchemaLink/HttpLinkas-integrations/next^1.1.0Apollo Server 与 Next.js API Routes 的集成层graphql-tools/schema^9.0.9由 typeDefs resolvers 组装可执行 Schemagraphql^16.6.0GraphQL 协议实现hapi/iron6.0.0加密/签名会话 Tokencookie^0.4.1序列化与解析 Cookiedeepmerge4.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/unsealIron │ ├── form.ts # GraphQL 错误消息提取 │ └── user.ts # 内存用户存储 pbkdf2 密码哈希 ├── pages/ │ ├── api/graphql.ts # API RouteApollo 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_SECRETlib/auth.ts 中直接读取process.env.TOKEN_SECRET运行前必须设置该变量否则 Iron 的 seal/unseal 会失败。三、GraphQL 层类型定义、Schema 组装与 Resolver3.1 类型定义typeDefsapollo/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 合并为可执行 Schemaimport { 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 Resolversviewer 查询与认证 Mutationapollo/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.tsimport { 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 ApolloServerExampleContext({ 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.tslib/user.ts 用内存数组模拟用户表源码注释明确说明真实应用必须使用数据库。安全要点在于密码处理const salt crypto.randomBytes(16).toString(hex); const hash crypto .pbkdf2Sync(password, salt, 1000, 64, sha512) .toString(hex);每个用户生成 16 字节随机盐hex 编码避免彩虹表使用 PBKDF2-SHA5121000 次迭代输出 64 字节哈希validatePassword用用户自己的salt对输入密码重新做 PBKDF2 后再与存储的hash比对绝不存明文。findUser({ email })与validatePassword(user, inputPassword)被 resolvers 直接复用职责边界清晰。5.2 会话 seal/unseallib/auth.tslib/auth.ts 使用hapi/iron实现自包含、可验证的会话setLoginSession(res, session)在会话对象上附加createdAt与maxAge用Iron.seal(obj, TOKEN_SECRET, Iron.defaults)加密并签名后写入 CookiegetLoginSession(req)读取 Cookie 中的 Token 后Iron.unseal还原会话并按createdAt maxAge * 1000校验过期时间过期则抛出Session expired——该异常会被Query.viewer捕获并转换为UNAUTHENTICATED错误。由于 Token 本身被加密签名服务端无需维护内存 session 表天然适配多实例部署。5.3 Cookie 参数细节lib/auth-cookies.tslib/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 窃取 Tokensecure生产环境为true仅通过 HTTPS 传输sameSitelax缓解 CSRFpath/全站可用另外两个工具函数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 同构 Linkfunction 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 undefinedInMemoryCache全新实例若传入initialState来自页面的getStaticProps/getServerSideProps先extract()取出客户端已有缓存用deepmerge将服务端初始状态合并进已有缓存再cache.restore(data)恢复服务端每次请求都返回新客户端避免跨请求串缓存客户端则创建一次后长期复用。配合useApollo(initialState)内部是useMemo(() initializeApollo(initialState), [initialState])和 _app.tsxexport 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.tsxpages/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非空则展示 Youre 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遵循同一模式useMutationgetErrorMessage 表单组件 components/field.tsx。八、从源码结构看的安全边界与适用前提结合源码可以确认并推断出以下使用前提移植到生产环境前需要补齐无数据持久化lib/user.ts 使用内存数组存储用户源码注释明确提示真实应用需替换为 MongoDB、Fauna、SQL 等数据库——重启即丢数据必须设置TOKEN_SECRETlib/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 Routerpages/目录、apollo/server4.x 与as-integrations/next1.xREADME 中提到的apollo-server-integration-next是该集成在旧版本的包名。九、小结与延伸阅读本示例用极小的代码量串起了完整的 GraphQL 鉴权链路pages/api/graphql.ts提供 Apollo Server 4 端点并透传req/res上下文apollo/resolvers.ts在该上下文中完成会话读取与 Cookie 写入lib/auth.tslib/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),仅供参考
返回列表