已登录时直接访问React+Express受保护路由JWT鉴权失败问题
问题:直接访问受保护路由时JWT令牌未携带导致认证失败
我正在开发一个基于React+Vite前端、Express后端、MongoDB数据库的Web应用,包含/employees、/clients等需要JWT令牌认证的受保护路由。
问题现象:
已登录状态下,通过页面内链接导航到受保护路由完全正常,但直接在地址栏输入URL访问时,会收到后端返回的错误:
{"error":"Access denied: No token provided"}
此时localStorage中已存储有效令牌,登录逻辑正常,应用内导航时请求头能正确携带令牌。
后端代码
auth.js(登录接口)
const express = require("express"); const router = express.Router(); const User = require("../models/User"); const bcrypt = require("bcryptjs"); const jwt = require("jsonwebtoken"); const JWT_SECRET = process.env.JWT_SECRET; // Login route router.post("/login", async (req, res) => { const { username, password } = req.body; try { const user = await User.findOne({ username }); if (!user) return res.status(400).send({ error: "Invalid credentials" }); const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) return res.status(400).send({ error: "Invalid credentials" }); const token = jwt.sign({ id: user._id }, JWT_SECRET, { expiresIn: "7d" }); res.send({ token }); } catch (err) { res.status(500).send({ error: "Internal server error" }); } }); module.exports = router;
authMiddleware.js(认证中间件)
const jwt = require("jsonwebtoken"); const JWT_SECRET = process.env.JWT_SECRET; const authMiddleware = (req, res, next) => { let token; const authHeader = req.header("Authorization"); if (authHeader && authHeader.startsWith("Bearer ")) { token = authHeader.replace("Bearer ", ""); } if (!token) { console.log("No token provided"); return res.status(401).send({ error: "Access denied: No token provided" }); } try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (err) { console.log("Invalid token:", err.message); res.status(401).send({ error: "Invalid token" }); } }; module.exports = authMiddleware;
前端代码
App.jsx
import { useEffect } from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import axios from 'axios'; import PrivateRoute from './components/PrivateRoute'; import EmployeesPage from './components/EmployeesPage'; import ClientsPage from './components/ClientsPage'; import LoginPage from './components/LoginPage'; const App = () => { useEffect(() => { const token = localStorage.getItem('token'); if (token) { axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; } }, []); return ( <Router> <Routes> <Route path="/login" element={<LoginPage />} /> <Route path="/employees" element={<PrivateRoute element={<EmployeesPage />} />} /> <Route path="/clients" element={<PrivateRoute element={<ClientsPage />} />} /> </Routes> </Router> ); }; export default App;
PrivateRoute.jsx
import { Navigate } from 'react-router-dom'; const PrivateRoute = ({ element, redirectTo = '/login' }) => { const token = localStorage.getItem('token'); if (!token) { return <Navigate to={redirectTo} />; } return element; }; export default PrivateRoute;
已尝试操作
- 确认登录后令牌已成功存入localStorage;
- 在应用加载时通过
useEffect设置Axios默认Authorization请求头; - 验证应用内链接访问受保护路由时请求头正确携带令牌,功能正常。
问题原因
直接在地址栏输入URL时,浏览器会发起全新的HTTP请求加载页面,此时React应用还未完成初始化:
App.jsx中的useEffect是在组件挂载后才执行,而页面初始化时的API请求(比如EmployeesPage组件挂载时发起的员工列表请求)会先于这个useEffect执行,导致Axios还没来得及设置默认请求头;PrivateRoute仅做了前端路由层面的令牌存在性校验,但无法保证API请求时令牌已被添加到请求头。
解决方案
1. 封装Axios实例(推荐)
创建src/utils/axiosInstance.js,通过请求拦截器在每次API调用前自动添加令牌:
import axios from 'axios'; const axiosInstance = axios.create({ baseURL: process.env.REACT_APP_API_URL // 替换为你的后端API地址 }); // 请求拦截器:每次发起请求前自动添加令牌 axiosInstance.interceptors.request.use( (config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); export default axiosInstance;
之后所有API调用都使用这个实例,比如EmployeesPage中:
import axiosInstance from '../utils/axiosInstance'; const EmployeesPage = () => { useEffect(() => { const fetchEmployees = async () => { try { const res = await axiosInstance.get('/employees'); // 处理返回数据 } catch (err) { // 处理错误 } }; fetchEmployees(); }, []); // ...组件其他内容 };
2. 优化PrivateRoute的加载逻辑
添加加载状态,确保令牌读取完成后再渲染组件,避免过早发起API请求:
import { Navigate, useState, useEffect } from 'react-router-dom'; const PrivateRoute = ({ element, redirectTo = '/login' }) => { const [isAuthenticated, setIsAuthenticated] = useState(null); useEffect(() => { const token = localStorage.getItem('token'); setIsAuthenticated(!!token); }, []); // 加载中可显示自定义加载组件 if (isAuthenticated === null) { return <div>Loading...</div>; } return isAuthenticated ? element : <Navigate to={redirectTo} />; }; export default PrivateRoute;
3. 后端可选优化(Cookie兼容)
如果希望同时支持从Cookie读取令牌,可修改认证中间件:
const authMiddleware = (req, res, next) => { let token; // 优先从Authorization头获取 const authHeader = req.header("Authorization"); if (authHeader && authHeader.startsWith("Bearer ")) { token = authHeader.replace("Bearer ", ""); } // 其次从Cookie获取(需前端登录时将令牌存入Cookie) else if (req.cookies.token) { token = req.cookies.token; } if (!token) { console.log("No token provided"); return res.status(401).send({ error: "Access denied: No token provided" }); } try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (err) { console.log("Invalid token:", err.message); res.status(401).send({ error: "Invalid token" }); } };
注意:使用Cookie存储令牌时,需在登录接口设置HttpOnly Cookie,并做好CSRF防护。
内容的提问来源于stack exchange,提问作者Ahmad Alam
相关产品推荐
相关产品推荐

