JWT登录鉴权在前端应用中的实现与实践
引言
在现代Web应用开发中,用户认证和授权是至关重要的安全功能。JSON Web Token (JWT)作为一种轻量级的认证方案,因其简单性、自包含性和跨语言支持等优点,已成为众多开发者的首选。本文将基于提供的代码和笔记,详细解析如何在一个React应用中实现JWT登录鉴权系统。
项目结构概述
首先让我们看一下项目的整体结构:
text
- src/
- api/
- config.js # Axios全局配置
- user.js # 用户相关API
- components/
- NavBar/ # 导航栏组件
- RequireAuth/ # 路由守卫组件
- store/
- user.js # 用户状态管理(Zustand)
- views/
- Home/ # 首页
- Login/ # 登录页
- Pay/ # 需要认证的支付页
- App.jsx # 主应用组件
JWT基础知识
什么是JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。它由三部分组成:
- Header - 包含令牌类型和使用的哈希算法
- Payload - 包含声明(claims),即用户数据和其他元数据
- Signature - 用于验证消息在传输过程中未被更改
JWT工作流程
- 用户提交登录凭证(用户名/密码)
- 服务器验证凭证,生成JWT并返回给客户端
- 客户端存储JWT(通常使用localStorage)
- 后续请求在Authorization头中携带JWT
- 服务器验证JWT并处理请求
代码实现解析
1. 用户状态管理 (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 data = await doLogin({ username, password })
console.log(data)
const { token, data: user } = data.data
console.log(token, user);
localStorage.setItem('token', token)
set({
isLogin: true,
user,
})
},
logout: () => {
localStorage.removeItem('token')
set({
isLogin: false,
user: null,
})
},
setUser: (user) => set({ user }),
}))
功能解析:
-
使用Zustand创建全局状态管理
-
维护
user和isLogin两个核心状态 -
login方法:- 调用登录API
- 将返回的token存入localStorage
- 更新全局状态
-
logout方法:- 移除localStorage中的token
- 重置全局状态
设计考虑:
- 将认证状态集中管理,便于全局访问
- 封装登录/登出逻辑,保持一致性
- 使用localStorage持久化token,避免页面刷新后状态丢失
2. Axios全局配置 (api/config.js)
import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:5173/api'
// 拦截器
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token') || ""
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
axios.interceptors.response.use((res) => {
console.log('////')
return res
})
export default axios
功能解析:
-
设置基础API地址
-
当我们后续请求当中,我们如何在Authorization头中携带JWT,使用请求拦截器:
- 从localStorage获取token
- 如果存在token,自动添加到请求头的
Authorization字段
-
响应拦截器(当前仅做简单日志记录)
设计考虑:
- 自动附加认证信息,避免手动设置
- 统一管理API配置,便于维护
- 遵循JWT最佳实践,使用
Bearertoken (Bearer 其实就是说明我持有这个token)
3. API服务封装 (api/user.js)
import axios from './config'
export const getUser = () => {
return axios.get('/user')
}
export const doLogin = (data) => {
return axios.post('/login', data)
}
功能解析:
- 封装用户相关API
doLogin- 处理登录请求getUser- 获取用户信息(当前未使用)
设计考虑:
- 集中API定义,便于管理和重用
- 基于配置好的axios实例,确保一致的行为
4. 路由守卫组件 (components/RequireAuth/index.jsx)
import { useUserStore } from '../../store/user'
import { useNavigate, useLocation } from 'react-router-dom'
import { useEffect } from 'react'
const RequireAuth = ({ children }) => {
const { isLogin } = useUserStore()
const navigate = useNavigate()
const { pathname } = useLocation()
useEffect(() => {
if (!isLogin) {
navigate('/login', { from: pathname })
}
}, [])
return (
<>
{children}
</>
)
}
export default RequireAuth
功能解析:
- 检查用户是否登录
- 如果未登录,重定向到登录页并记录原始路径
- 如果已登录,渲染子组件
设计考虑:
- 保护需要认证的路由
- 提供友好的重定向体验,登录后可返回原页面
- 使用React Router的hooks实现导航
5. 导航栏组件 (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>Welcome, {user.username}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)
}
</nav>
)
}
export default NavBar
功能解析:
-
显示全局导航
-
根据登录状态显示不同内容:
- 已登录:显示欢迎信息和登出按钮
- 未登录:显示登录链接
设计考虑:
- 提供一致的导航体验
- 直观反映认证状态
- 方便的登出功能
6. 登录页面 (views/Login/index.jsx)
import { useRef } from 'react'
import { useUserStore } from '../../store/user'
import { useNavigate } from 'react-router-dom'
const Login = () => {
const usernameRef = useRef(null)
const passwordRef = useRef(null)
const { login } = useUserStore()
const navigate = useNavigate()
const handleSubmit = (e) => {
e.preventDefault()
const username = usernameRef.current.value
const password = passwordRef.current.value
if (!username || !password) {
alert('请输入用户名和密码')
return
}
login({ username, password })
.then(res => {
setTimeout(() => {
navigate('/')
}, 1000)
}).catch(err => {
alert(err.message)
})
}
return (
<>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username</label>
<input id="username" ref={usernameRef} type="text" required placeholder="请输入用户名" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" ref={passwordRef} type="password" required placeholder="请输入密码" />
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</>
)
}
export default Login
功能解析:
-
提供登录表单
-
处理表单提交:
- 验证输入
- 调用全局store的login方法
- 登录成功后导航到首页
- 处理错误情况
设计考虑:
- 使用非受控组件简化代码
我们使用非受控组件性能会好一点,受控组件可以实时获取到数据,但是性能会差一点
- 集成全局状态管理
- 基本的表单验证
- 用户友好的反馈机制
7.主应用组件App.jsx
import {
useState,
useEffect,
lazy,
Suspense
} from 'react'
import './App.css'
import {
Routes,
Route
} from 'react-router-dom'
import NavBar from './components/NavBar'
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 />} />
<Route path="/pay" element={
<RequireAuth>
<Pay />
</RequireAuth>
} />
</Routes>
</Suspense>
</>
)
}
export default App
8.入口文件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>
)
9.Mock文件login.js
import jwt from "jsonwebtoken";
// 安全性 编码的时候用于加密
// 解码的时候用于解密
// 加盐
const secret = '!@#$%^&*'
// login 模块 mock
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: '用户名或密码错误',
}
}
// JSON 用户数据对象
const token = jwt.sign({
user: {
id: "001",
username: "admin",
}
}, secret, {
expiresIn: '24h',
})
// console.log(token, '///')
// 生成token 颁发令牌
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.decode(token, secret)
console.log(decode)
return {
code: 0,
data: decode.user
}
} catch (err) {
return {
code: 1,
message: 'Invalid token'
}
}
return {
token
}
}
}
]
功能解析
1. 登录功能 (/api/login POST)
-
验证凭证:检查用户名是否为"admin",密码是否为"123456"
-
错误处理:如果验证失败,返回错误码和消息
-
JWT生成:验证成功后,使用
jsonwebtoken库生成JWT令牌- 令牌包含用户信息(id和username)
- 使用预设的
secret进行签名 - 设置过期时间为24小时
-
响应:返回生成的token和用户数据
2. 用户信息获取 (/api/user GET)
-
令牌验证:从请求头的Authorization字段中提取JWT
-
令牌解码:使用相同的
secret验证并解码JWT -
响应:
- 成功时返回用户信息
- 失败时返回错误信息
JWT认证流程详解
结合上述组件,让我们梳理完整的JWT认证流程:
-
用户访问登录页面:
- 输入凭据并提交表单
- 触发
handleSubmit函数
-
登录请求处理:
- 调用
useUserStore的login方法 login方法通过doLoginAPI发送请求
- 调用
-
服务器响应:
- 验证凭据
- 生成JWT(包含用户信息)
- 返回token和用户数据
-
客户端处理响应:
- 存储token到localStorage
- 更新全局状态(
isLogin和user)
-
后续认证请求:
- Axios拦截器自动附加
Authorization头 - 格式:
Bearer <token>
- Axios拦截器自动附加
-
访问受保护路由:
RequireAuth组件检查登录状态- 未登录则重定向到登录页
-
用户登出:
- 清除localStorage中的token
- 重置全局状态
总结
本文详细解析了基于JWT的前端认证系统实现。通过Zustand管理全局状态,Axios拦截器处理认证头,React Router守卫保护路由,我们构建了一个完整的前端认证流程。虽然当前实现满足基本需求,但在生产环境中还需要考虑更多安全因素和用户体验优化。
JWT作为一种现代化的认证方案,其简洁性和灵活性使其成为众多开发者的选择。理解其工作原理和实现细节,有助于我们构建更安全、更可靠的Web应用。