深入浅出JWT登录鉴权:从原理到React实战

172 阅读3分钟

在现代化Web应用中,安全可靠的用户认证系统是必备基础。本文将带你深入理解JWT技术,并手把手实现一个完整的React登录鉴权系统。

一、JWT:现代Web认证的利器

什么是JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全传输信息作为JSON对象。它由三部分组成,用点号分隔:

  1. Header:包含算法和令牌类型
  2. Payload:携带的用户数据(声明)
  3. Signature:验证消息完整性的签名

一个典型的JWT看起来像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT的优势

  • 无状态:服务器不需要存储会话信息
  • 跨域支持:天然支持CORS跨域请求
  • 自包含性:所有必要信息都包含在令牌中
  • 可扩展性:轻松添加自定义声明
  • 安全性:基于数字签名防止篡改

二、实战:React + JWT登录鉴权系统

项目结构概览

src/
├── App.jsx         # 主应用组件
├── authStore.js    # Zustand状态管理
├── api/
│   └── login.js    # 登录API模拟
└── components/
    ├── LoginForm.jsx   # 登录表单组件
    └── Dashboard.jsx   # 登录后仪表盘

1. 状态管理:使用Zustand

// src/authStore.js
import { create } from 'zustand';

const useAuthStore = create((set) => ({
  isLogin: false,
  user: null,
  token: null,
  
  login: (userData, token) => set({
    isLogin: true,
    user: userData,
    token
  }),
  
  logout: () => set({
    isLogin: false,
    user: null,
    token: null
  }),
}));

export default useAuthStore;

2. 增强Mock登录API(返回JWT)

// src/api/login.js
import jwt from 'jsonwebtoken';

const users = [
  { id: 1, username: 'admin', password: 'admin123', role: 'admin' },
  { id: 2, username: 'user', password: 'user123', role: 'user' }
];

const SECRET_KEY = 'your_secret_key_here';

export default [
  {
    url: '/api/login',
    method: 'post',
    timeout: 1000,
    response: ({ body }) => {
      const { username, password } = body;
      const user = users.find(u => 
        u.username === username && u.password === password
      );
      
      if (!user) {
        return { code: 401, message: '用户名或密码错误' };
      }
      
      // 生成JWT(有效期为1小时)
      const token = jwt.sign(
        { userId: user.id, role: user.role },
        SECRET_KEY,
        { expiresIn: '1h' }
      );
      
      return {
        code: 200,
        data: {
          user: { 
            id: user.id, 
            username: user.username, 
            role: user.role 
          },
          token
        }
      };
    }
  }
];

3. 登录表单组件实现

// src/components/LoginForm.jsx
import { useState } from 'react';
import useAuthStore from '../authStore';

const LoginForm = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const login = useAuthStore(state => state.login);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
      });
      
      const result = await response.json();
      
      if (result.code === 200) {
        // 保存到状态管理
        login(result.data.user, result.data.token);
        
        // 存储到localStorage
        localStorage.setItem('authToken', result.data.token);
        localStorage.setItem('user', JSON.stringify(result.data.user));
      } else {
        setError(result.message || '登录失败');
      }
    } catch (err) {
      setError('网络请求失败');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="login-form">
      <h2>用户登录</h2>
      {error && <div className="error">{error}</div>}
      
      <div className="form-group">
        <label>用户名</label>
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          required
        />
      </div>
      
      <div className="form-group">
        <label>密码</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      
      <button type="submit" className="login-btn">登录</button>
    </form>
  );
};

export default LoginForm;

4. 主应用组件整合

// src/App.jsx
import { useEffect } from 'react';
import useAuthStore from './authStore';
import LoginForm from './components/LoginForm';
import Dashboard from './components/Dashboard';

function App() {
  const { isLogin, user, login } = useAuthStore();
  
  // 检查本地存储的登录状态
  useEffect(() => {
    const token = localStorage.getItem('authToken');
    const userData = JSON.parse(localStorage.getItem('user'));
    
    if (token && userData) {
      login(userData, token);
    }
  }, [login]);

  return (
    <div className="app">
      <header>
        <h1>JWT认证系统</h1>
        {isLogin && <div className="user-info">欢迎, {user.username}</div>}
      </header>
      
      <main>
        {isLogin ? <Dashboard /> : <LoginForm />}
      </main>
      
      <footer>
        <p>© 2023 JWT认证演示</p>
      </footer>
    </div>
  );
}

export default App;

5. 请求拦截器(添加JWT)

// 在应用入口文件添加
import axios from 'axios';

axios.interceptors.request.use(config => {
  const token = localStorage.getItem('authToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
}, error => {
  return Promise.reject(error);
});

三、JWT安全最佳实践

  1. 使用HTTPS:防止令牌在传输中被窃取

  2. 设置合理有效期:缩短令牌生命周期

  3. 存储安全

    • 避免在localStorage存储敏感信息
    • 考虑使用HttpOnly Cookie
  4. 令牌刷新机制

    // 刷新令牌示例
    const refreshToken = async () => {
      const response = await fetch('/api/refresh-token', {
        method: 'POST',
        credentials: 'include' // 发送刷新令牌的Cookie
      });
      const { token } = await response.json();
      localStorage.setItem('authToken', token);
      return token;
    };
    
  5. 黑名单处理:对于需要提前失效的令牌,使用黑名单机制

四、JWT vs Session:如何选择?

特性JWTSession
状态管理无状态有状态
扩展性高(天然支持分布式)需要共享存储方案
存储位置客户端服务器
跨域支持容易需要额外配置
安全性依赖签名/加密强度依赖Cookie安全配置
令牌大小较大较小(仅Session ID)

五、总结

JWT作为现代Web认证的标准方案,通过本文我们:

  1. 理解了JWT的结构和工作原理
  2. 实现了React + Zustand + JWT的完整登录鉴权流程
  3. 掌握了JWT的安全实践和最佳方案
  4. 了解了JWT与传统Session的差异