You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

已登录时直接访问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应用还未完成初始化:

  1. App.jsx中的useEffect是在组件挂载后才执行,而页面初始化时的API请求(比如EmployeesPage组件挂载时发起的员工列表请求)会先于这个useEffect执行,导致Axios还没来得及设置默认请求头;
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 05:05:04