我写个登录怎么就那么难?React + JWT 也太绕了吧!

72 阅读6分钟

我写个登录怎么就那么难?React + JWT 也太绕了吧!

在现代前端应用开发中,用户认证是核心功能之一,而 JWT(JSON Web Token)凭借其无状态、安全性高等特点,成为主流的身份验证方案。本文将详细介绍如何使用 React 作为 UI 库,结合 Zustand 进行状态管理、Axios 处理网络请求、Mock 模拟后端接口,实现一套完整的 JWT 表单登录校验功能。

一、技术栈简介

在开始实现前,先了解一下本次使用的核心技术:

  • Zustand:轻量级的状态管理库,相比 Redux 更简洁,API 设计直观,适合中小型应用。

  • Axios:基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js,支持请求拦截、响应拦截等功能。

  • **Mock **:用于在浏览器中模拟后端 API,避免依赖真实服务器即可完成开发。

  • JWT:一种紧凑的、URL 安全的令牌(token)格式,用于在各方之间传递声明,通常用于身份验证和信息交换。

    用户登录后,服务器验证身份并生成一个包含用户信息的 JSON Web Token。Token 通常由三部分组成:Header(头部)、Payload(载荷)、Signature(签名)。服务器每次收到请求后,解析 请求携带的Token 并验证其签名合法性,确认用户身份。

二、项目初始化与依赖安装

首先创建 React 项目并安装所需依赖:

# 初始化React项目,选择react 与js
npm init vite

# 安装路由依赖
npm i react-router-dom 
# 安装axios
npm i axios 
# 安装jwt 库
npm i jsonwebtoken
# 安装zustand 状态管理库
npm i zustand
# 安装 mock 插件
pnpm i vite-plugin-mock -D 开发阶段

三、实现步骤

使用mock模拟后端

vite.config.js中配置后端mock

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { viteMockServe } from 'vite-plugin-mock'

// https://vite.dev/config/
export default defineConfig({
   // 添加mock插件 
  plugins: [react(), viteMockServe({
    mockPath: 'mock',
    localEnabled: true,
  })],
  
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

在项目目录下创建mock目录,在该目录下创建login.js文件,文件内容如下。

登录接口的执行流程:首先从请求体中获取 uername 与 passward ,根据用户名来查询数据库中的用户信息,之后进行用户信息校验,验证用户名和密码,正确则生成 JWT 令牌(包含用户 ID、用户名、角色等信息)并返回。

import jwt from "jsonwebtoken";

// 模拟数据库
// 用户表
const users = [
  {
    userId: 1,
    username: "admin",
    passward: "123456",
  },
];

// 爱好表
const likes = [
  {
    userId: 1,
    hobbyList: [
      { id: 1, name: "阅读" },
      { id: 2, name: "游泳" },
      { id: 3, name: "跑步" },
    ],
  },d
  {
    userId: 2,
    hobbyList: [
      { id: 4, name: "绘画" },
      { id: 5, name: "音乐" },
    ],
  },
];

// 定义加密的密钥 加盐
const secret = "86_486_0105";

// 创建登录mock
export default [
    // 登录接口
  {
    url: "/login",
    method: "POST",
    response: (req) => {
      const { username, password } = req.body;
      console.log("登录参数", username, password);

      const user = users.find((item) => item.username === username);
      if (user && user.passward === password) {
        // 使用算法生成token
        const token = jwt.sign(user, secret, {
          expiresIn: "1h",
        });
        return {
          code: 0,
          msg: "登录成功",
          // 生成一个随机的token
          data: {
            user: {
              userId: user.userId,
              username: user.username,
            },
            token,
          },
        };
      }

      return {
        code: 1,
        msg: "用户名或密码错误",
      };
    },
  },
  {
    url: "/getLikeList",
    method: "GET",
    response: (req) => {
      const token = req.headers["authorization"].split(" ")[1];
      try {
        // 解析token 获取用户对象
        const payload = jwt.verify(token, secret);

        //  用户是否存在
        const user = findUser(payload.userId);

        if (!user) throw new Error("token错误");
        // 是否过期
        if (payload.exp < Date.now() / 1000) throw new Error("token过期");

        // 查询用户爱好
        const likeList = likes.find((item) => item.userId === payload.userId);

        // 成功
        return {
          code: 0,
          msg: "获取成功",
          data: likeList,
        };
      } catch (error) {
        return {
          code: 1,
          msg: "token错误",
        };
      }
    },
  },
];

// 查找数据库
const findUser = (userId) => {
  return users.find((item) => item.userId === userId);
};

使用Apifox 对mock模拟的后端进行测试

image-20250723214951123.png

响应结果如下

{
    "code": 0,
    "msg": "登录成功",
    "data": {
        "user": {
            "userId": 1,
            "username": "admin"
        },
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd2FyZCI6IjEyMzQ1NiIsImlhdCI6MTc1MzI1OTEzMCwiZXhwIjoxNzUzMjYyNzMwfQ.T_TpJExql8qdUgminhyzGk67W2ytD_l0C-93BJcVvL8"
    }
}

前端

  1. 封装 Axios 请求与 JWT 拦截器

    编写 axios 配置和拦截器,这个拦截器的作用是在每个请求的请求头中中添加Authorization: Bearer ,实现令牌的自动携带。

    import axios from "axios";
    
    // 配置基地址
    // 拦截请求,添加token
    const instance = axios.create({
      baseURL: "http://localhost:5173",
      timeout: 1000,
      headers: { "X-Custom-Header": "foobar" },
    });
    
    // 拦截请求,添加token   config 是请求配置对象
    instance.interceptors.request.use((config) => {
      const token = localStorage.getItem("token");
      if (token) {
        config.headers["authorization"] = `Bearer ${token}`;
      }
      return config;
    });
    
    
    // 导出实例
    export default instance;
    
    

    编写api请求

    import instance  from "./config";
    
    export const login = async (data) => {
        // 登录 设置请求体
        return await instance.post('/login', data)
    }
    
    export const getLikeList = async () => {
        const data = await instance.get('/getLikeList')
        return data
    }
    
    
  2. 使用 Zustand 管理登录状态

    用 Zustand 创建存储用户状态(登录状态、用户信息、令牌)的 store

    • 状态:包含user(用户信息)、token(JWT 令牌)、isLogin (是否登录)、loading(加载状态)、error(错误信息)。

    • 核心方法:login(调用登录 API 并存储令牌)、logout(清除令牌和用户信息)。

    • 令牌存储:使用 localStorage 存储 JWT

    import { create } from "zustand";
    import { login } from "@/api/user";
    
    const tokenKey = "token";
    
    const useUserStore = create((set) => ({
      user: null, // {userId,username}
      isLogin: false,
      token: "",
      loading: false,
      error: null,
    
      setError: (error) => {
        set({ error });
      },
    
      login: async (data) => {
        set({ loading: true });
        const res = await login(data);
        // 结构响应的数据
        const { code, msg, data: userData } = res.data;
    
        const { token, user } = userData;
        // 若登录成功则 设置登录成功信息,并存储token到localStorage中
        if (code === 0) {
          set({
            user: user,
            isLogin: true,
            token: token,
            loading: false,
          });
          // 存储token
          localStorage.setItem(tokenKey, token);
        } else {
          set({
            loading: false,
            error: msg,
          });
        }
      },
    
      logout: () => {
        set({
          user: null,
          isLogin: false,
          token: "",
        });
        // 清除本地存储
        localStorage.removeItem(tokenKey);
      },
    }));
    
    export { useUserStore };
    
    
  3. 创建登录表单组件以及相关路由

    创建路由

    import { useEffect, lazy, Suspense } from "react";
    import { Routes, Route } from "react-router-dom";
    import "./App.css";
    
    const Login = lazy(() => import("@/pages/Login"));
    const Home = lazy(() => import("@/pages/Home"));
    const Loading = lazy(() => import("@/components/Loading"));
    
    function App() {
      return (
        <>
          <Suspense fallback={<Loading />}>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/login" element={<Login />} />
            </Routes>
          </Suspense>
        </>
      );
    }
    
    export default App;
    

    Login 登录组件,包含用户名、密码输入框和登录按钮,

    import { useState } from "react";
    import Style from "./index.module.css";
    import { useNavigate } from "react-router-dom";
    import { useUserStore } from "@/store/userStore";
    
    export default function Login() {
      const { login, error, loading, isLogin, setError } = useUserStore();
    
      // 表单数据
      const [formData, setFormData] = useState({
        username: "",
        password: "",
      });
    
      const navigate = useNavigate();
    
      const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData((prev) => ({ ...prev, [name]: value }));
      };
    
      const handleSubmit = async (e) => {
        e.preventDefault();
        const { username, password } = formData;
    
        // 简单验证
        if (!username || !password) {
          setError("请输入用户名和密码");
          return;
        }
        await login({ username, password });
        console.log("---------------");
        console.log(isLogin);
        if (isLogin) {
          console.log("nav");
          navigate("/");
        }
      };
    
      return (
        <div className={Style.loginContainer}>
          <div className={Style.loginBox}>
            <h1 className={Style.title}>用户登录</h1>
            <form onSubmit={handleSubmit} className={Style.form}>
              {error && <div className={Style.error}>{error}</div>}
    
              <div className={Style.inputGroup}>
                <label htmlFor="username" className={Style.label}>
                  用户名
                </label>
                <input
                  type="text"
                  id="username"
                  name="username"
                  value={formData.username}
                  onChange={handleChange}
                  className={Style.input}
                  placeholder="请输入用户名"
                  disabled={loading}
                />
              </div>
    
              <div className={Style.inputGroup}>
                <label htmlFor="password" className={Style.label}>
                  密码
                </label>
                <input
                  type="password"
                  id="password"
                  name="password"
                  value={formData.password}
                  onChange={handleChange}
                  className={Style.input}
                  placeholder="请输入密码"
                  disabled={loading}
                />
              </div>
    
              <button type="submit" className={Style.btn} disabled={loading}>
                {loading ? "登录中..." : "登录"}
              </button>
            </form>
          </div>
        </div>
      );
    }
    
    

    Home组件

    
    import { useState, useEffect } from 'react';
    import { useNavigate } from 'react-router-dom';
    import Style from './index.module.css';
    import Loading from '@/components/Loading';
    import NavBar from '@/components/NavBar';
    
    export default function Home() {
      const [userInfo, setUserInfo] = useState(null);
      const [hobbyList, setHobbyList] = useState([]);
      const [loading, setLoading] = useState(true);
    
      const navigate = useNavigate();
    
      // 获取用户信息和爱好列表
      const fetchUserInfo = async () => {
        try {
          const res = await request.get('/getLikeList');
          if (res.code === 0 && res.data) {
            setHobbyList(res.data.hobbyList || []);
            // 从token中解析用户信息(实际项目中应从后端获取)
            const token = localStorage.getItem('token');
            if (token) {
              const payload = JSON.parse(atob(token.split('.')[1]));
              setUserInfo(payload);
            }
          }
        } catch (error) {
          console.error('获取用户信息失败:', error);
          // token无效或过期,重定向到登录页
          navigate('/login');
        } finally {
          setLoading(false);
        }
      };
    
      // 组件挂载时检查登录状态
      useEffect(() => {
        const token = localStorage.getItem('token');
        if (!token) {
          navigate('/login');
          return;
        }
        fetchUserInfo();
      }, [navigate]);
    
      if (loading) {
        return (
         <Loading/>
        );
      }
    
      return (
        <div className={Style.container}>
          {/* 导航栏 */}
          <header className={Style.header}>
            <NavBar/>
          </header>
    
          {/* 主内容区 */}
          <main className={Style.main}>
            <h1 className={Style.title}>欢迎回来,{userInfo?.username}!</h1>
    
            <section className={Style.card}>
              <h2 className={Style.sectionTitle}>你的爱好</h2>
              {hobbyList.length > 0 ? (
                <ul className={Style.hobbyList}>
                  {hobbyList.map(hobby => (
                    <li key={hobby.id} className={Style.hobbyItem}>
                      {hobby.name}
                    </li>
                  ))}
                </ul>
              ) : (
                <p className={Style.emptyText}>暂无爱好数据</p>
              )}
            </section>
          </main>
    
          {/* 页脚 */}
          <footer className={Style.footer}>
            <p>© 2025 JWT Demo 项目</p>
          </footer>
        </div>
      );
    }
    

效果展示

jwt.gif