大家好!我是你们的老朋友FogLetter,今天给大家带来一篇React项目中实现JWT登录鉴权的完整指南。我会用最接地气的方式,带大家从零开始实现一个安全的登录系统。
一、JWT是什么?为什么选择它? 🤔
1.1 传统Session的痛点
记得最开始时,项目用的都是传统的Session-Cookie方案。每次用户登录,服务器创建一个Session,返回一个Session ID存到Cookie里。这种方式有什么问题呢?
- 服务器需要存储Session:用户量一大,内存压力就上来了
- 跨域问题:现在前后端分离是常态,Cookie跨域很麻烦
- 扩展性差:集群部署时,Session共享是个头疼事
1.2 JWT的优雅解决方案
JWT(JSON Web Token)就像是一张"数字身份证",它把用户信息直接编码到Token里,服务器不需要存储任何东西。它的结构分为三部分:
Header.Payload.Signature
举个栗子🌰:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
优点:
- 无状态:服务器不需要存储
- 跨域友好:放在Header里就行
- 自包含:用户信息就在Token里
二、项目实战:React + JWT完整实现 🔨
2.1 项目结构概览
先看看我们的React项目关键结构:
/src
/api
user.js # 用户相关API
axios.js # Axios配置
/store
user.js # Zustand状态管理
/components
RequireAuth.js # 路由守卫
/pages
Login.js # 登录页
...其他页面
2.2 Axios配置:请求拦截与代理
首先配置我们的HTTP客户端,这是与后端通信的桥梁:
// src/api/axios.js
import axios from 'axios'
// 1. 设置基础URL
axios.defaults.baseURL = 'http://localhost:5173/api'
// 2. 请求拦截器 - 添加Token
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token') || ''
if(token){
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 3. 响应拦截器 - 简化响应数据
axios.interceptors.response.use((data) => {
return data.data
})
export default axios
关键点解析:
Bearer是JWT的标准前缀- 响应拦截器直接返回
data.data,这样业务代码就不用每次都写.data了 - 生产环境记得处理错误拦截(这里简化了)
2.3 Zustand状态管理
我们用Zustand来管理用户状态,比Redux简洁多了:
// src/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=''}) => {
try {
const response = await doLogin({username, password})
const {token, data: user} = response
if (!token || !user) {
throw new Error('登录失败')
}
localStorage.setItem('token', token)
set({ user, isLogin: true })
return true
} catch (error) {
console.error('登录失败:', error)
set({ user: null, isLogin: false })
throw error
}
},
// 登出方法
logout: () => {
localStorage.removeItem('token')
set({ user: null, isLogin: false })
},
}))
设计思路:
- 登录成功后将token存到localStorage
- 状态变化自动更新UI
- 错误处理完善,避免页面崩溃
2.4 路由守卫:RequireAuth组件
这是保护我们路由安全的关键:
// src/components/RequireAuth.js
import { useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
const RequireAuth = ({ children }) => {
const navigate = useNavigate()
const { pathname } = useLocation()
useEffect(() => {
const token = localStorage.getItem('token')
if (!token) {
// 记录来源页面,登录后可以跳转回来
navigate('/login', { state: { from: pathname } })
}
}, [])
return <>{children}</>
}
export default RequireAuth
使用方式:
<Route element={<RequireAuth><Layout /></RequireAuth>}>
<Route path="/protected" element={<ProtectedPage />} />
</Route>
进阶优化:
- 可以检查token是否过期(需要解码JWT)
- 可以添加权限校验逻辑
2.5 登录页面实现
一个简单的登录表单示例:
// src/pages/Login.js
import { useState } from 'react'
import { useUserStore } from '@/store/user'
import { useLocation, useNavigate } from 'react-router-dom'
export default function Login() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const login = useUserStore(state => state.login)
const navigate = useNavigate()
const location = useLocation()
const from = location.state?.from || '/home'
const handleSubmit = async (e) => {
e.preventDefault()
try {
await login({ username, password })
navigate(from, { replace: true })
} catch (error) {
alert('登录失败: ' + error.message)
}
}
return (
<div className="login-container">
<h2>欢迎登录</h2>
<form onSubmit={handleSubmit}>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="用户名"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
</div>
)
}
用户体验优化点:
- 记住来源页面,登录后自动跳转
- 错误提示友好
- 加载状态处理(可以添加loading动画)
三、JWT安全最佳实践 🔒
3.1 前端安全注意事项
- 不要将敏感信息存入JWT:JWT可以被解码(只是Base64编码)
- 使用HTTPS:防止Token被中间人攻击
- 合理设置Token过期时间:通常1-2小时
- 实现Token自动刷新:快过期时用refresh token获取新token
3.2 处理Token过期
改进我们的RequireAuth组件:
// 新增jwt解码函数
const decodeJWT = (token) => {
try {
return JSON.parse(atob(token.split('.')[1]))
} catch {
return null
}
}
const RequireAuth = ({ children }) => {
// ...原有代码
useEffect(() => {
const token = localStorage.getItem('token')
if (!token) {
navigate('/login', { state: { from: pathname } })
return
}
// 检查是否过期
const payload = decodeJWT(token)
if (payload && payload.exp * 1000 < Date.now()) {
localStorage.removeItem('token')
navigate('/login', { state: { from: pathname } })
}
}, [])
// ...原有代码
}
四、常见问题与解决方案 💡
4.1 跨域问题
如果你的前后端分离部署,可能会遇到:
Access-Control-Allow-Origin 错误
解决方案:
- 后端设置CORS头部
- 开发环境配置代理(vite示例):
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://your-backend.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
4.2 Token失效处理
当后端返回401时,我们可以统一处理:
// 在axios响应拦截器中添加
axios.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
五、总结与扩展 🎯
通过这篇文章,我们完整实现了:
- Axios的请求拦截和JWT自动添加
- Zustand状态管理登录状态
- 路由守卫保护敏感页面
- 完整的登录登出流程
扩展思考:
- 如何实现"记住我"功能?(长过期时间的token)
- 如何做权限控制?(在JWT payload中添加roles)
- 如何实现多端登录?(可以加入设备信息)
如果你觉得这篇文章有帮助,别忘了点赞收藏!有什么问题欢迎在评论区讨论,我会定期回复大家的问题。