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

JWT路由私有化:页面重载后状态持久化失败及Token丢失问题

页面重载后JWT路由验证失效,Auth状态结构异常

我用JWT实现路由私有化,登录时用户数据和Token能正常存入AuthProvider的state,但页面重载后,数据被存入state的子字段,Token丢失,页面重定向到登录页。正常应该重载后用户数据和Token保留,因为这是私有路由操作的必要条件。

相关代码

AuthProvider

import { useState, useEffect, createContext } from "react";
import clientAxios from "../config/ClientAxios";
import { useNavigate } from "react-router-dom";

const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  const [auth, setAuth] = useState({});

  const navigate = useNavigate();
  
  useEffect(() => {
    const authUser = async () => {
      const token = localStorage.getItem("token");
      
      if (!token) {
        return;
      }
      const config = {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
      };
      try {
        const data = await clientAxios("/usuarios/perfil", config);
        setAuth(data);
        navigate('/inicio')
        
      } catch (error) {
        setAuth({});
      }
    };

    authUser();
  }, []);

  return (
    <AuthContext.Provider
      value={{
        auth,
        setAuth,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};
export { AuthProvider };

export default AuthContext;

ProtectedRoute

import React from "react";
import { Outlet, Navigate } from "react-router-dom";
import useAuth from "../hooks/useAuth";
import Header from "../components/Header";

const ProtectedRoute = () => {
  const { auth } = useAuth();

  return(
    <div>
       {auth._id ? (
        <div className="bg-gray-100">
          <Header/>
          <div className="md:min-h-screen">
            <main>
             <Outlet/>
            </main>
          </div>
        </div>
       )
       :<Navigate to="/"/> }
    </div>
  ) 
};

export default ProtectedRoute;

useAuth

import { useContext } from "react";
import AuthContext from "../context/AuthProvider";

const useAuth = () => {
  return useContext(AuthContext);
};

export default useAuth;

Login

import { Link, useNavigate } from "react-router-dom";
import { useState } from "react";
import Alert from "../components/Alert";
import clientAxios from "../config/ClientAxios";
import useAuth from "../hooks/useAuth";
import { AuthProvider } from "../context/AuthProvider";

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [alert, setAlert] = useState({});

  const {setAuth}=useAuth()

  const navigate=useNavigate()

  const handleSubmit = async (e) => {
    e.preventDefault();
    if ([email, password].includes("")) {
      setAlert({
        msg: "Hay uno o mas campos vacios",
        error: true,
      });
      return;
    }
    try {
      const {data}=await clientAxios.post('/usuarios/',{email,password})
      setAlert({})
      localStorage.setItem('token',data.token)
      setAuth(data)
      navigate('/inicio')
    } catch (error) {
      setAlert({
        msg:error.response.data.msg,
        error:true
      })
    }
  };

  const { msg } = alert;
  return (
    <div>
      <h1 className="text-sky-600 font-black text-4xl capitalize mt-3">
        Bienvenido de vuelta !
      </h1>
      <h3 className="text-gray-400 text-xl font-black">
        {" "}
        Inicia sesion y mejora tu
        <span className="text-sky-500"> ambiente laboral</span>
      </h3>

      {msg && <Alert alert={alert} />}
      <form onSubmit={handleSubmit} className="my-5">
        <label
          htmlFor="email"
          className=" font-bold uppercase block text-xl text-gray-600"
        >
          Email
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email Laboral"
          className="w-full mt-3 p-3 border rounded-xl bg-gray-200 cursor-pointer"
        />
        <label
          htmlFor="password"
          className=" font-bold uppercase block text-xl text-gray-600 mt-3"
        >
          Contraseña
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Contraseña"
          className="w-full mt-3 p-3 border rounded-xl bg-gray-200 cursor-pointer"
        />

        <input
          type="submit"
          value="Iniciar Sesion"
          className="bg-sky-700 text-white w-full py-3 mt-5 font-bold rounded-md uppercase hover:cursor-pointer hover:bg-sky-900 transition-colors"
        />
      </form>
      <nav>
        <Link
          className="hover:underline block text-center my-5 text-slate-500 font-semibold uppercase text-sm"
          to="forget-password"
        >
          Cambia tu Contraseña
        </Link>
      </nav>
    </div>
  );
};

export default Login;

状态截图

  • 登录后正常状态:登录后正常状态(此状态应在页面重载后保持)
  • 页面重载后的异常状态:页面重载后的异常状态(重载后返回登录页,数据存入state子字段,无法通过auth._id验证)

问题分析与解决方案

核心问题

  1. API返回结构不一致:登录时/usuarios/接口返回的是直接包含_id的用户对象,但重载时调用的/usuarios/perfil接口返回的数据可能嵌套了一层(比如response.data.data才是用户信息),导致auth状态结构异常,auth._id不存在。
  2. 状态初始化缺失:页面重载时auth初始化为空对象,ProtectedRoute会先触发重定向,等useEffect中的接口请求完成时已经来不及。
  3. 错误处理不彻底:接口请求失败时没有清除localStorage中的无效token,导致后续验证逻辑混乱。

解决步骤

1. 统一API数据结构处理

检查/usuarios/perfil接口的返回格式,确保和登录接口一致。如果后端返回嵌套结构,调整AuthProvider中的数据提取逻辑:

// AuthProvider的useEffect中
try {
  const response = await clientAxios("/usuarios/perfil", config);
  // 假设后端返回数据嵌套在response.data里,根据实际结构调整
  const userData = response.data; 
  setAuth(userData);
} catch (error) {
  localStorage.removeItem("token");
  setAuth({});
  navigate('/');
}

2. 优化状态初始化与持久化

页面重载时先从localStorage恢复基础信息,避免短暂未认证状态,同时封装setAuth同步更新localStorage:

const AuthProvider = ({ children }) => {
  // 初始化时从localStorage恢复状态
  const initialAuth = () => {
    const token = localStorage.getItem("token");
    const userInfo = localStorage.getItem("userInfo");
    return token && userInfo ? JSON.parse(userInfo) : {};
  };
  const [auth, setAuth] = useState(initialAuth());

  const navigate = useNavigate();
  
  useEffect(() => {
    const authUser = async () => {
      const token = localStorage.getItem("token");
      
      if (!token) {
        setAuth({});
        return;
      }
      const config = {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
      };
      try {
        const response = await clientAxios("/usuarios/perfil", config);
        const userData = response.data;
        setAuth(userData);
        localStorage.setItem("userInfo", JSON.stringify(userData));
      } catch (error) {
        localStorage.removeItem("token");
        localStorage.removeItem("userInfo");
        setAuth({});
        navigate('/', { replace: true });
      }
    };

    authUser();
  }, [navigate]);

  // 封装同步localStorage的setAuth
  const updateAuth = (newAuth) => {
    setAuth(newAuth);
    if (newAuth.token) {
      localStorage.setItem("token", newAuth.token);
      localStorage.setItem("userInfo", JSON.stringify(newAuth));
    } else {
      localStorage.removeItem("token");
      localStorage.removeItem("userInfo");
    }
  };

  return (
    <AuthContext.Provider
      value={{
        auth,
        setAuth: updateAuth,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

3. 增强ProtectedRoute验证逻辑

增加token直接验证,避免仅依赖auth._id,同时添加加载状态避免错误跳转:

import { useState, useEffect } from "react";

const ProtectedRoute = () => {
  const { auth } = useAuth();
  const token = localStorage.getItem("token");
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // 等待auth状态初始化完成
    setIsLoading(false);
  }, [auth]);

  if (isLoading) {
    return <div>Cargando...</div>; // 或加载组件
  }

  return (
    <div>
       {token && auth._id ? (
        <div className="bg-gray-100">
          <Header/>
          <div className="md:min-h-screen">
            <main>
             <Outlet/>
            </main>
          </div>
        </div>
       )
       :<Navigate to="/" replace/> }
    </div>
  ) 
};

4. 修复登录组件冗余代码

移除Login组件中多余的AuthProvider导入,确保组件正确使用全局上下文:

// Login组件中删除此行
import { AuthProvider } from "../context/AuthProvider";

内容的提问来源于stack exchange,提问作者B3rriess

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 12:37:02