React + JWT 登录鉴权实战,让你的应用"认得"用户

219 阅读5分钟

今天来跟大家分享一个完整的 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>&nbsp;&nbsp;
            <Link to="/pay">Pay</Link>&nbsp;&nbsp;
            {
                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 的工作流程:

  1. 用户登录:前端发送用户名密码到后端
  2. 生成 Token:后端验证用户信息,使用 jsonwebtoken 库生成 token
  3. 返回 Token:后端将 token 返回给前端
  4. 存储 Token:前端将 token 存储在 localStorage 中
  5. 携带 Token:后续请求在 Authorization 头部携带 Bearer ${token}
  6. 验证 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);

项目展示

image.png
image.png
image.png
image.png

项目亮点总结

  1. 现代化技术栈:Vite + React + Zustand + React Router
  2. Mock 数据支持:开发阶段无需后端支持
  3. 自动 Token 管理:axios 拦截器自动处理
  4. 路由守卫机制:保护敏感页面
  5. 懒加载优化:提升应用性能
  6. 状态管理简洁:Zustand 比 Redux 轻量级

结语

这套 JWT 鉴权方案已经能够满足大部分项目的需求了。代码简洁明了,架构清晰,扩展性良好。