在今天的项目中,我实现了一个完整的 JWT(JSON Web Token)登录鉴权系统,涵盖了从状态管理到路由守卫的全流程。我们要实现实现一个如图的效果,当我们点击Pay,我们没有处于已登录的状态,我们会直接跳转到登录页面,默认用户名和密码为“admin”,“123456”,当我们登录成功后,我们的本地会存储我们的用户信息,包括用户名和密码,当然是经过加密的,作为身份验证:
以下是核心内容的技术总结:
一、JWT 鉴权核心原理
JWT 解决了 HTTP 无状态协议下的用户认证问题,其工作流程如下:
- 登录认证:用户提交凭证 → 服务端验证 → 生成含用户信息的 JWT
- 令牌返回:服务端返回 JWT 给客户端(存储于 localStorage)
- 请求鉴权:客户端在请求头添加
Authorization: Bearer <token> - 服务端验证:解析 JWT 获取用户信息 → 响应请求
我们曾经使用过cookie来做身份认证,但是cookie是明文的,不安全,容易被窃取,安全隐患大,我写过一篇关于cookie的文章,感兴趣的可以看看 当饼干遇上代码:一场HTTP与Cookie的奇幻漂流 🍪🌊
二、关键实现模块详解
1. Mock 服务实现(login.js)
当然要使用jwt,我们首先得先安装 pnpm i jsonwebtoken
import jwt from 'jsonwebtoken'
// login 模块 mock
// 安全性 编码的时候加密
// 解码的时候用于解码
// 加盐
const secret = '!&124coddefgg'
export default [
{
url: '/api/login',
method: 'post',
timeout: 2000, // 请求耗时
response: (req, res) => {
// req, username, password
const { username, password } = req.body;
if (username !== 'admin' || password !== '123456') {
return {
code: 1,
message: '用户名或密码错误'
}
}
// 生成token 颁发一个令牌
// json 用户数据
const token = jwt.sign({
user: {
id: "001",
username: "admin",
}
}, secret, {
expiresIn: 86400, // 有效期24小时
})
return {
token,
data: {
id: "001",
username: "admin"
}
}
}
},
{
url: '/api/user',
method: 'get',
response: (req, res) => {
// 用户端 token headers
const token = req.headers['authorization'].split(' ')[1];
console.log(token);
try {
const decode = jwt.verify(token, secret)
console.log(decode);
return {
code: 0,
data: decode.user
}
} catch (err) {
return {
code: 1,
message: 'Invalid Token'
}
}
return {
token
}
}
}
]
-
安全要点:
- 使用
secret密钥签名防止篡改 expiresIn设置令牌有效期增强安全性- 验证时严格捕获异常防止服务崩溃
- 使用
我们用apifox 来测试一下是否可以成功得拿到数据,测试的地址就是你的react项目端口,加上我们设置的url,可以看到成功返回了数据,返回的经过加盐的token:
科普时间
我们上述提到一个加盐的概念,什么是
加盐呢?加盐是一种增加额外数据到密码或数据输入的技术,目的是保护数据不被通过预计算的攻击方法轻易破解。具体来说,盐值是一个随机生成的数据片段,它被添加到密码或其他敏感数据中,然后再进行哈希处理。
我们再来试试模拟服务器来进行解密呢,可以看到也是能成功解密的:
const token = req.headers['authorization'].split(' ')[1]大家可能对这段代码有点疑惑,当我们带着带着身份信息,这通常存放在Authorization:'Bearer ${token}' Bearer表示持有者,通常都会加上,所以我们在解码token的时候需要以空格分割
2. Axios 全局配置(/api/config.js)
模块化axios,不需要频繁的设置基础地址和token等
// 请求拦截器:自动注入Token
import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:5173/api'
// 请求拦截器
axios.interceptors.request.use(config => {
// console.log('////');
let token = localStorage.getItem('token') || '';
if (token) config.headers.Authorization = `Bearer ${token}`
// config.headers.Authorization = token
return config
})
// 响应拦截器
axios.interceptors.response.use(res => {
console.log('???');
return res
})
export default axios
模块后的请求方法
import axios from './config'
export const getUser = async () => {
return await axios.get('/user')
}
export const doLogin = async (data) => {
return await axios.post('/login', data)
}
-
设计亮点:
- 统一处理认证逻辑,避免重复代码
Bearer前缀符合 OAuth 2.0 规范- 基础 URL 集中配置便于环境切换
3. Zustand 状态管理(user.js)
export const useUserStore = create((set) => ({
user: null,
isLogin: false,
login: async ({ username, password }) => {
const res = await doLogin({ username, password })
const { token, data: user } = res.data;
localStorage.setItem('token', token);
set({ user, isLogin: true }) // 更新状态
},
logout: () => {
localStorage.removeItem('token')
set({ user: null, isLogin: false })
}
}))
-
状态设计:
user:存储用户信息对象isLogin:布尔值登录状态- 登录/登出操作封装为原子方法
三、前端组件实现技巧
1. 路由守卫(/components/RequireAuth.jsx)
const RequireAuth = ({ children }) => {
const { isLogin } = useUserStore()
const navigate = useNavigate()
const { pathname } = useLocation()
useEffect(() => {
if (!isLogin) {
navigate('/login', { state: { from: pathname } }) // 记录来源页面
}
}, [])
return <>{children}</>
}
-
实现要点:
- 在
useEffect中执行跳转避免渲染阻塞 - 传递来源路径实现登录后自动回跳
- 纯组件设计不影响子组件渲染
- 在
2. 登录表单(/view/Login/index.jsx)
const Login = () => {
const usernameRef = useRef()
const passwordRef = useRef()
const { login } = useUserStore()
const navigate = useNavigate()
const handleLogin = (e) => {
e.preventDefault()
const username = usernameRef.current.value
const password = passwordRef.current.value
if (!username || !password) {
return alert('请输入用户名和密码')
}
login({ username, password })
setTimeout(() => navigate('/'), 1000) // 登录成功跳转
}
return (
<form onSubmit={handleLogin}>
{/* 表单内容 */}
</form>
)
}
-
优化点:
useRef替代useState减少渲染次数- 表单提交触发状态管理方法
- 定时跳转提供视觉反馈时间
3. 导航栏(/view/NavBar/index.jsx)
const NavBar = () => {
const { isLogin, user, logout } = useUserStore()
return (
<nav>
<Link to='/'>Home</Link>
<Link to='/pay'>Pay</Link>
{isLogin ? (
<>
<span>欢迎您,{user.username}</span>
<button onClick={logout}>退出登录</button>
</>
) : (
<Link to='/login'>登录</Link>
)}
</nav>
)
}
-
状态绑定:
- 根据
isLogin动态渲染内容 - 直接调用
logout方法实现状态同步清除
- 根据
四、核心流程剖析
-
初始化应用:
- 检查 localStorage 是否存在 token
- 有 token 则设置 isLogin=true 并获取用户信息
-
登录流程:
3.受保护路由访问
4.API 请求鉴权:
// 请求拦截器自动注入
GET /api/user
Headers:
Authorization: Bearer eyJhbGciOi...
总结
通过本次实践,我深入理解了 JWT 鉴权在前端应用中的完整实现链路,掌握了 Zustand 状态管理与 axios 拦截器的配合技巧。关键收获:
- 认证与状态分离:JWT 提供无状态认证,前端状态管理负责用户体验
- 拦截器威力:极大减少重复代码,实现关注点分离
- 路由守卫模式:提供简洁的权限控制方案
- Mock API 价值:前后端并行开发的关键基础设施
本项目完整代码已托管至 GitHub 仓库,包含详细注释和优化建议。