解决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
相关产品推荐
相关产品推荐

