🔐 前端JWT登录鉴权实战:从原理到Zustand状态管理

🔐 前端JWT登录鉴权实战:从原理到Zustand状态管理

现代Web应用中,安全可靠的用户认证系统至关重要。本文将带你深入理解JWT登录鉴权机制,并手把手实现一个完整的React鉴权系统!

🌟 为什么需要JWT登录鉴权?

在传统的Web应用中,服务器通过Session和Cookie来管理用户状态。但在现代前后端分离架构中,JWT(JSON Web Token) 凭借其无状态、跨域友好和易于扩展的特性,成为了主流的认证方案。

JWT vs Cookie/Session

特性 JWT Cookie/Session
状态管理 无状态 有状态
跨域支持 ✅ 优秀 ❌ 受限
移动端支持 ✅ 优秀 ⚠️ 一般
扩展性 ✅ 优秀 ⚠️ 一般
安全性 ✅ 优秀 ❌ 不太安全,信息较为透明

JWT 使用流程

1.引入依赖

2. 用户登录,验证身份

当用户输入用户名和密码后,前端将这些信息发送到后端 /api/login 接口。

示例代码(mock 登录接口):
js 复制代码
import jwt from 'jsonwebtoken'

const secret = 'ababab' // 加密密钥

export default [
  {
    url: '/api/login',
    method: 'post',
    timeout: 2000,
    response: (req, res) => {
      const { username, password } = req.body

      // 验证用户名和密码
      if (username !== 'a' || password !== '1') {
        return {
          code: 1,
          message: '用户名或密码出错了'
        }
      }

      // 生成 JWT token
      const token = jwt.sign({
        user: {
          id: '001',
          username: 'a'
        }
      }, secret, {
        expiresIn: 86400 // token 有效期为 1 天
      })

      return {
        token,
        username,
        password
      }
    }
  }
]

login 模块的 mock,模拟发送请求,生成token,进行鉴权判断

如果输入错误的用户名和密码会报错

输入正确的用户名和密码将会得到Token

3. 后端生成 JWT Token

使用 jsonwebtoken 库的 sign 方法生成 Token,结构如下:

js 复制代码
jwt.sign(payload, secretOrPrivateKey, options)
  • payload:需要编码的数据,如用户信息。
  • secretOrPrivateKey:加密密钥或私钥。
  • options:可选配置项,如过期时间。
js 复制代码
const token = jwt.sign({
        user: {
          id: '001',
          username: 'a'
        }
      }, secret, {
        expiresIn: 86400 // token 有效期为 1 天
      })

这里涉及到的 secretexpiresIn 参数的具体作用如下:

secret
  • 定义secret 是用于对 JWT 进行签名的密钥或私钥。它可以是一个字符串或者一个缓冲区。

  • 作用

    • 签名 :当你创建一个 JWT 时,jsonwebtoken 使用提供的 secret 对 payload 进行签名。这意味着任何人即使能够查看到 JWT 的内容(因为 JWT 是可以被解码的),如果没有正确的 secret,也无法伪造一个有效的 JWT。
    • 验证 :当接收到一个 JWT 请求时,服务器端需要使用相同的 secret 来验证该 JWT 的真实性。如果签名不匹配,则表示该令牌可能已被篡改,不应被信任。
  • 安全性 :确保 secret 不会被泄露给未经授权的用户。它应该保存在一个安全的地方,并且不应该硬编码在前端应用中。

expiresIn
  • 定义expiresIn 是一个选项,用来指定生成的 JWT 的过期时间。它可以是秒数,也可以是指定时间长度的字符串(如 "2 days""10h""7d" 等)。

  • 作用

    • 设置过期时间 :在这个例子中,expiresIn: 86400 表示该 JWT 将在创建后的一天(即 86400 秒)后过期。一旦 JWT 过期,服务器将不再接受它作为有效的认证凭证,除非重新生成一个新的 JWT。
    • 增强安全性:通过设置合理的过期时间,可以减少令牌被盗用的风险。较短的有效期意味着攻击者即使获取了令牌,也仅有有限的时间窗口可以利用它进行未授权访问。

综上所述,secret 主要用于保证 JWT 的完整性和真实性,防止令牌被伪造;而 expiresIn 则用于控制 JWT 的生命周期,确保其不会无限期地保持有效状态,从而提高系统的安全性。这两者的结合使用有助于构建一个既安全又灵活的身份验证机制。


4. 前端接收 Token 并存储

登录成功后,前端从响应中获取 Token,并存储到本地(如 localStorage):

js 复制代码
// 假设你使用 axios 请求登录
const res = await axios.post('/api/login', { username, password })
localStorage.setItem('token', res.data.token)

5. 每次请求携带 Token

前端在每次请求时,将 Token 放入请求头中,通常格式为:

makefile 复制代码
Authorization: Bearer <your_token_here>
示例:配置 axios 拦截器自动添加 Token
js 复制代码
// 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) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器:处理响应
axios.interceptors.response.use((res) => {
  return res
})

export default axios

5. 后端验证 Token

在受保护的接口中,后端需要验证 Token 的合法性。

示例:验证 Token 的接口 /api/user
js 复制代码
{
  url: '/api/user',
  method: 'get',
  response: (req, res) => {
    const token = req.headers.authorization?.split(' ')[1] // 提取 token

    if (!token) {
      return {
        code: 401,
        message: '未提供 token'
      }
    }

    try {
      const decoded = jwt.verify(token, secret) // 验证并解码 token
      return {
        user: decoded.user,
        message: '成功获取用户信息'
      }
    } catch (error) {
      return {
        code: 401,
        message: '无效或过期的 token'
      }
    }
  }
}
解释const token = req.headers.authorization?.split(' ')[1]
  1. 获取 Authorization :首先从请求对象 reqheaders 属性中获取名为 authorization 的头值。注意这里的属性名是小写的,因为 HTTP 头名称在 Node.js 中会被转换为小写。

  2. 安全访问(可选链操作符 ?. :使用可选链操作符 ?. 来确保如果 authorization 头不存在时不会抛出错误,而是返回 undefined

  3. 分割字符串 :使用 .split(' ') 方法根据空格分割字符串。因为我们知道 Authorization 头的格式是 Bearer <token>,所以通过这种方式可以轻松地将前缀 Bearer 和实际的令牌分开。

    • split(' ') 返回一个数组,其中第一个元素是 Bearer,第二个元素是实际的令牌。
    • [1] 表示我们只关心这个数组中的第二个元素,也就是真正的 JWT 令牌。
  4. 什么是Bearer: 意思是持有者,表示只要是持有这个Token的人,就被认为是经过授权的


6. Token 验证失败处理

如果 Token 无效或过期,后端应返回 401(Unauthorized)错误,前端可以据此跳转到登录页或刷新 Token。

JWT 的结构说明

JWT 由三部分组成:

  1. Header(头部) :指定加密算法(如 HS256)和 Token 类型(如 JWT)。
  2. Payload(载荷):包含用户信息、过期时间等数据。
  3. Signature(签名):使用密钥对前两部分签名,确保 Token 未被篡改。

例如:

erlang 复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjp7ImlkIjoiMDAxIiwidXNlcm5hbWUiOiJhIn0sImlhdCI6MTc1MzIzOTUxNSwiZXhwIjoxNzUzMzI1OTE1fQ.
Pm5yPXko6NarIyhkO-QRDrwwX_WQ2vHEG6JPyBxA19M

特性 JWT Cookie
存储位置 客户端(localStorage / sessionStorage) 浏览器自动管理(可跨域)
安全性 更安全(需签名验证) 易受 XSS / CSRF 攻击
状态管理 无状态(适合分布式系统) 有状态(需服务端维护 session)
跨域支持 更好(无需同源限制) 依赖同源策略

✅ JWT 使用流程图

markdown 复制代码
前端请求登录
     ↓
后端验证账号密码
     ↓
生成 JWT Token
     ↓
返回 Token 给前端
     ↓
前端存储 Token
     ↓
后续请求携带 Token
     ↓
后端验证 Token 合法性
     ↓
返回受保护资源

🛠 实战:构建React JWT鉴权系统

项目结构概览

bash 复制代码
jwt-demo
├── mock
│   └── login.js    # 模拟登录API
├── src
│   ├── api
│   │   ├── config.js  # Axios配置
│   │   └── user.js    # 用户API
│   ├── components
│   │   ├── NavBar     # 导航栏
│   │   └── RequiredAuth # 路由守卫
│   ├── store
│   │   └── user.js    # Zustand状态管理
│   └── views
│       ├── Home       # 首页
│       ├── Login      # 登录页
│       └── Pay        # 支付页(需鉴权)

第一步:配置Axios拦截器

src/api/config.js - 全局请求配置:

jsx 复制代码
import axios from 'axios';

axios.defaults.baseURL = 'http://localhost:5173/api';

// 请求拦截器 - 自动添加Token
axios.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器 - 统一处理错误
axios.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response.status === 401) {
      // Token过期处理
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default axios;

第二步:实现Zustand状态管理

src/store/user.js - 用户状态管理:

jsx 复制代码
import { create } from 'zustand';
import { doLogin, getUser } from '../api/user';

export const useUserStore = create((set) => ({
  user: null,
  isLogin: false,
  
  // 登录动作
  login: async ({ username, password }) => {
    try {
      const res = await doLogin({ username, password });
      const { token, data: user } = res.data;
      
      localStorage.setItem('token', token);
      set({ isLogin: true, user });
      
      return true;
    } catch (error) {
      console.error('登录失败:', error);
      return false;
    }
  },
  
  // 登出动作
  logout: () => {
    localStorage.removeItem('token');
    set({ isLogin: false, user: null });
  },
  
  // 检查登录状态
  checkLogin: async () => {
    try {
      const res = await getUser();
      set({ isLogin: true, user: res.data.data });
    } catch {
      set({ isLogin: false, user: null });
    }
  }
}));

第三步:创建路由守卫组件

src/components/RequiredAuth/index.jsx - 保护需要登录的页面: 当我想要进入pay页面时,如果未登入则跳转到login页面

jsx 复制代码
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUserStore } from '../../store/user';

const RequiredAuth = ({ children }) => {
  const { isLogin, checkLogin } = useUserStore();
  const navigate = useNavigate();
  const { pathname } = useLocation();

  useEffect(() => {
    // 检查登录状态
    checkLogin().then(() => {
      if (!isLogin) {
        navigate('/login', { state: { from: pathname } });
      }
    });
  }, [isLogin, checkLogin, navigate, pathname]);

  return isLogin ? children : <div>验证中...</div>;
};

export default RequiredAuth;

第四步:实现登录页面

src/views/Login/index.jsx - 用户登录界面: 这里使用非受控组件完成表单,注意使用时要阻止表单的默认提交,具体内容可以参考🌟 React表单秘籍:受控组件 vs 非受控组件全面解析🌟 React表单秘籍:受控组件 vs 非受控组件全面解析 - 掘金

jsx 复制代码
import { useRef } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUserStore } from '../../store/user';

const Login = () => {
  const usernameRef = useRef();
  const passwordRef = useRef();
  const { login } = useUserStore();
  const navigate = useNavigate();
  const location = useLocation();
  
  const from = location.state?.from || '/';

  const handleLogin = async (e) => {
    e.preventDefault();
    const username = usernameRef.current.value;
    const password = passwordRef.current.value;
    
    if (!username || !password) {
      alert('请输入用户名和密码');
      return;
    }

    const success = await login({ username, password });
    if (success) {
      navigate(from); // 重定向到之前访问的页面
    }
  };

  return (
    <div className="login-container">
      <h2>用户登录</h2>
      <form onSubmit={handleLogin}>
        <div className="form-group">
          <label htmlFor="username">用户名</label>
          <input 
            type="text" 
            id="username" 
            ref={usernameRef} 
            placeholder="请输入用户名"
            defaultValue="a" // 演示用
          />
        </div>
        <div className="form-group">
          <label htmlFor="password">密码</label>
          <input 
            type="password" 
            id="password" 
            ref={passwordRef} 
            placeholder="请输入密码"
            defaultValue="1" // 演示用
          />
        </div>
        <button type="submit" className="login-btn">登录</button>
      </form>
    </div>
  );
};

export default Login;

第五步:配置Mock服务

mock/login.js - 模拟登录API和用户信息:

js 复制代码
import jwt from 'jsonwebtoken';

const secret = 'ababab'; // 加密密钥

export default [
  {
    url: '/api/login',
    method: 'post',
    timeout: 1000,
    response: ({ body }) => {
      const { username, password } = body;
      
      // 模拟登录验证
      if (username !== 'a' || password !== '1') {
        return {
          code: 1,
          message: '用户名或密码错误'
        };
      }

      // 生成JWT令牌
      const token = jwt.sign(
        {
          user: {
            id: '001',
            username: 'a'
          }
        },
        secret,
        { expiresIn: '1d' } // 1天有效期
      );

      return {
        code: 0,
        token,
        data: {
          id: '001',
          username: 'a'
        }
      };
    }
  },
  {
    url: '/api/user',
    method: 'get',
    response: ({ headers }) => {
      const authHeader = headers.authorization;
      
      if (!authHeader) {
        return {
          code: 401,
          message: '未提供认证令牌'
        };
      }

      const token = authHeader.split(' ')[1];
      
      try {
        // 验证并解析JWT
        const decoded = jwt.verify(token, secret);
        return {
          code: 0,
          data: decoded.user
        };
      } catch (err) {
        return {
          code: 401,
          message: '无效的令牌'
        };
      }
    }
  }
];

第六步:应用路由配置

src/App.jsx - 主应用路由:

jsx 复制代码
import { lazy, Suspense, useEffect } from 'react';
import { Routes, Route, useNavigate } from 'react-router-dom';
import NavBar from './components/NavBar';
import { useUserStore } from './store/user';

const Home = lazy(() => import('./views/Home'));
const Login = lazy(() => import('./views/Login'));
const Pay = lazy(() => import('./views/Pay'));
const RequiredAuth = lazy(() => import('./components/RequiredAuth'));

function App() {
  const { checkLogin } = useUserStore();
  const navigate = useNavigate();

  useEffect(() => {
    // 应用启动时检查登录状态
    checkLogin();
  }, [checkLogin]);

  return (
    <div className="app">
      <NavBar />
      <Suspense fallback={<div className="loading">加载中...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route 
            path="/pay" 
            element={
              <RequiredAuth>
                <Pay />
              </RequiredAuth>
            } 
          />
        </Routes>
      </Suspense>
    </div>
  );
}

export default App;

🚀 核心功能亮点

1. 自动Token刷新机制

jsx 复制代码
// 在axios响应拦截器中添加Token刷新逻辑
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      // 尝试刷新Token
      const refreshToken = localStorage.getItem('refreshToken');
      if (refreshToken) {
        try {
          const { data } = await axios.post('/api/refresh', { refreshToken });
          localStorage.setItem('token', data.token);
          
          // 重试原始请求
          originalRequest.headers.Authorization = `Bearer ${data.token}`;
          return axios(originalRequest);
        } catch (refreshError) {
          // 刷新失败,跳转登录
          useUserStore.getState().logout();
          window.location.href = '/login';
        }
      }
    }
    
    return Promise.reject(error);
  }
);

2. 细粒度权限控制

jsx 复制代码
// 扩展路由守卫组件
const RoleBasedAuth = ({ children, requiredRole }) => {
  const { user } = useUserStore();
  
  if (!user) {
    return <Navigate to="/login" />;
  }
  
  if (user.role !== requiredRole) {
    return (
      <div className="unauthorized">
        <h2>权限不足</h2>
        <p>您没有访问此页面的权限</p>
      </div>
    );
  }
  
  return children;
};

// 使用示例
<Route 
  path="/admin" 
  element={
    <RoleBasedAuth requiredRole="admin">
      <AdminDashboard />
    </RoleBasedAuth>
  } 
/>

🧪 测试你的JWT系统

  1. 未登录访问受保护页面

    • 尝试访问/pay,将被重定向到登录页
  2. 登录流程

    • 使用用户名a和密码1登录
    • 成功后将重定向到之前尝试访问的页面
  3. Token验证

    • 查看请求头中的Authorization字段
    • 服务器端验证Token有效性

💡 最佳实践与安全建议

  1. Token存储安全

    • 使用HttpOnly Cookie存储Token(防XSS)
    • 敏感操作要求重新认证
  2. 短期有效令牌

    js 复制代码
    // 生成15分钟有效的访问令牌
    jwt.sign(payload, secret, { expiresIn: '15m' });
    
    // 生成7天有效的刷新令牌
    jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
  3. 密钥管理

    • 使用环境变量存储密钥
    • 定期轮换密钥
    • 不同环境使用不同密钥
  4. 增强JWT安全性

    js 复制代码
    // 在Payload中添加客户端指纹
    const fingerprint = crypto
      .createHash('sha256')
      .update(req.headers['user-agent'])
      .digest('hex');
    
    jwt.sign({
      user: { id: '001' },
      fingerprint
    }, secret);

🎯 总结

通过本文,我们系统性地实现了基于JWT的前端鉴权系统,核心要点包括:

  1. JWT工作机制 - Header.Payload.Signature的三段式结构
  2. 状态管理 - 使用Zustand管理用户状态
  3. 路由守卫 - 保护需要认证的页面
  4. Axios拦截器 - 自动化Token管理
  5. Mock服务 - 模拟后端API

前端安全无小事,JWT只是安全链条中的一环。完整的认证系统还需要HTTPS、CSRF防护、速率限制等多重保障措施。希望本文能为你构建安全可靠的Web应用提供坚实基础!

项目完整代码 :[GitHub仓库链接](lh_ai/react/jwt-demo at main · lhlhlhlhl/lh_ai)(示例链接)

相关推荐
米思特儿林12 小时前
NuxtImage 配置上传目录配置
前端
JohnYan12 小时前
Bun技术评估 - 22 Stream
javascript·后端·bun
Mr_chiu12 小时前
AI加持的交互革新:手把手教你用Vue3打造智能模板输入框
前端
精神状态良好12 小时前
告别聊天式编程:引入 OpenSpec,构建结构化的 AI 开发工作流
前端
Aevget12 小时前
界面控件DevExpress JS & ASP.NET Core v25.1 - 全新的Stepper组件
javascript·asp.net·界面控件·devexpress·ui开发
WangHappy12 小时前
出海不愁!用Vue3 + Node.js + Stripe实现全球支付
前端·node.js
林希_Rachel_傻希希13 小时前
手写Promise最终版本
前端·javascript·面试
visnix13 小时前
AI大模型-LLM原理剖析到训练微调实战(第二部分:大模型核心原理与Transformer架构)
前端·llm
老妪力虽衰13 小时前
零基础的小白也能通过AI搭建自己的网页应用
前端
该用户已不存在13 小时前
Node.js后端开发必不可少的7个核心库
javascript·后端·node.js