今天来跟大家分享一个完整的 JWT 登录鉴权方案。话不多说,咱们直接上干货!
前言:为什么需要用户鉴权?
想象一下,你开了个小卖部,但是不记得谁是谁,每次客人来买东西都要重新自我介绍一遍。这得多尴尬?HTTP 协议就是这样的"健忘症患者"——它是无状态的,每次请求都是全新的开始。
所以我们需要一种方式让服务器"记住"用户,这就是鉴权的意义所在。
技术选型:为什么选择 JWT?
传统的 Session + Cookie 方案就像是给每个客人发个号码牌,服务器记住号码对应的客人信息。但 JWT 更酷一些——它就像给每个客人发个身份证,上面写着所有必要信息,服务器只需要验证身份证的真伪就行了。
JWT(JSON Web Token)的核心思想:
{id: 007, username: '高级特工', level: 4...}
→ 通过算法加密 →
→ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTEyLCJ1c2VybmFtZSI6Iuax...
项目结构一览
在开始撸代码之前,先让我们看看这个项目的整体结构。一个清晰的项目架构就像房子的地基,决定了后续开发的舒适度:
jwt-auth-project/
├── src/
│ ├── api/ # API 接口层
│ │ ├── config.js # axios 配置和拦截器
│ │ └── user.js # 用户相关接口
│ ├── components/ # 通用组件
│ │ ├── NavBar/
│ │ │ └── index.jsx # 导航栏组件
│ │ └── RequireAuth/
│ │ └── index.jsx # 路由守卫组件
│ ├── store/ # 状态管理
│ │ └── user.js # 用户状态 (Zustand)
│ ├── views/ # 页面组件
│ │ ├── Home/
│ │ │ └── index.jsx # 首页
│ │ ├── Login/
│ │ │ └── index.jsx # 登录页
│ │ └── Pay/
│ │ └── index.jsx # 支付页 (需要登录)
│ ├── App.jsx # 主应用组件
│ ├── App.css # 应用样式
│ ├── main.jsx # 应用入口
│ └── index.css # 全局样式
├── mock/ # Mock 数据 (vite-plugin-mock)
│ └── index.jsx
├── vite.config.js # Vite 配置
├── package.json
└── README.md # 项目说明 (中文注释很详细哦)
这个结构遵循了几个重要原则:
- 分层架构:api、components、store、views 各司其职
- 功能聚合:相关文件放在同一目录下
- 可扩展性:新增功能时不会破坏现有结构
- 可读性:目录名一目了然
项目架构搭建
现在让我们看看 Vite 配置,这个现代化的构建工具让开发体验飞起:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock'
import path from 'path'
export default defineConfig({
plugins: [
react(),
viteMockServe({
mockPath: 'mock',
localEnabled: true, // 开发环境启用 mock
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src') // 路径别名,再也不用 ../../../ 了
}
}
})
这里用了 vite-plugin-mock 来模拟后端接口,这样在没有后端的情况下也能愉快地开发前端功能。
Mock 接口实现
来看看我们的 Mock:
//mock/login.js
import jwt from 'jsonwebtoken';
// 安全性关键:加盐
const secret = '!xly@1314520$$';
export default [
{
url: '/api/login',
method: 'post',
timeout: 2000, // 模拟网络延迟
response: (req, res) => {
const {username, password} = req.body;
if(username !== 'admin' || password !== '123456'){
return {
code: 1,
message: '用户名或密码错误',
}
}
// 生成 token 颁发令牌
const token = jwt.sign({
user:{
id:"007",
username:'admin'
}
}, secret, {
expiresIn: 86400 // 24小时过期
})
return {
token,
data: {
id: '007',
username: 'admin'
}
}
}
},
{
url: '/api/user',
method: 'get',
response: (req, res) => {
// 从请求头获取 token
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return {
code: 1,
message: 'No token provided'
}
}
try {
const decode = jwt.decode(token, secret);
return {
code: 0,
data: decode.user
}
} catch (error) {
return {
code: 1,
message: 'Invalid token'
}
}
}
}
]
注意这里的 secret,这是 JWT 的核心安全保障。就像做菜要加盐一样,没有它 token 就不安全了!
状态管理:Zustand 轻量级方案
相比 Redux 的繁琐,Zustand 简直是清流般的存在:
// store/user.js
import { create } from 'zustand'
import { doLogin } from '../api/user'
export const useUserStore = create((set) => ({
user: null, // 用户信息
isLogin: false, // 登录状态
// 登录方法
Login: async ({username = "", password = ""}) => {
const res = await doLogin({username, password});
console.log(res);
const {token, data: user} = res.data;
console.log(token, user, '\\\');
// 把 token 存到 localStorage
localStorage.setItem('token', token);
// 更新状态
set({
user,
isLogin: true
})
},
// 登出方法
Logout: () => {
localStorage.removeItem('token');
set({
user: null,
isLogin: false
})
}
}))
HTTP 拦截器:自动携带 Token
手动在每个请求里加 token?那多累呀!咱们用拦截器自动处理:
// api/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){
// 注意这里的 Bearer 前缀,这是标准做法
config.headers.Authorization = `Bearer ${token}`;
}
return config;
})
// 响应拦截器:统一处理响应
axios.interceptors.response.use(res => {
console.log('|||||||||');
return res;
})
export default axios;
API 接口封装
// api/user.js
import axios from './config';
export const getUser = () => {
return axios.get('/user');
}
export const doLogin = (data) => {
return axios.post('/login', data);
}
登录页面:简洁而实用
// views/Login/index.jsx
import { useRef } from 'react';
import { useUserStore } from '../../store/user'
import { useNavigate } from 'react-router-dom'
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) {
alert("请输入用户名或密码");
return;
}
Login({username, password});
// 简单的延迟跳转,实际项目中应该等接口返回成功后再跳转
setTimeout(() => {
navigate('/')
}, 1000)
}
return (
<form onSubmit={handleLogin}>
<div>
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
ref={usernameRef}
placeholder="请输入用户名"
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
ref={passwordRef}
placeholder="请输入密码"
required
/>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
)
}
export default Login;
这里用了 useRef 来获取表单数据,这是非受控组件的写法,简单直接。
导航栏:动态显示用户状态
// components/NavBar/index.jsx
import { Link } from 'react-router-dom'
import { useUserStore } from '../../store/user'
const NavBar = () => {
const {isLogin, user, Logout} = useUserStore();
console.log(isLogin, user, '//++//'); // 调试信息,开发时很有用
return (
<nav style={{padding:'10px', borderBottom:'1px solid #ccc'}}>
<Link to="/">Home</Link>
<Link to="/pay">Pay</Link>
{
isLogin ? (
<>
<span>欢迎:{user.username}</span>
<button onClick={Logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)
}
</nav>
)
}
export default NavBar;
路由守卫:保护需要登录的页面
这是整个鉴权系统的核心组件:
// components/RequireAuth/index.jsx
import { useUserStore } from '../../store/user'
import { useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
const RequireAuth = ({children}) => {
const {isLogin} = useUserStore();
const navigate = useNavigate();
const {pathname} = useLocation();
useEffect(() => {
if(!isLogin){
// 未登录时跳转到登录页,并记录当前路径
navigate('/login', {from: pathname});
}
}, []);
return (
<>
{children}
</>
)
}
export default RequireAuth;
主应用组件:路由配置
// App.jsx
import { useState, useEffect, lazy, Suspense } from 'react'
import './App.css'
import NavBar from './components/NavBar'
import { Routes, Route } from 'react-router-dom'
// 懒加载优化
const Home = lazy(() => import('./views/Home'))
const Login = lazy(() => import('./views/Login'))
const Pay = lazy(() => import('./views/Pay'))
const RequireAuth = lazy(() => import('./components/RequireAuth'))
function App() {
return (
<>
<NavBar />
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* Pay 页面需要登录后才能访问 */}
<Route path="/pay" element={
<RequireAuth>
<Pay />
</RequireAuth>
} />
</Routes>
</Suspense>
</>
)
}
export default App
路由入口文件:
// main.jsx
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { BrowserRouter as Router } from 'react-router-dom'
createRoot(document.getElementById('root')).render(
<Router>
<App />
</Router>
)
核心理念:JWT 的工作原理
让我再详细解释一下 JWT 的工作流程:
- 用户登录:前端发送用户名密码到后端
- 生成 Token:后端验证用户信息,使用
jsonwebtoken库生成 token - 返回 Token:后端将 token 返回给前端
- 存储 Token:前端将 token 存储在 localStorage 中
- 携带 Token:后续请求在 Authorization 头部携带
Bearer ${token} - 验证 Token:后端验证 token 的有效性,解码出用户信息
加盐和安全性
项目中提到的"加盐"(secret)是 JWT 的关键安全措施。就像炒菜要放盐调味一样,JWT 也需要一个密钥来确保数据的完整性和真实性。
// 后端伪代码
const jwt = require('jsonwebtoken');
const secret = 'your-secret-key'; // 这就是"盐"
// 生成 token
const token = jwt.sign(userInfo, secret);
// 验证 token
const decoded = jwt.verify(token, secret);
项目展示
项目亮点总结
- 现代化技术栈:Vite + React + Zustand + React Router
- Mock 数据支持:开发阶段无需后端支持
- 自动 Token 管理:axios 拦截器自动处理
- 路由守卫机制:保护敏感页面
- 懒加载优化:提升应用性能
- 状态管理简洁:Zustand 比 Redux 轻量级
结语
这套 JWT 鉴权方案已经能够满足大部分项目的需求了。代码简洁明了,架构清晰,扩展性良好。