在前端开发的江湖里,“登录”是一个我们绕不开的话题。它看似简单,背后却隐藏着一系列关于状态、安全和用户体验的深思熟虑。我们都遇到过这样的场景:网站是如何“记住”我们的?为什么关闭浏览器再打开,依然是登录状态?
这一切的背后,都离不开“授权”二字。HTTP 协议本身是无状态的,它记不住你是谁。为了解决这个问题,前辈们想出了两种主流方案:一种是基于服务端的 Session-Cookie 机制,另一种则是我们今天要深入探讨的,更现代、更灵活的 JWT (JSON Web Token) 方案。
本文将带你从零开始,在一个完整的 React 项目中,一步步实现一个健壮、可扩展的 JWT 登录鉴权流程。我们将用到 React Router 进行路由管理,Zustand 进行全局状态管理,并用 Axios 处理 API 请求。
读完本文,你不仅能掌握 JWT 的核心思想,更能学会如何搭建一个清晰、优雅的前端认证系统。让我们开始吧!
一、项目架构:一切始于合理的规划
一个好的项目,始于清晰的目录结构。它能让我们的代码更易于维护和理解。在我们的 jwt-demo 项目中,代码被精心组织在以下几个核心目录中:
-
@/mock: 前后端分离开发的“模拟神器”。它让我们在没有后端接口的情况下,也能独立完成前端开发和调试。
-
@/api: 应用的“通信层”。所有与后端的数据交互都集中在这里,方便管理和维护。
-
@/store: 全局状态的“大脑”。我们使用 Zustand 在这里管理用户的登录状态和信息。
-
@/components: 可复用的“积木”。比如导航栏、路由守卫等公共组件。
-
@/views: 用户看到的“页面”。如首页、登录页、支付页等。
-
@/App.jsx & @/main.jsx: 应用的“心脏”。前者负责定义全局路由和布局,后者是整个 React 应用的入口。
这样的分层结构,让每一部分代码都各司其职,是现代前端工程化的最佳实践。
二、JWT 核心思想:一个“令牌”引发的故事
在深入代码之前,我们必须理解 JWT 到底是什么,以及它在整个认证流程中扮演的角色。
想象一下,你登录成功后,服务器不再是在自己家里(Session)给你记个小本本,而是发给你一个加密的“令牌”(Token)。这个令牌本身就包含了你的身份信息和权限。
我们先从宏观上看一下标准的认证流程:
屏幕截图 2025-07-25 102502.png
这个流程清晰地展示了“用户”、“客户端应用”、“授权服务器”和“资源服务器”之间的关系。用户通过授权服务器拿到令牌(Access Token),然后用这个令牌去访问资源服务器上的数据。
而这个令牌(Token),通常就是 JWT。它由三个部分组成:
-
Header (头部) : 描述令牌的元数据,比如类型和加密算法。
-
Payload (载荷) : 存放实际的用户信息或权限信息。
-
Signature (签名) : 这是最关键的部分。服务器用一个密钥 (Secret) ,将头部和载荷进行加密,生成签名。
当你后续访问需要权限的页面时,只需在请求头里带上这个令牌。服务器收到后,会用相同的密钥来验证签名的真伪。如果验证通过,服务器就确认了你的身份和权限,并返回你想要的数据。
这种方式的核心优势在于无状态。服务器无需存储任何 Session 信息,大大减轻了服务器的负担,也使得应用更容易横向扩展。
三、深度辨析:ID Token vs. Access Token
在更专业、更安全的认证体系(如 OAuth 2.0, OpenID Connect)中,我们通常会接触到两种令牌:ID Token 和 Access Token。虽然在我们的简单项目中只用了一个 JWT,但理解它们的区别对你的成长至关重要。
这张图完美地解释了二者的差异:
-
ID Token (身份令牌,左侧蓝色) :
-
作用:像一张身份证,用来确认用户的身份。
-
内容:包含用户的基本信息,如姓名、邮箱、头像等。它的目的是告诉客户端应用:“嘿,这是张三,他已经成功登录了。”
-
给谁看:主要由**前端(客户端)**使用。
-
Access Token (访问令牌,右侧粉色) :
-
作用:像一把**“钥匙”** 或一张 “门禁卡”,用来访问受保护的资源。
-
内容:通常只包含权限信息(scope),比如“读取数据”、“写入数据”。它不应该包含敏感的用户信息。
-
给谁看:主要由**后端(资源服务器)**使用。资源服务器只关心这把“钥匙”能不能开门,不关心“钥匙”是谁的。
在我们的 jwt-demo 项目中,为了简化,我们将身份和权限信息都放在了一个 JWT 里,它同时扮演了两个角色。但在大型应用中,将二者分离是更安全、更规范的设计。
四、实战演练:一步步构建认证流程
理论讲完了,让我们卷起袖子,用代码说话!
第 1 步:用 @/mock 模拟一个“假”后端
在 mock/login.js 中,我们用几行代码就模拟出了登录和获取用户信息的接口。
// mock/login.js
import jwt from'jsonwebtoken';
const secret = 'gjdkgjjg'; // 用于加密的密钥,实际项目中应更复杂且保密
exportdefault [
{
url: '/api/login',
method: 'post',
response: (req) => {
const {username, password} = req.body;
if(username !== 'admin' || password !== '123456'){
return { code: 1, message: '用户名或密码错误' };
}
// 登录成功,用 jwt.sign 颁发令牌
const token = jwt.sign(
{ user: { id: "001", username: "admin" } },
secret,
{ expiresIn: 86400 } // 令牌有效期 24 小时
);
return { token, data: { id: "001", username: "admin" } };
}
},
// ... 其他 mock 接口
]
这里的核心是 jwt.sign() 方法。它接收三样东西:你想塞进令牌的数据(Payload),加密用的密钥(Secret),以及一些配置项(如 expiresIn 有效期)。一个神圣的令牌就此诞生!
第 2 步:用 Zustand 管理全局状态
用户的登录状态是全局的,NavBar 需要知道是否显示欢迎语,路由需要知道是否允许访问。Zustand 是一个轻量级的状态管理库,非常适合这个场景。
在 store/user.js 中,我们创建了一个 useUserStore:
// store/user.js
import { create } from'zustand';
import { doLogin } from'../api/user';
exportconst useUserStore = create(set => ({
user: null, // 用户信息
isLogin: false, // 是否登录
// 登录 action
login: async ({username, password}) => {
const res = await doLogin({username, password});
const { token, data: user } = res.data;
// 核心操作:将令牌存入 localStorage
localStorage.setItem('token', token);
// 更新 store
set({ user, isLogin: true });
},
// 登出 action
logout: () => {
localStorage.removeItem('token');
set({ user: null, isLogin: false });
}
}));
这个 Store 定义了所有与用户状态相关的“原子操作”:
-
login 方法负责调用 API,成功后将令牌存入 localStorage 并更新全局状态。
-
logout 方法则负责清理令牌和状态。
第 3 步:优雅地处理 API 请求
如何让每次 API 请求都自动带上我们的令牌呢?答案是 Axios 拦截器。
在 api/config.js 中,我们配置了一个请求拦截器:
// api/config.js
import axios from "axios";
// 请求拦截器
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token') || "";
if(token){
// 遵循 Bearer token 规范
config.headers.Authorization = `Bearer ${token}`;
}
return config;
})
export default axios;
这段代码太优雅了!它会在每个请求发送前,自动从 localStorage 读取令牌,并将其放入 Authorization 请求头中。从此,我们再也不用在每个 API 调用里手动添加 token 了。
第 4 步:打造登录页面和导航栏
我们的登录页面 views/Login/index.jsx 使用了 useRef 来获取表单值,这是一种非受控组件的实现方式,对于简单表单来说代码更简洁。
// views/Login/index.jsx
const Login = () => {
const usernameRef = useRef();
const passwordRef = useRef();
const { login } = useUserStore(); // 从 store 中获取 login action
const navigate = useNavigate();
const handleLogin = (e) => {
e.preventDefault();
// ...
login({ username, password }); // 调用 action
navigate('/'); // 跳转到首页
}
// ... JSX
}
当用户点击登录,我们直接调用从 useUserStore 中解构出来的 login 方法,一行代码就驱动了整个登录流程!
同时,components/NavBar/index.jsx 组件也在监听 useUserStore 的变化,以动态地展示“登录”链接或“欢迎你,admin”:
// components/NavBar/index.jsx
const NavBar = () => {
const { isLogin, user, logout } = useUserStore();
return (
<nav>
{isLogin ? (
<>
<span>欢迎:{user.username}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)}
</nav>
)
}
第 5 步:用“路由守卫”保护你的页面
有些页面,比如“支付页”,我们不希望未登录的用户看到。这时,“路由守卫”就该登场了。
我们创建了一个 RequireAuth 组件:
// components/RequireAuth/index.jsx
import { useUserStore } from'../../store/user';
import { Navigate, useLocation } from'react-router-dom';
const RequireAuth = ({ children }) => {
const { isLogin } = useUserStore();
const { pathname } = useLocation();
if (!isLogin) {
// 如果未登录,重定向到 /login
// 并将当前想访问的路径记录下来,以便登录后跳回
return<Navigate to="/login" state={{ from: pathname }} replace />;
}
return children;
}
exportdefault RequireAuth;
这个组件逻辑很清晰:如果 isLogin 为 false,就重定向到登录页。
最后,在 App.jsx 中,我们像使用一个普通的组件一样,用它来包裹需要保护的路由:
// App.jsx
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route
path="/pay"
element={
<RequireAuth>
<Pay />
</RequireAuth>
}
/>
</Routes>
);
}
现在,任何试图在未登录状态下访问 /pay 的用户,都会被优雅地引导至登录页面。
五、流程全景图
至此,我们已经串联起了整个认证流程。让我们用一张图来回顾一下:
用户登录页Zustand StoreAxios拦截器Mock API全局状态更新,UI自动响应isLogin is true1. 输入账号密码,点击登录2. 调用 login(data) action3. 发起 doLogin() API 请求4. [请求拦截] 附加 Authorization Header5. 验证通过,返回 Token6. 请求成功,返回数据7. 将 Token 存入 localStorage8. set({ isLogin: true, user })9. 导航到受保护页面(/pay)10. RequireAuth组件检查 isLogin11. 允许访问用户登录页Zustand StoreAxios拦截器Mock API
总结
通过这个项目,我们不仅实现了一个完整的 JWT 登录鉴权流程,更深入地理解了其背后的核心思想和行业最佳实践。
-
职责分离:API、状态管理、UI 组件各司其职,授权服务器和资源服务器的分离也是这一思想的体现。
-
状态驱动:UI 的变化由全局状态 isLogin 统一驱动,实现了真正的响应式。
-
自动化:Axios 拦截器自动处理了 Token 的附加,避免了重复劳动。
-
概念深化:我们辨析了 ID Token 和 Access Token 的区别,这对于构建更大型、更安全的应用至关重要。
希望这篇文章能帮助你彻底搞懂前端登录鉴权。当然,这个项目还有可以优化的地方,比如更优雅地处理 Token 过期、在登录后自动跳转回之前的页面等,这些都留给你作为练习。
作者:爱学习的茄子
https://juejin.cn/post/7530558924165922826
---END---
最近面试BAT,整理一份面试资料《前端面试BAT通关手册》,覆盖了前端技术、CSS、JavaScript、框架、 数据库、数据结构等等。
获取方式:关注公众号并回复 前端 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ♡
在前端开发的江湖里,“登录”是一个我们绕不开的话题。它看似简单,背后却隐藏着一系列关于状态、安全和用户体验的深思熟虑。我们都遇到过这样的场景:网站是如何“记住”我们的?为什么关闭浏览器再打开,依然是登录状态?
这一切的背后,都离不开“授权”二字。HTTP 协议本身是无状态的,它记不住你是谁。为了解决这个问题,前辈们想出了两种主流方案:一种是基于服务端的 Session-Cookie 机制,另一种则是我们今天要深入探讨的,更现代、更灵活的 JWT (JSON Web Token) 方案。
本文将带你从零开始,在一个完整的 React 项目中,一步步实现一个健壮、可扩展的 JWT 登录鉴权流程。我们将用到 React Router 进行路由管理,Zustand 进行全局状态管理,并用 Axios 处理 API 请求。
读完本文,你不仅能掌握 JWT 的核心思想,更能学会如何搭建一个清晰、优雅的前端认证系统。让我们开始吧!
一、项目架构:一切始于合理的规划
一个好的项目,始于清晰的目录结构。它能让我们的代码更易于维护和理解。在我们的 jwt-demo 项目中,代码被精心组织在以下几个核心目录中:
-
@/mock: 前后端分离开发的“模拟神器”。它让我们在没有后端接口的情况下,也能独立完成前端开发和调试。 -
@/api: 应用的“通信层”。所有与后端的数据交互都集中在这里,方便管理和维护。 -
@/store: 全局状态的“大脑”。我们使用 Zustand 在这里管理用户的登录状态和信息。 -
@/components: 可复用的“积木”。比如导航栏、路由守卫等公共组件。 -
@/views: 用户看到的“页面”。如首页、登录页、支付页等。 -
@/App.jsx&@/main.jsx: 应用的“心脏”。前者负责定义全局路由和布局,后者是整个 React 应用的入口。
这样的分层结构,让每一部分代码都各司其职,是现代前端工程化的最佳实践。
二、JWT 核心思想:一个“令牌”引发的故事
在深入代码之前,我们必须理解 JWT 到底是什么,以及它在整个认证流程中扮演的角色。
想象一下,你登录成功后,服务器不再是在自己家里(Session)给你记个小本本,而是发给你一个加密的“令牌”(Token)。这个令牌本身就包含了你的身份信息和权限。
我们先从宏观上看一下标准的认证流程:
这个流程清晰地展示了“用户”、“客户端应用”、“授权服务器”和“资源服务器”之间的关系。用户通过授权服务器拿到令牌(Access Token),然后用这个令牌去访问资源服务器上的数据。
而这个令牌(Token),通常就是 JWT。它由三个部分组成:
-
Header (头部) : 描述令牌的元数据,比如类型和加密算法。 -
Payload (载荷) : 存放实际的用户信息或权限信息。 -
Signature (签名) : 这是最关键的部分。服务器用一个密钥 (Secret) ,将头部和载荷进行加密,生成签名。
当你后续访问需要权限的页面时,只需在请求头里带上这个令牌。服务器收到后,会用相同的密钥来验证签名的真伪。如果验证通过,服务器就确认了你的身份和权限,并返回你想要的数据。
这种方式的核心优势在于无状态。服务器无需存储任何 Session 信息,大大减轻了服务器的负担,也使得应用更容易横向扩展。
三、深度辨析:ID Token vs. Access Token
在更专业、更安全的认证体系(如 OAuth 2.0, OpenID Connect)中,我们通常会接触到两种令牌:ID Token 和 Access Token。虽然在我们的简单项目中只用了一个 JWT,但理解它们的区别对你的成长至关重要。
这张图完美地解释了二者的差异:
-
ID Token (身份令牌,左侧蓝色) :
-
作用:像一张身份证,用来确认用户的身份。 -
内容:包含用户的基本信息,如姓名、邮箱、头像等。它的目的是告诉客户端应用:“嘿,这是张三,他已经成功登录了。” -
给谁看:主要由**前端(客户端)**使用。 -
Access Token (访问令牌,右侧粉色) :
-
作用:像一把**“钥匙”** 或一张 “门禁卡”,用来访问受保护的资源。 -
内容:通常只包含权限信息( scope),比如“读取数据”、“写入数据”。它不应该包含敏感的用户信息。 -
给谁看:主要由**后端(资源服务器)**使用。资源服务器只关心这把“钥匙”能不能开门,不关心“钥匙”是谁的。
在我们的 jwt-demo 项目中,为了简化,我们将身份和权限信息都放在了一个 JWT 里,它同时扮演了两个角色。但在大型应用中,将二者分离是更安全、更规范的设计。
四、实战演练:一步步构建认证流程
理论讲完了,让我们卷起袖子,用代码说话!
第 1 步:用 @/mock 模拟一个“假”后端
在 mock/login.js 中,我们用几行代码就模拟出了登录和获取用户信息的接口。
// mock/login.js
import jwt from'jsonwebtoken';
const secret = 'gjdkgjjg'; // 用于加密的密钥,实际项目中应更复杂且保密
exportdefault [
{
url: '/api/login',
method: 'post',
response: (req) => {
const {username, password} = req.body;
if(username !== 'admin' || password !== '123456'){
return { code: 1, message: '用户名或密码错误' };
}
// 登录成功,用 jwt.sign 颁发令牌
const token = jwt.sign(
{ user: { id: "001", username: "admin" } },
secret,
{ expiresIn: 86400 } // 令牌有效期 24 小时
);
return { token, data: { id: "001", username: "admin" } };
}
},
// ... 其他 mock 接口
]
这里的核心是 jwt.sign() 方法。它接收三样东西:你想塞进令牌的数据(Payload),加密用的密钥(Secret),以及一些配置项(如 expiresIn 有效期)。一个神圣的令牌就此诞生!
第 2 步:用 Zustand 管理全局状态
用户的登录状态是全局的,NavBar 需要知道是否显示欢迎语,路由需要知道是否允许访问。Zustand 是一个轻量级的状态管理库,非常适合这个场景。
在 store/user.js 中,我们创建了一个 useUserStore:
// store/user.js
import { create } from'zustand';
import { doLogin } from'../api/user';
exportconst useUserStore = create(set => ({
user: null, // 用户信息
isLogin: false, // 是否登录
// 登录 action
login: async ({username, password}) => {
const res = await doLogin({username, password});
const { token, data: user } = res.data;
// 核心操作:将令牌存入 localStorage
localStorage.setItem('token', token);
// 更新 store
set({ user, isLogin: true });
},
// 登出 action
logout: () => {
localStorage.removeItem('token');
set({ user: null, isLogin: false });
}
}));
这个 Store 定义了所有与用户状态相关的“原子操作”:
-
login方法负责调用 API,成功后将令牌存入localStorage并更新全局状态。 -
logout方法则负责清理令牌和状态。
第 3 步:优雅地处理 API 请求
如何让每次 API 请求都自动带上我们的令牌呢?答案是 Axios 拦截器。
在 api/config.js 中,我们配置了一个请求拦截器:
// api/config.js
import axios from "axios";
// 请求拦截器
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token') || "";
if(token){
// 遵循 Bearer token 规范
config.headers.Authorization = `Bearer ${token}`;
}
return config;
})
export default axios;
这段代码太优雅了!它会在每个请求发送前,自动从 localStorage 读取令牌,并将其放入 Authorization 请求头中。从此,我们再也不用在每个 API 调用里手动添加 token 了。
第 4 步:打造登录页面和导航栏
我们的登录页面 views/Login/index.jsx 使用了 useRef 来获取表单值,这是一种非受控组件的实现方式,对于简单表单来说代码更简洁。
// views/Login/index.jsx
const Login = () => {
const usernameRef = useRef();
const passwordRef = useRef();
const { login } = useUserStore(); // 从 store 中获取 login action
const navigate = useNavigate();
const handleLogin = (e) => {
e.preventDefault();
// ...
login({ username, password }); // 调用 action
navigate('/'); // 跳转到首页
}
// ... JSX
}
当用户点击登录,我们直接调用从 useUserStore 中解构出来的 login 方法,一行代码就驱动了整个登录流程!
同时,components/NavBar/index.jsx 组件也在监听 useUserStore 的变化,以动态地展示“登录”链接或“欢迎你,admin”:
// components/NavBar/index.jsx
const NavBar = () => {
const { isLogin, user, logout } = useUserStore();
return (
<nav>
{isLogin ? (
<>
<span>欢迎:{user.username}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)}
</nav>
)
}
第 5 步:用“路由守卫”保护你的页面
有些页面,比如“支付页”,我们不希望未登录的用户看到。这时,“路由守卫”就该登场了。
我们创建了一个 RequireAuth 组件:
// components/RequireAuth/index.jsx
import { useUserStore } from'../../store/user';
import { Navigate, useLocation } from'react-router-dom';
const RequireAuth = ({ children }) => {
const { isLogin } = useUserStore();
const { pathname } = useLocation();
if (!isLogin) {
// 如果未登录,重定向到 /login
// 并将当前想访问的路径记录下来,以便登录后跳回
return<Navigate to="/login" state={{ from: pathname }} replace />;
}
return children;
}
exportdefault RequireAuth;
这个组件逻辑很清晰:如果 isLogin 为 false,就重定向到登录页。
最后,在 App.jsx 中,我们像使用一个普通的组件一样,用它来包裹需要保护的路由:
// App.jsx
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route
path="/pay"
element={
<RequireAuth>
<Pay />
</RequireAuth>
}
/>
</Routes>
);
}
现在,任何试图在未登录状态下访问 /pay 的用户,都会被优雅地引导至登录页面。
五、流程全景图
至此,我们已经串联起了整个认证流程。让我们用一张图来回顾一下:
用户登录页Zustand StoreAxios拦截器Mock API全局状态更新,UI自动响应isLogin is true1. 输入账号密码,点击登录2. 调用 login(data) action3. 发起 doLogin() API 请求4. [请求拦截] 附加 Authorization Header5. 验证通过,返回 Token6. 请求成功,返回数据7. 将 Token 存入 localStorage8. set({ isLogin: true, user })9. 导航到受保护页面(/pay)10. RequireAuth组件检查 isLogin11. 允许访问用户登录页Zustand StoreAxios拦截器Mock API
总结
通过这个项目,我们不仅实现了一个完整的 JWT 登录鉴权流程,更深入地理解了其背后的核心思想和行业最佳实践。
-
职责分离:API、状态管理、UI 组件各司其职,授权服务器和资源服务器的分离也是这一思想的体现。 -
状态驱动:UI 的变化由全局状态 isLogin统一驱动,实现了真正的响应式。 -
自动化:Axios 拦截器自动处理了 Token 的附加,避免了重复劳动。 -
概念深化:我们辨析了 ID Token 和 Access Token 的区别,这对于构建更大型、更安全的应用至关重要。
希望这篇文章能帮助你彻底搞懂前端登录鉴权。当然,这个项目还有可以优化的地方,比如更优雅地处理 Token 过期、在登录后自动跳转回之前的页面等,这些都留给你作为练习。
https://juejin.cn/post/7530558924165922826
---END---
最近面试BAT,整理一份面试资料《前端面试BAT通关手册》,覆盖了前端技术、CSS、JavaScript、框架、 数据库、数据结构等等。
获取方式:关注公众号并回复 前端 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ♡

