React前端实战:JWT登录鉴权全攻略 🚀

145 阅读4分钟

大家好!我是你们的老朋友FogLetter,今天给大家带来一篇React项目中实现JWT登录鉴权的完整指南。我会用最接地气的方式,带大家从零开始实现一个安全的登录系统。

一、JWT是什么?为什么选择它? 🤔

1.1 传统Session的痛点

记得最开始时,项目用的都是传统的Session-Cookie方案。每次用户登录,服务器创建一个Session,返回一个Session ID存到Cookie里。这种方式有什么问题呢?

  • 服务器需要存储Session:用户量一大,内存压力就上来了
  • 跨域问题:现在前后端分离是常态,Cookie跨域很麻烦
  • 扩展性差:集群部署时,Session共享是个头疼事

1.2 JWT的优雅解决方案

JWT(JSON Web Token)就像是一张"数字身份证",它把用户信息直接编码到Token里,服务器不需要存储任何东西。它的结构分为三部分:

Header.Payload.Signature

举个栗子🌰:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

优点

  • 无状态:服务器不需要存储
  • 跨域友好:放在Header里就行
  • 自包含:用户信息就在Token里

二、项目实战:React + JWT完整实现 🔨

2.1 项目结构概览

先看看我们的React项目关键结构:

/src
  /api
    user.js    # 用户相关API
    axios.js   # Axios配置
  /store
    user.js    # Zustand状态管理
  /components
    RequireAuth.js # 路由守卫
  /pages
    Login.js   # 登录页
    ...其他页面

2.2 Axios配置:请求拦截与代理

首先配置我们的HTTP客户端,这是与后端通信的桥梁:

// src/api/axios.js
import axios from 'axios'

// 1. 设置基础URL
axios.defaults.baseURL = 'http://localhost:5173/api'

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

// 3. 响应拦截器 - 简化响应数据
axios.interceptors.response.use((data) => {
    return data.data
})

export default axios

关键点解析

  • Bearer是JWT的标准前缀
  • 响应拦截器直接返回data.data,这样业务代码就不用每次都写.data
  • 生产环境记得处理错误拦截(这里简化了)

2.3 Zustand状态管理

我们用Zustand来管理用户状态,比Redux简洁多了:

// src/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=''}) => {
        try {
            const response = await doLogin({username, password})
            const {token, data: user} = response
            
            if (!token || !user) {
                throw new Error('登录失败')
            }
            
            localStorage.setItem('token', token)
            set({ user, isLogin: true })
            return true
        } catch (error) {
            console.error('登录失败:', error)
            set({ user: null, isLogin: false })
            throw error
        }
    },
    
    // 登出方法
    logout: () => {
        localStorage.removeItem('token')
        set({ user: null, isLogin: false })
    },
}))

设计思路

  • 登录成功后将token存到localStorage
  • 状态变化自动更新UI
  • 错误处理完善,避免页面崩溃

2.4 路由守卫:RequireAuth组件

这是保护我们路由安全的关键:

// src/components/RequireAuth.js
import { useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'

const RequireAuth = ({ children }) => {
    const navigate = useNavigate()
    const { pathname } = useLocation()
    
    useEffect(() => {
        const token = localStorage.getItem('token')
        if (!token) {
            // 记录来源页面,登录后可以跳转回来
            navigate('/login', { state: { from: pathname } })
        }
    }, [])
    
    return <>{children}</>
}

export default RequireAuth

使用方式

<Route element={<RequireAuth><Layout /></RequireAuth>}>
    <Route path="/protected" element={<ProtectedPage />} />
</Route>

进阶优化

  • 可以检查token是否过期(需要解码JWT)
  • 可以添加权限校验逻辑

2.5 登录页面实现

一个简单的登录表单示例:

// src/pages/Login.js
import { useState } from 'react'
import { useUserStore } from '@/store/user'
import { useLocation, useNavigate } from 'react-router-dom'

export default function Login() {
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const login = useUserStore(state => state.login)
    const navigate = useNavigate()
    const location = useLocation()
    
    const from = location.state?.from || '/home'
    
    const handleSubmit = async (e) => {
        e.preventDefault()
        try {
            await login({ username, password })
            navigate(from, { replace: true })
        } catch (error) {
            alert('登录失败: ' + error.message)
        }
    }
    
    return (
        <div className="login-container">
            <h2>欢迎登录</h2>
            <form onSubmit={handleSubmit}>
                <input 
                    value={username}
                    onChange={(e) => setUsername(e.target.value)}
                    placeholder="用户名"
                />
                <input
                    type="password"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    placeholder="密码"
                />
                <button type="submit">登录</button>
            </form>
        </div>
    )
}

用户体验优化点

  • 记住来源页面,登录后自动跳转
  • 错误提示友好
  • 加载状态处理(可以添加loading动画)

三、JWT安全最佳实践 🔒

3.1 前端安全注意事项

  1. 不要将敏感信息存入JWT:JWT可以被解码(只是Base64编码)
  2. 使用HTTPS:防止Token被中间人攻击
  3. 合理设置Token过期时间:通常1-2小时
  4. 实现Token自动刷新:快过期时用refresh token获取新token

3.2 处理Token过期

改进我们的RequireAuth组件:

// 新增jwt解码函数
const decodeJWT = (token) => {
    try {
        return JSON.parse(atob(token.split('.')[1]))
    } catch {
        return null
    }
}

const RequireAuth = ({ children }) => {
    // ...原有代码
    
    useEffect(() => {
        const token = localStorage.getItem('token')
        if (!token) {
            navigate('/login', { state: { from: pathname } })
            return
        }
        
        // 检查是否过期
        const payload = decodeJWT(token)
        if (payload && payload.exp * 1000 < Date.now()) {
            localStorage.removeItem('token')
            navigate('/login', { state: { from: pathname } })
        }
    }, [])
    
    // ...原有代码
}

四、常见问题与解决方案 💡

4.1 跨域问题

如果你的前后端分离部署,可能会遇到:

Access-Control-Allow-Origin 错误

解决方案

  • 后端设置CORS头部
  • 开发环境配置代理(vite示例):
// vite.config.js
export default defineConfig({
    server: {
        proxy: {
            '/api': {
                target: 'http://your-backend.com',
                changeOrigin: true,
                rewrite: path => path.replace(/^\/api/, '')
            }
        }
    }
})

4.2 Token失效处理

当后端返回401时,我们可以统一处理:

// 在axios响应拦截器中添加
axios.interceptors.response.use(
    response => response.data,
    error => {
        if (error.response?.status === 401) {
            localStorage.removeItem('token')
            window.location.href = '/login'
        }
        return Promise.reject(error)
    }
)

五、总结与扩展 🎯

通过这篇文章,我们完整实现了:

  1. Axios的请求拦截和JWT自动添加
  2. Zustand状态管理登录状态
  3. 路由守卫保护敏感页面
  4. 完整的登录登出流程

扩展思考

  • 如何实现"记住我"功能?(长过期时间的token)
  • 如何做权限控制?(在JWT payload中添加roles)
  • 如何实现多端登录?(可以加入设备信息)

如果你觉得这篇文章有帮助,别忘了点赞收藏!有什么问题欢迎在评论区讨论,我会定期回复大家的问题。