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

解决React Router 6加载器在Keycloak认证前请求导致401问题

React Router V6加载器请求过早导致401未授权问题

我的Web应用采用React前端、.NET API与Keycloak身份认证服务。用户通过Keycloak登录后携带JWT重定向至前端,JWT通过Axios请求拦截器附加到API请求头,常规流程正常。但使用React Router V6的createBrowserRouter和加载器功能时,首页加载器请求发起过早,此时Axios拦截器还未完成JWT的添加,导致请求返回401未授权错误。尝试过添加加载等待逻辑但未解决,寻求解决方案。

index.tsx

/*----- 导入模块 -----*/
import App from './App';
import './styles/index.scss';
import { createRoot } from 'react-dom/client';
import { MantineProvider } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import { NotificationsProvider } from '@mantine/notifications';

/*----- KEYCLOAK 导入模块-----*/
import { ReactKeycloakProvider } from '@react-keycloak/web';
import keycloak from './keycloak/keycloak';
import { AxiosInterceptor } from './keycloak/Api';

/*----- REDUX 状态管理导入模块 -----*/
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import store, { persistor } from './redux/store';
import MantineConfig from './ressources/Mantineconfig';

/*----- 根节点渲染 -----*/
const container = document.getElementById('root');
const root = createRoot(container!);

root.render(
  <ReactKeycloakProvider
      authClient={keycloak}
      initOptions={{ 
        onLoad: 'login-required',
        checkLoginIframe: false
      }}  
  >
    <AxiosInterceptor>
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          <MantineProvider withCSSVariables theme={MantineConfig}>
            <ModalsProvider>
              <NotificationsProvider
                position='bottom-right'
                zIndex={2000}
                containerWidth={300}
              >
                <App />
              </NotificationsProvider>
            </ModalsProvider>
          </MantineProvider>
        </PersistGate>
      </Provider>
    </AxiosInterceptor>
  </ReactKeycloakProvider>
)

App.tsx

import ScrollToTop from './components/ScrollToTop';
import { RouterProvider } from 'react-router-dom';
import { router } from './utils/Routes';

function App() {

  return (
    <>

          <div className="ContentContainer">
            <ScrollToTop />
            <RouterProvider router={router} />
          </div>
      
    
    </> 
  );
}

export default App;

Routes.tsx (加载器定义在此处,为精简内容已删减部分路由与导入项)

export const router = createBrowserRouter([

    {
        path:"/",
        element: <Layout/> ,
        children:[
            {
                index: true,
                element: <Home/>,
                loader: homeLoader
            },
            {
                path: "marques",
                element: <Marques />,
                loader: brandLoader
            },
            {
                path: "marques/:id",
                element: <Marque />,
                loader: ({params}) => {
                    return singleBrandLoader(params.id)
                }
            },
            {
                path: "approbations",
                element: <Approbations />
            },
            {
                path: "catalogue",
                element: <Catalogue />,
                loader: catalogLoader
            },
            {
                path: 'panier', 
                element: <Panier />
            },
            {
                path: 'familles', 
                element: <Familles />,
                loader: familleLoader
            },
            {
                path: 'familles/:id', 
                element: <Famille />,
                loader: ({params}) => {
                    return singleFamilyLoader(params.id)
                }
            },
            {
                path: 'formations', 
                element: <Formations />,
                loader: ({params}) => {
                    return formationsLoader()
                }
            },
            {
                path: 'produits/:id', 
                element: <Produits />,
                loader: ({params}) => {
                   return productLoader(params.id)
                }
            },
            {
                path: '/error/:id',
                element: <Error/>
            },
            {
                path: "*", 
                element: <Navigate to='error/404' replace /> 
            }
        ]
    }
])

AxiosInterceptor.tsx

import { useKeycloak } from "@react-keycloak/web";
import axios, { InternalAxiosRequestConfig, RawAxiosRequestHeaders  } from "axios";
import { useEffect } from "react";

// 创建一个Axios实例,通过拦截器在每个请求中携带Keycloak令牌
const API = axios.create({
    headers: {'X-gravitee-api-key': process.env.REACT_APP_API_KEY }
});

/**
 * 拦截器包装组件
 */
const AxiosInterceptor = ({children}) => {
    
    const { keycloak } = useKeycloak();

    useEffect(() => {

        // 创建请求处理函数,获取当前令牌
        // 若认证成功,则在请求头中添加令牌
        
        const handleRequest = ( config: InternalAxiosRequestConfig ) => {
            console.log('HELLO')
            
            if(keycloak?.authenticated){
                let token = keycloak.token;
                config["headers"] = config.headers ?? {};
                (config.headers as RawAxiosRequestHeaders)["Authorization"] = `Bearer ` + token;
            } else {
                console.log('GOODBYE')
            }
            return config;
        }

        // 错误处理函数
        const handleError = ( error: any ) => {
            console.log('ERROR HERE')
            Promise.reject(error)
        };

        // 为请求拦截器添加处理函数
        const requestInterceptors = API.interceptors.request.use(handleRequest, handleError);

        // 组件卸载时移除拦截器
        return () => {
            API.interceptors.request.eject(requestInterceptors);
        }

    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [])

    return children;
}

export default API;
export { AxiosInterceptor };

Layout.tsx

import { Outlet, useNavigate } from "react-router-dom"
import Footer from "./Footer"
import Header from "./Header"
import GoToTop from "./GoToTop"
import { KeycloakUserProvider } from "../keycloak/Context"
import { useHandleMenu } from "../utils/useHandleMenu"
import { useKeycloak } from "@react-keycloak/web"
import { useEffect, useState } from "react"
import CustomLoader from "../utils/CustomLoader"

const Layout = () => {
  const [opened,, toggle] = useHandleMenu();
  const { keycloak } = useKeycloak();
  const isAuth = keycloak.authenticated;
  const navigate = useNavigate();
  const [visible, setVisible] = useState<boolean>(false)

  useEffect(() => {
    if(isAuth){
      setVisible(true)
    }
    if(!isAuth ){
      setVisible(false)
    }
  }, [isAuth, navigate])

  return (

    visible ? ( <CustomLoader /> )
    : (
    <KeycloakUserProvider>
      <GoToTop />

        <Header 
          opened={opened}
          toggle={toggle}
        />
        <Outlet />
        <Footer />
        
    </KeycloakUserProvider>
    )
  )
}

export default Layout

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 06:29:57