news 2026/9/25 16:11:11

How To GraphQL:用 React + Apollo Client 实现登录注册与请求级认证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
How To GraphQL:用 React + Apollo Client 实现登录注册与请求级认证

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载

本篇指南基于 How To GraphQL 教程中 React + Apollo 前端的 Authentication 章节,带你完整走通一套前后端配合的认证方案:用signup/login两个 GraphQL Mutation 获取 JWT、把 token 存入浏览器、再通过 Apollo 的setContextLink 为每个请求自动附加Authorization头,最终让服务端识别“以谁的身份”发起请求并据此关联数据。

完整的认证链路可以概括为四步:

  1. 用户在/login页面提交表单,前端通过useMutation发起signup或loginMutation;
  2. Mutation 返回token,前端将其写入localStorage并跳转回首页;
  3. 之后ApolloClient发出的每个请求,都会经过authLink这个中间件把 token 拼进Authorization: Bearer <token>请求头;
  4. 服务端从Authorization头中解析出userId放入 resolver 的context,从而在创建Link时自动关联postedBy。

以下代码示例均来自教程配套的hackernews-react-apollo前端项目与server后端项目,章节原文见 5-authentication.md。

一、准备 Login 组件与路由

1. Login 组件:两种状态的登录/注册表单

认证功能的起点是src/components/Login.js组件。它用一份formState同时承载登录和注册两种模式:

  • formState.login为true时,面向已有账户的用户,只渲染email和password两个输入框;
  • formState.login为false时,面向尚未注册的用户,额外渲染一个name输入框用于创建账户。

提交按钮的onClick稍后会绑定到两个 Mutation,此处先保留占位逻辑:

// src/components/Login.js import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; const Login = () => { const navigate = useNavigate(); const [formState, setFormState] = useState({ login: true, email: '', password: '', name: '' }); return ( <div> <h4 className="mv3"> {formState.login ? 'Login' : 'Sign Up'} </h4> <div className="flex flex-column"> {!formState.login && ( <input value={formState.name} onChange={(e) => setFormState({ ...formState, name: e.target.value }) } type="text" placeholder="Your name" /> )} <input value={formState.email} onChange={(e) => setFormState({ ...formState, email: e.target.value }) } type="text" placeholder="Your email address" /> <input value={formState.password} onChange={(e) => setFormState({ ...formState, password: e.target.value }) } type="password" placeholder="Choose a safe password" /> </div> <div className="flex mt3"> <button className="pointer mr2 button" onClick={() => console.log('onClick')} > {formState.login ? 'login' : 'create account'} </button> <button className="pointer button" onClick={(e) => setFormState({ ...formState, login: !formState.login }) } > {formState.login ? 'need to create an account?' : 'already have an account?'} </button> </div> </div> ); }; export default Login;

第二个按钮通过login: !formState.login在两种模式间切换,从而在同一个页面里复用了登录与注册两个流程。

2. 定义 localStorage 中 token 的键名

在src下新建constants.js,统一声明用于在浏览器localStorage中存放凭据的键名:

// src/constants.js export const AUTH_TOKEN = 'auth-token';

安全提示:把 JWT 存放在localStorage并不是前端认证的安全做法(一旦页面存在 XSS,token 即可被脚本读取并随请求发走)。本教程之所以这样处理,是为了聚焦 GraphQL 本身、保持示例简洁。生产环境应结合 HttpOnly Cookie、更短的 token 有效期与刷新机制等方案来权衡,切勿直接照搬。

3. 注册 /login 路由

打开App.js,把Login加入路由表:

// src/components/App.js // ... import Login from './Login'; const App = () => { return ( <div className="center w85"> <Header /> <div className="ph3 pv1 background-gray"> <Routes> <Route path="/" element={<LinkList/>} /> <Route path="/create" element={<CreateLink/>} /> <Route path="/login" element={<Login/>} /> </Routes> </div> </div> ); }; export default App;

4. Header 组件:按登录态渲染入口

打开Header.js,让它根据localStorage中是否存在 token 来切换界面:未登录时右侧显示login入口、不渲染submit入口;已登录时显示logout,并放出submit入口(提交新链接)。这样在 UI 层就能保证“只有认证用户才看得到创建链接的入口”:

// src/components/Header.js import React from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { AUTH_TOKEN } from '../constants'; const Header = () => { const navigate = useNavigate(); const authToken = localStorage.getItem(AUTH_TOKEN); return ( <div className="flex pa1 justify-between nowrap orange"> <div className="flex flex-fixed black"> <Link to="/" className="no-underline black"> <div className="fw7 mr1">Hacker News</div> </Link> <Link to="/" className="ml1 no-underline black"> new </Link> <div className="ml1">|</div> <Link to="/search" className="ml1 no-underline black" > search </Link> {authToken && ( <div className="flex"> <div className="ml1">|</div> <Link to="/create" className="ml1 no-underline black" > submit </Link> </div> )} </div> <div className="flex flex-fixed"> {authToken ? ( <div className="ml1 pointer black" onClick={() => { localStorage.removeItem(AUTH_TOKEN); navigate(`/`); }} > logout </div> ) : ( <Link to="/login" className="ml1 no-underline black" > login </Link> )} </div> </div> ); }; export default Header;

两处细节值得注意:

  • authToken = localStorage.getItem(AUTH_TOKEN)是整个组件渲染逻辑的开关:没有 token 时submit不渲染;
  • 点击logout时执行localStorage.removeItem(AUTH_TOKEN)并navigate('/')回到首页——因为 token 是前端唯一的状态源,清掉它即完成登出,下次请求自然不再携带Authorization头。

二、实现 signup 与 login 两个 Mutation

signup和login就是两个普通的 GraphQL Mutation,用法与前面章节里的createLink完全一致。在Login.js顶部定义两份 mutation 文档:

// src/components/Login.js const SIGNUP_MUTATION = gql` mutation SignupMutation( $email: String! $password: String! $name: String! ) { signup( email: $email password: $password name: $name ) { token } } `; const LOGIN_MUTATION = gql` mutation LoginMutation( $email: String! $password: String! ) { login(email: $email, password: $password) { token } } `;

两个 Mutation 结构非常相似:接收若干入参,返回一个token。这个 token 之后会被附加到所有后续请求上,用来向服务端表明“请求是以该用户的名义发出的”。

1. 用 useMutation 绑定表单

把提交按钮的onClick改为三目表达式,按当前状态分别调用login或signup:

<div className="flex mt3"> <button className="pointer mr2 button" onClick={formState.login ? login : signup} > {formState.login ? 'login' : 'create account'} </button> <button className="pointer button" onClick={(e) => setFormState({ ...formState, login: !formState.login }) } > {formState.login ? 'need to create an account?' : 'already have an account?'} </button> </div>

然后在组件内用 Apollo 的useMutationhook 创建这两个 mutation 触发器:

const [login] = useMutation(LOGIN_MUTATION, { variables: { email: formState.email, password: formState.password }, onCompleted: ({ login }) => { localStorage.setItem(AUTH_TOKEN, login.token); navigate('/'); } }); const [signup] = useMutation(SIGNUP_MUTATION, { variables: { name: formState.name, email: formState.email, password: formState.password }, onCompleted: ({ signup }) => { localStorage.setItem(AUTH_TOKEN, signup.token); navigate('/'); } });

三个要点:

  • variables直接取自表单状态,mutation 执行时把当前输入作为变量发送;
  • onCompleted回调在 mutation 成功完成后触发:先把响应中的login.token/signup.token写入localStorage(键名即AUTH_TOKEN),再跳转回首页;
  • 别忘了补齐 import:
import { useMutation, gql } from '@apollo/client'; import { useNavigate } from 'react-router-dom'; import { AUTH_TOKEN } from '../constants';

2. 本地验证

运行yarn start,打开http://localhost:3000/login,点击need to create an account?切换到注册模式,填写name、email、password后点击create account。一切正常的话,应用会跳回首页,并且 Header 中submit按钮重新出现——说明 token 已写入localStorage。

要确认用户确实落库,可以在 database 项目的 dev Playground 中发送users查询验证新用户存在。

三、用 Apollo Link 为所有请求附加认证头

登录成功只是拿到了 token,真正的关键是让之后发出的每一个 GraphQL 请求都自动携带它。由于所有 API 请求都是由应用根部的ApolloClient实例创建并发送的,需要在index.js中为它配置一个认证 Link。

Apollo 的 Link 机制允许你构建“中间件”:在请求到达httpLink之前对其进行修改。认证正是这一机制的典型用法——用setContext创建一个authLink,放在httpLink与ApolloClient实例化之间:

// src/index.js const authLink = setContext((_, { headers }) => { const token = localStorage.getItem(AUTH_TOKEN); return { headers: { ...headers, authorization: token ? `Bearer ${token}` : '' } }; });

配套 import:

import { setContext } from '@apollo/client/link/context'; import { AUTH_TOKEN } from './constants';

工作原理分两步:每次ApolloClient发请求时都会执行这个回调;回调先从localStorage取出 token(如果存在),然后把authorization: Bearer <token>合并进请求头返回给 context,供后续的httpLink读取并真正发出 HTTP 请求。

最后把 link 链交给客户端:

const client = new ApolloClient({ link: authLink.concat(httpLink), cache: new InMemoryCache() });

authLink.concat(httpLink)表示请求先经过authLink注入请求头,再交给httpLink发起网络调用。至此,只要localStorage里有 token,所有 API 请求都会被自动认证;登出后(token 被移除),authorization头变为空串,请求即回到匿名状态。

这正是本章节配套测验考察的核心点:“ApolloClient 发出的 HTTP 请求是如何认证的?”正确答案不是给ApolloClient传参或给它传一个认证方法,而是通过专门的 Apollo Link 中间件把认证 token 附加到请求上。

四、服务端如何消费这个 token

前端的最终目的,是让服务端能识别请求身份。教程的后端项目(server)在server/src/resolvers/Mutation.js中的postresolver 展示了这一过程:

// server/src/resolvers/Mutation.js async function post(parent, args, context, info) { const { userId } = context; let postedBy = undefined if (userId) { postedBy = { connect: { id: userId } } } const newLink = await context.prisma.link.create({ data: { url: args.url, description: args.description, postedBy } }); context.pubsub.publish('NEW_LINK', newLink); // not important for now return newLink; }

逐行拆解:

  • const { userId } = context:userId是服务端在构造 request context 时,从Authorization请求头中解析 JWT 得到的。请求未携带该头时userId为undefined;
  • 若userId存在,则构造postedBy = { connect: { id: userId } },用 Prisma 的connect写法把即将创建的Link直接外键关联到提交它的User;
  • 若用户未登录,postedBy保持undefined,Prisma 会忽略该字段,Link便没有归属用户。教程特意允许“无主链接”存在,仅出于演示考虑,生产应用通常应改为强制认证;
  • context.pubsub.publish('NEW_LINK', newLink)会在后续订阅章节中发挥作用,用于向订阅者推送新链接事件。

对照服务端实现:token 从哪里来、userId 如何入 context

本仓库中配套的后端教程 graphql-js 认证章节 给出了同一套方案的完整服务端实现,可对照理解前后端是如何咬合的:

  • token 的签发:signupresolver 先用bcrypt.hash(args.password, 10)对密码做哈希入库,再执行jwt.sign({ userId: user.id }, APP_SECRET),把userId装进 JWT 载荷并用APP_SECRET签名(见 signup/login resolver);loginresolver 则按email查库、用bcrypt.compare比对密码,校验通过后签发同样的 token;
  • userId 的解析:工具函数getUserId从req.headers.authorization取出Bearer前缀后的 token,调用jwt.verify(token, APP_SECRET)验证签名后解出userId,任何一步失败都会抛Not authenticated异常(见 getUserId 实现);
  • context 的组装:Apollo Server 的context被写成一个函数,在每个请求到来时执行userId: req && req.headers.authorization ? getUserId(req) : null,从而把“认证后的身份”注入context,供post等 resolver 直接读取(见 context 配置)。

把前后端两段代码放在一起看,整条链路就非常清晰了:前端authLink写入的Authorization: Bearer <token>头,正是服务端getUserId读取并验证的同一份数据;userId进入context后,又被postresolver 用于connect关联postedBy。前端负责“把身份带过去”,服务端负责“验证身份并消费它”,中间没有任何额外通道。

小结

本章节完成的认证方案可以归纳为四个关键件:

组件位置职责
Login组件 + 路由src/components/Login.js、App.js提供登录/注册双态表单,触发对应 Mutation
AUTH_TOKEN常量src/constants.js统一localStorage中的 token 键名,贯穿组件与 Link
authLink(setContext)src/index.js每次发请求前把localStorage中的 token 写入Authorization: Bearer头
postresolverserver/src/resolvers/Mutation.js从context.userId(由 JWT 解析而来)自动关联postedBy

这套模式——“Mutation 返回 token、客户端中间件统一附加请求头、服务端从 context 读取身份”——是 GraphQL 项目中最常见的无状态认证方式。理解了它之后,后续章节的投票、订阅等功能都可以建立在这个身份体系之上继续扩展(教程整体章节规划可参考 React + Apollo 课程结构)。

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载
上一篇:Axure 汉化 3 分钟搞定:axure-cn 免费中文语言包安装教程
下一篇:VC++ 运行库修复:一条命令装齐 2005–2022 全版本

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

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

868MHz工业射频模块设计要点与工程落地指南

1. 这不是又一个“通用模块”&#xff0c;而是专为868MHz工业场景打磨的硬核器件你手头如果正在做智能表计、农业传感器网络、工业远程IO或者低功耗楼宇自控系统&#xff0c;看到“868MHz频段专用”这八个字&#xff0c;应该立刻停下手里的调试板——这不是营销话术&#xff0c…

作者头像 李华
网站建设 2026/9/25 16:06:26

腾讯云WorkBuddy Enterprise企业级AI平台与Agent生态实战指南

1. 从零理解 WorkBuddy Enterprise 的定位与核心价值1.1 这个平台到底解决什么问题WorkBuddy Enterprise 是腾讯云推出的一套企业级 AI 平台与 Agent 生态产品。说白了&#xff0c;它要解决的核心问题是&#xff1a;企业想用 AI&#xff0c;但不知道怎么把 AI 能力安全、可控、…

作者头像 李华
网站建设 2026/9/25 16:04:26

工业缺陷检测实战:UNet++热力图+Flask监管看板

简介&#xff1a;本资源是一套基于Python与深度学习的工业表面缺陷检测与可视化监管系统源码&#xff0c;专为计算机、人工智能及相关专业本科生毕业设计与课程实践打造&#xff0c;解决制造业质检中缺陷识别精度低、监管流程不透明等实际问题。压缩包共241个文件&#xff0c;含…

作者头像 李华
网站建设 2026/9/25 16:04:00

5G核心网N1/N2/N3/N4/N6接口实战解析:传什么、怎么传、为何这样设计

1. 这不是教科书里的抽象图——5GC接口N1/N2/N3/N4/N6到底在干啥&#xff1f;你打开任何一份3GPP TS 23.501文档&#xff0c;第一眼看到的5GC架构图里密密麻麻全是带N前缀的连线&#xff1a;N1、N2、N3、N4、N6、N9……它们不是编号游戏&#xff0c;也不是工程师画图时随手填的…

作者头像 李华
网站建设 2026/9/25 15:57:41

开源大模型本地部署与安全实战:Qwen微调、微软工具链与谷歌生态

1. 开源AI浪潮下的技术选型与安全博弈过去一年里&#xff0c;我身边做开发和运维的朋友聊得最多的话题&#xff0c;从“你用了哪个API”逐渐变成了“你本地跑了哪个模型”。这个转变背后其实是一个很明显的信号&#xff1a;开源大模型的能力已经跨过了“能用”的门槛&#xff0…

作者头像 李华