🔐 前端JWT登录鉴权实战:从原理到Zustand状态管理
现代Web应用中,安全可靠的用户认证系统至关重要。本文将带你深入理解JWT登录鉴权机制,并手把手实现一个完整的React鉴权系统!
🌟 为什么需要JWT登录鉴权?
在传统的Web应用中,服务器通过Session和Cookie来管理用户状态。但在现代前后端分离架构中,JWT(JSON Web Token) 凭借其无状态、跨域友好和易于扩展的特性,成为了主流的认证方案。
JWT vs Cookie/Session
| 特性 | JWT | Cookie/Session |
|---|---|---|
| 状态管理 | 无状态 | 有状态 |
| 跨域支持 | ✅ 优秀 | ❌ 受限 |
| 移动端支持 | ✅ 优秀 | ⚠️ 一般 |
| 扩展性 | ✅ 优秀 | ⚠️ 一般 |
| 安全性 | ✅ 优秀 | ❌ 不太安全,信息较为透明 |
✅ JWT 使用流程
1.引入依赖

2. 用户登录,验证身份
当用户输入用户名和密码后,前端将这些信息发送到后端 /api/login 接口。
示例代码(mock 登录接口):
js
import jwt from 'jsonwebtoken'
const secret = 'ababab' // 加密密钥
export default [
{
url: '/api/login',
method: 'post',
timeout: 2000,
response: (req, res) => {
const { username, password } = req.body
// 验证用户名和密码
if (username !== 'a' || password !== '1') {
return {
code: 1,
message: '用户名或密码出错了'
}
}
// 生成 JWT token
const token = jwt.sign({
user: {
id: '001',
username: 'a'
}
}, secret, {
expiresIn: 86400 // token 有效期为 1 天
})
return {
token,
username,
password
}
}
}
]
login 模块的 mock,模拟发送请求,生成token,进行鉴权判断
如果输入错误的用户名和密码会报错

输入正确的用户名和密码将会得到Token
3. 后端生成 JWT Token
使用 jsonwebtoken 库的 sign 方法生成 Token,结构如下:
js
jwt.sign(payload, secretOrPrivateKey, options)
payload:需要编码的数据,如用户信息。secretOrPrivateKey:加密密钥或私钥。options:可选配置项,如过期时间。
js
const token = jwt.sign({
user: {
id: '001',
username: 'a'
}
}, secret, {
expiresIn: 86400 // token 有效期为 1 天
})
这里涉及到的 secret 和 expiresIn 参数的具体作用如下:
secret
-
定义 :
secret是用于对 JWT 进行签名的密钥或私钥。它可以是一个字符串或者一个缓冲区。 -
作用:
- 签名 :当你创建一个 JWT 时,
jsonwebtoken使用提供的secret对 payload 进行签名。这意味着任何人即使能够查看到 JWT 的内容(因为 JWT 是可以被解码的),如果没有正确的secret,也无法伪造一个有效的 JWT。 - 验证 :当接收到一个 JWT 请求时,服务器端需要使用相同的
secret来验证该 JWT 的真实性。如果签名不匹配,则表示该令牌可能已被篡改,不应被信任。
- 签名 :当你创建一个 JWT 时,
-
安全性 :确保
secret不会被泄露给未经授权的用户。它应该保存在一个安全的地方,并且不应该硬编码在前端应用中。
expiresIn
-
定义 :
expiresIn是一个选项,用来指定生成的 JWT 的过期时间。它可以是秒数,也可以是指定时间长度的字符串(如"2 days"、"10h"、"7d"等)。 -
作用:
- 设置过期时间 :在这个例子中,
expiresIn: 86400表示该 JWT 将在创建后的一天(即 86400 秒)后过期。一旦 JWT 过期,服务器将不再接受它作为有效的认证凭证,除非重新生成一个新的 JWT。 - 增强安全性:通过设置合理的过期时间,可以减少令牌被盗用的风险。较短的有效期意味着攻击者即使获取了令牌,也仅有有限的时间窗口可以利用它进行未授权访问。
- 设置过期时间 :在这个例子中,
综上所述,secret 主要用于保证 JWT 的完整性和真实性,防止令牌被伪造;而 expiresIn 则用于控制 JWT 的生命周期,确保其不会无限期地保持有效状态,从而提高系统的安全性。这两者的结合使用有助于构建一个既安全又灵活的身份验证机制。
4. 前端接收 Token 并存储
登录成功后,前端从响应中获取 Token,并存储到本地(如 localStorage):
js
// 假设你使用 axios 请求登录
const res = await axios.post('/api/login', { username, password })
localStorage.setItem('token', res.data.token)
5. 每次请求携带 Token
前端在每次请求时,将 Token 放入请求头中,通常格式为:
makefile
Authorization: Bearer <your_token_here>

示例:配置 axios 拦截器自动添加 Token
js
// config.js
import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:5173/api'
// 请求拦截器:自动添加 token
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器:处理响应
axios.interceptors.response.use((res) => {
return res
})
export default axios
5. 后端验证 Token
在受保护的接口中,后端需要验证 Token 的合法性。
示例:验证 Token 的接口 /api/user
js
{
url: '/api/user',
method: 'get',
response: (req, res) => {
const token = req.headers.authorization?.split(' ')[1] // 提取 token
if (!token) {
return {
code: 401,
message: '未提供 token'
}
}
try {
const decoded = jwt.verify(token, secret) // 验证并解码 token
return {
user: decoded.user,
message: '成功获取用户信息'
}
} catch (error) {
return {
code: 401,
message: '无效或过期的 token'
}
}
}
}

解释const token = req.headers.authorization?.split(' ')[1]
-
获取
Authorization头 :首先从请求对象req的headers属性中获取名为authorization的头值。注意这里的属性名是小写的,因为 HTTP 头名称在 Node.js 中会被转换为小写。 -
安全访问(可选链操作符
?.) :使用可选链操作符?.来确保如果authorization头不存在时不会抛出错误,而是返回undefined。 -
分割字符串 :使用
.split(' ')方法根据空格分割字符串。因为我们知道Authorization头的格式是Bearer <token>,所以通过这种方式可以轻松地将前缀Bearer和实际的令牌分开。split(' ')返回一个数组,其中第一个元素是Bearer,第二个元素是实际的令牌。[1]表示我们只关心这个数组中的第二个元素,也就是真正的 JWT 令牌。
-
什么是
Bearer: 意思是持有者,表示只要是持有这个Token的人,就被认为是经过授权的
6. Token 验证失败处理
如果 Token 无效或过期,后端应返回 401(Unauthorized)错误,前端可以据此跳转到登录页或刷新 Token。
✅ JWT 的结构说明
JWT 由三部分组成:
- Header(头部) :指定加密算法(如
HS256)和 Token 类型(如JWT)。 - Payload(载荷):包含用户信息、过期时间等数据。
- Signature(签名):使用密钥对前两部分签名,确保 Token 未被篡改。
例如:
erlang
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjp7ImlkIjoiMDAxIiwidXNlcm5hbWUiOiJhIn0sImlhdCI6MTc1MzIzOTUxNSwiZXhwIjoxNzUzMzI1OTE1fQ.
Pm5yPXko6NarIyhkO-QRDrwwX_WQ2vHEG6JPyBxA19M
✅ JWT 与 Cookie 的区别
| 特性 | JWT | Cookie |
|---|---|---|
| 存储位置 | 客户端(localStorage / sessionStorage) | 浏览器自动管理(可跨域) |
| 安全性 | 更安全(需签名验证) | 易受 XSS / CSRF 攻击 |
| 状态管理 | 无状态(适合分布式系统) | 有状态(需服务端维护 session) |
| 跨域支持 | 更好(无需同源限制) | 依赖同源策略 |
✅ JWT 使用流程图
markdown
前端请求登录
↓
后端验证账号密码
↓
生成 JWT Token
↓
返回 Token 给前端
↓
前端存储 Token
↓
后续请求携带 Token
↓
后端验证 Token 合法性
↓
返回受保护资源
🛠 实战:构建React JWT鉴权系统

项目结构概览
bash
jwt-demo
├── mock
│ └── login.js # 模拟登录API
├── src
│ ├── api
│ │ ├── config.js # Axios配置
│ │ └── user.js # 用户API
│ ├── components
│ │ ├── NavBar # 导航栏
│ │ └── RequiredAuth # 路由守卫
│ ├── store
│ │ └── user.js # Zustand状态管理
│ └── views
│ ├── Home # 首页
│ ├── Login # 登录页
│ └── Pay # 支付页(需鉴权)
第一步:配置Axios拦截器
src/api/config.js - 全局请求配置:
jsx
import axios from 'axios';
axios.defaults.baseURL = 'http://localhost:5173/api';
// 请求拦截器 - 自动添加Token
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器 - 统一处理错误
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response.status === 401) {
// Token过期处理
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default axios;
第二步:实现Zustand状态管理
src/store/user.js - 用户状态管理:
jsx
import { create } from 'zustand';
import { doLogin, getUser } from '../api/user';
export const useUserStore = create((set) => ({
user: null,
isLogin: false,
// 登录动作
login: async ({ username, password }) => {
try {
const res = await doLogin({ username, password });
const { token, data: user } = res.data;
localStorage.setItem('token', token);
set({ isLogin: true, user });
return true;
} catch (error) {
console.error('登录失败:', error);
return false;
}
},
// 登出动作
logout: () => {
localStorage.removeItem('token');
set({ isLogin: false, user: null });
},
// 检查登录状态
checkLogin: async () => {
try {
const res = await getUser();
set({ isLogin: true, user: res.data.data });
} catch {
set({ isLogin: false, user: null });
}
}
}));
第三步:创建路由守卫组件
src/components/RequiredAuth/index.jsx - 保护需要登录的页面: 当我想要进入pay页面时,如果未登入则跳转到login页面
jsx
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUserStore } from '../../store/user';
const RequiredAuth = ({ children }) => {
const { isLogin, checkLogin } = useUserStore();
const navigate = useNavigate();
const { pathname } = useLocation();
useEffect(() => {
// 检查登录状态
checkLogin().then(() => {
if (!isLogin) {
navigate('/login', { state: { from: pathname } });
}
});
}, [isLogin, checkLogin, navigate, pathname]);
return isLogin ? children : <div>验证中...</div>;
};
export default RequiredAuth;
第四步:实现登录页面
src/views/Login/index.jsx - 用户登录界面: 这里使用非受控组件完成表单,注意使用时要阻止表单的默认提交,具体内容可以参考🌟 React表单秘籍:受控组件 vs 非受控组件全面解析🌟 React表单秘籍:受控组件 vs 非受控组件全面解析 - 掘金
jsx
import { useRef } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUserStore } from '../../store/user';
const Login = () => {
const usernameRef = useRef();
const passwordRef = useRef();
const { login } = useUserStore();
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from || '/';
const handleLogin = async (e) => {
e.preventDefault();
const username = usernameRef.current.value;
const password = passwordRef.current.value;
if (!username || !password) {
alert('请输入用户名和密码');
return;
}
const success = await login({ username, password });
if (success) {
navigate(from); // 重定向到之前访问的页面
}
};
return (
<div className="login-container">
<h2>用户登录</h2>
<form onSubmit={handleLogin}>
<div className="form-group">
<label htmlFor="username">用户名</label>
<input
type="text"
id="username"
ref={usernameRef}
placeholder="请输入用户名"
defaultValue="a" // 演示用
/>
</div>
<div className="form-group">
<label htmlFor="password">密码</label>
<input
type="password"
id="password"
ref={passwordRef}
placeholder="请输入密码"
defaultValue="1" // 演示用
/>
</div>
<button type="submit" className="login-btn">登录</button>
</form>
</div>
);
};
export default Login;
第五步:配置Mock服务
mock/login.js - 模拟登录API和用户信息:
js
import jwt from 'jsonwebtoken';
const secret = 'ababab'; // 加密密钥
export default [
{
url: '/api/login',
method: 'post',
timeout: 1000,
response: ({ body }) => {
const { username, password } = body;
// 模拟登录验证
if (username !== 'a' || password !== '1') {
return {
code: 1,
message: '用户名或密码错误'
};
}
// 生成JWT令牌
const token = jwt.sign(
{
user: {
id: '001',
username: 'a'
}
},
secret,
{ expiresIn: '1d' } // 1天有效期
);
return {
code: 0,
token,
data: {
id: '001',
username: 'a'
}
};
}
},
{
url: '/api/user',
method: 'get',
response: ({ headers }) => {
const authHeader = headers.authorization;
if (!authHeader) {
return {
code: 401,
message: '未提供认证令牌'
};
}
const token = authHeader.split(' ')[1];
try {
// 验证并解析JWT
const decoded = jwt.verify(token, secret);
return {
code: 0,
data: decoded.user
};
} catch (err) {
return {
code: 401,
message: '无效的令牌'
};
}
}
}
];
第六步:应用路由配置
src/App.jsx - 主应用路由:
jsx
import { lazy, Suspense, useEffect } from 'react';
import { Routes, Route, useNavigate } from 'react-router-dom';
import NavBar from './components/NavBar';
import { useUserStore } from './store/user';
const Home = lazy(() => import('./views/Home'));
const Login = lazy(() => import('./views/Login'));
const Pay = lazy(() => import('./views/Pay'));
const RequiredAuth = lazy(() => import('./components/RequiredAuth'));
function App() {
const { checkLogin } = useUserStore();
const navigate = useNavigate();
useEffect(() => {
// 应用启动时检查登录状态
checkLogin();
}, [checkLogin]);
return (
<div className="app">
<NavBar />
<Suspense fallback={<div className="loading">加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route
path="/pay"
element={
<RequiredAuth>
<Pay />
</RequiredAuth>
}
/>
</Routes>
</Suspense>
</div>
);
}
export default App;
🚀 核心功能亮点
1. 自动Token刷新机制
jsx
// 在axios响应拦截器中添加Token刷新逻辑
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// 尝试刷新Token
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
try {
const { data } = await axios.post('/api/refresh', { refreshToken });
localStorage.setItem('token', data.token);
// 重试原始请求
originalRequest.headers.Authorization = `Bearer ${data.token}`;
return axios(originalRequest);
} catch (refreshError) {
// 刷新失败,跳转登录
useUserStore.getState().logout();
window.location.href = '/login';
}
}
}
return Promise.reject(error);
}
);
2. 细粒度权限控制
jsx
// 扩展路由守卫组件
const RoleBasedAuth = ({ children, requiredRole }) => {
const { user } = useUserStore();
if (!user) {
return <Navigate to="/login" />;
}
if (user.role !== requiredRole) {
return (
<div className="unauthorized">
<h2>权限不足</h2>
<p>您没有访问此页面的权限</p>
</div>
);
}
return children;
};
// 使用示例
<Route
path="/admin"
element={
<RoleBasedAuth requiredRole="admin">
<AdminDashboard />
</RoleBasedAuth>
}
/>
🧪 测试你的JWT系统
-
未登录访问受保护页面
- 尝试访问
/pay,将被重定向到登录页
- 尝试访问
-
登录流程
- 使用用户名
a和密码1登录 - 成功后将重定向到之前尝试访问的页面
- 使用用户名
-
Token验证
- 查看请求头中的Authorization字段
- 服务器端验证Token有效性
💡 最佳实践与安全建议
-
Token存储安全
- 使用
HttpOnlyCookie存储Token(防XSS) - 敏感操作要求重新认证
- 使用
-
短期有效令牌
js// 生成15分钟有效的访问令牌 jwt.sign(payload, secret, { expiresIn: '15m' }); // 生成7天有效的刷新令牌 jwt.sign(payload, refreshSecret, { expiresIn: '7d' }); -
密钥管理
- 使用环境变量存储密钥
- 定期轮换密钥
- 不同环境使用不同密钥
-
增强JWT安全性
js// 在Payload中添加客户端指纹 const fingerprint = crypto .createHash('sha256') .update(req.headers['user-agent']) .digest('hex'); jwt.sign({ user: { id: '001' }, fingerprint }, secret);
🎯 总结
通过本文,我们系统性地实现了基于JWT的前端鉴权系统,核心要点包括:
- JWT工作机制 - Header.Payload.Signature的三段式结构
- 状态管理 - 使用Zustand管理用户状态
- 路由守卫 - 保护需要认证的页面
- Axios拦截器 - 自动化Token管理
- Mock服务 - 模拟后端API
前端安全无小事,JWT只是安全链条中的一环。完整的认证系统还需要HTTPS、CSRF防护、速率限制等多重保障措施。希望本文能为你构建安全可靠的Web应用提供坚实基础!
项目完整代码 :[GitHub仓库链接](lh_ai/react/jwt-demo at main · lhlhlhlhl/lh_ai)(示例链接)

