React路由鉴权组件在accessToken更新前错误重定向至/signin问题
React路由认证组件在accessToken异步更新时错误重定向的问题
我正在开发一个React应用,需要确保用户访问特定路由前已完成身份验证。我编写了RequireAuthN组件,用于将未认证用户重定向至/signin页面,但该组件在accessToken异步更新时仍会跳转到/signin。
问题描述
RequireAuthN组件本应响应accessToken状态变化并重新渲染,但它似乎未等待accessToken更新就直接重定向至/signin。
疑问
- 为何
accessToken正在更新时,RequireAuthN仍会重定向至/signin? - 如何修改
RequireAuthN组件,使其等待accessToken更新后再决定是否重定向? - 在此场景下,是否有更优的身份验证与路由保护实现方案?
相关代码
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
相关产品推荐
相关产品推荐

