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

React路由鉴权组件在accessToken更新前错误重定向至/signin问题

React路由认证组件在accessToken异步更新时错误重定向的问题

我正在开发一个React应用,需要确保用户访问特定路由前已完成身份验证。我编写了RequireAuthN组件,用于将未认证用户重定向至/signin页面,但该组件在accessToken异步更新时仍会跳转到/signin。

问题描述

RequireAuthN组件本应响应accessToken状态变化并重新渲染,但它似乎未等待accessToken更新就直接重定向至/signin。

疑问

  1. 为何accessToken正在更新时,RequireAuthN仍会重定向至/signin?
  2. 如何修改RequireAuthN组件,使其等待accessToken更新后再决定是否重定向?
  3. 在此场景下,是否有更优的身份验证与路由保护实现方案?

相关代码

RequireAuthN组件

import { useSelector } from "react-redux";
import { Navigate, Outlet, useLocation } from "react-router-dom";

const RequireAuthN = () => {
  const location = useLocation();
  const { accessToken } = useSelector((state) => state.auth);

  console.log("RequireAuthN accessToken", accessToken);
  return accessToken ? (
    <Outlet />
  ) : (
    <Navigate to="/signin" state={{ from: location }} replace />
  );
};

export default RequireAuthN;

Redux Auth Slice

import { createSlice } from "@reduxjs/toolkit";

const authSlice = createSlice({
  name: "auth",
  initialState: {
    accessToken: null,
    permissionToken: null,
    userInfo: null,
    userRoles: null,
  },
  reducers: {
    setAuthCreds: (state, action) => {
      state.accessToken = action.payload.accessToken;
      state.permissionToken = action.payload.permissionToken;
      state.userInfo = action.payload.userInfo;
      state.userRoles = action.payload.userRoles;
    },
    resetAuthCreds: (state) => {
      state.accessToken = null;
      state.permissionToken = null;
      state.userInfo = null;
      state.userRoles = null;
    },
  },
});

export const { setAuthCreds, resetAuthCreds } = authSlice.actions;
export default authSlice.reducer;

解答

1. 问题原因

组件初始化时,accessToken的初始值是null,此时组件会直接触发重定向逻辑。而accessToken的异步更新(比如从本地存储读取、接口请求获取)是在组件首次渲染之后才完成的,虽然状态更新会触发组件重新渲染,但之前的重定向已经执行,浏览器路由已经跳转,后续的渲染无法逆转这个操作。

2. 修改RequireAuthN组件的方案

需要添加加载状态,等待异步认证流程完成后再做判断。具体步骤如下:

第一步:更新Redux Auth Slice

新增isLoading字段标记认证状态是否正在确认:

import { createSlice } from "@reduxjs/toolkit";

const authSlice = createSlice({
  name: "auth",
  initialState: {
    accessToken: null,
    permissionToken: null,
    userInfo: null,
    userRoles: null,
    isLoading: true, // 初始为加载中状态
  },
  reducers: {
    setAuthCreds: (state, action) => {
      state.accessToken = action.payload.accessToken;
      state.permissionToken = action.payload.permissionToken;
      state.userInfo = action.payload.userInfo;
      state.userRoles = action.payload.userRoles;
      state.isLoading = false; // 认证完成后关闭加载
    },
    resetAuthCreds: (state) => {
      state.accessToken = null;
      state.permissionToken = null;
      state.userInfo = null;
      state.userRoles = null;
      state.isLoading = false; // 重置后关闭加载
    },
    setAuthLoading: (state, action) => {
      state.isLoading = action.payload; // 手动设置加载状态
    },
  },
});

export const { setAuthCreds, resetAuthCreds, setAuthLoading } = authSlice.actions;
export default authSlice.reducer;

第二步:修改RequireAuthN组件

加载中时渲染加载UI,不执行重定向:

import { useSelector } from "react-redux";
import { Navigate, Outlet, useLocation } from "react-router-dom";

const RequireAuthN = () => {
  const location = useLocation();
  const { accessToken, isLoading } = useSelector((state) => state.auth);

  // 加载中时显示加载组件,替换成你自己的加载UI
  if (isLoading) {
    return <div>加载中...</div>;
  }

  return accessToken ? (
    <Outlet />
  ) : (
    <Navigate to="/signin" state={{ from: location }} replace />
  );
};

export default RequireAuthN;

第三步:处理异步认证初始化

在应用入口(比如App组件)执行异步获取token的逻辑:

import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { setAuthCreds, setAuthLoading } from "./authSlice";

function App() {
  const dispatch = useDispatch();

  useEffect(() => {
    const initAuth = async () => {
      try {
        // 模拟从本地存储读取token,或调用接口获取
        const savedToken = localStorage.getItem("accessToken");
        if (savedToken) {
          // 可选:验证token有效性
          // const valid = await fetch("/api/validate-token", {
          //   headers: { Authorization: `Bearer ${savedToken}` }
          // }).then(res => res.ok);
          // if (valid) {
            dispatch(setAuthCreds({ accessToken: savedToken }));
          // } else {
          //   localStorage.removeItem("accessToken");
          //   dispatch(setAuthLoading(false));
          // }
        } else {
          dispatch(setAuthLoading(false));
        }
      } catch (err) {
        console.error("认证初始化失败", err);
        dispatch(setAuthLoading(false));
      }
    };

    initAuth();
  }, [dispatch]);

  // ... 路由渲染逻辑
}

3. 更优的实现方案

方案一:使用React Router v6.4+的loader函数

路由匹配时提前执行异步认证,避免组件级的初始渲染问题:

import { createBrowserRouter, RouterProvider, redirect } from "react-router-dom";
import { getStore } from "./store"; // 获取Redux store实例
import { setAuthCreds } from "./authSlice";

// 认证loader,路由加载前执行
async function authLoader() {
  const savedToken = localStorage.getItem("accessToken");
  if (!savedToken) {
    return redirect("/signin");
  }
  // 可选:验证token有效性
  // const isValid = await fetch("/api/validate-token", {
  //   headers: { Authorization: `Bearer ${savedToken}` }
  // }).then(res => res.ok);
  // if (!isValid) {
  //   localStorage.removeItem("accessToken");
  //   return redirect("/signin");
  // }
  // 更新Redux状态
  getStore().dispatch(setAuthCreds({ accessToken: savedToken }));
  return null;
}

const router = createBrowserRouter([
  {
    path: "/protected",
    loader: authLoader,
    element: <RequireAuthN />,
    children: [
      { path: "", element: <ProtectedPage /> },
    ],
  },
  { path: "/signin", element: <SignInPage /> },
]);

function App() {
  return <RouterProvider router={router} />;
}

方案二:封装自定义认证Hook

把认证逻辑封装成Hook,提高复用性:

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setAuthCreds, setAuthLoading } from "./authSlice";

export function useAuth() {
  const { accessToken, isLoading } = useSelector((state) => state.auth);
  const dispatch = useDispatch();
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    const checkAuth = async () => {
      const token = localStorage.getItem("accessToken");
      if (token) {
        // 可选:验证token
        // const valid = await validateToken(token);
        // if (valid) {
          dispatch(setAuthCreds({ accessToken: token }));
          setIsAuthenticated(true);
        // } else {
        //   localStorage.removeItem("accessToken");
        //   setIsAuthenticated(false);
        // }
      } else {
        setIsAuthenticated(false);
      }
      dispatch(setAuthLoading(false));
    };

    checkAuth();
  }, [dispatch]);

  return { isAuthenticated, isLoading, accessToken };
}

在RequireAuthN组件中使用:

import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuth } from "./useAuth";

const RequireAuthN = () => {
  const location = useLocation();
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) {
    return <div>加载中...</div>;
  }

  return isAuthenticated ? <Outlet /> : <Navigate to="/signin" state={{ from: location }} replace />;
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 11:05:54