React+TypeScript中keycloak-js客户端认证时authenticated返回undefined
Keycloak认证后
authenticated始终为undefined的问题排查与解决 问题现象
在React/TypeScript开发的SPA中集成Keycloak时,点击登录按钮跳转至Keycloak认证页面,认证成功后网络请求显示将跳转至/internal,但Router.tsx中keycloakClient.authenticated始终返回undefined,页面被强制重定向到/login,疑似Keycloak实例每次都会重新初始化,导致认证状态丢失。
现有代码实现
App.tsx(Provider包裹)
import { keycloakClient } from './components/KeycloakProvider/keycloakClient'; const App = () => { return ( <KeycloakProvider client={keycloakClient}> <Router /> </KeycloakProvider> ); };
Router.tsx(路由保护)
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => { const { keycloakClient } = useKeycloak(); console.log(keycloakClient.authenticated); if (!keycloakClient.authenticated) return <Navigate to="/login" replace />; return children; }; const Router = () => { return ( <Routes> <Route path="/login" element={<Login />} /> <Route path="/internal" element={ <PrivateRoute> <Internal /> </PrivateRoute> } /> </Routes> ); };
Login.tsx(登录触发逻辑)
const doLogin = async (keycloakClient: Keycloak) => { await keycloakClient.init({ onLoad: 'login-required', redirectUri: `${window.location.origin}/internal`, checkLoginIframe: false, pkceMethod: 'S256' }); };
Keycloak Provider相关实现
keycloakClient.tsx
import Keycloak, { KeycloakConfig } from 'keycloak-js'; const keycloakConfig: KeycloakConfig = await fetch('/api/auth/props') .then((response) => response.json()) .then((data) => { return { url: data.auth_server_url, realm: data.realm, clientId: data.client_name }; }); export const keycloakClient = new Keycloak(keycloakConfig);
keycloakContext.tsx
const KeycloakContext = createContext<IKeycloakContextData | undefined>(undefined); export default KeycloakContext;
keycloakProvider.tsx
export const KeycloakProvider: React.FC<KeycloakProviderProps> = ({ client, children }) => { const contextValue: IKeycloakContextData = { keycloakClient: client }; return <KeycloakContext.Provider value={contextValue}>{children}</KeycloakContext.Provider>; };
useKeycloak.tsx
export const useKeycloak = (): IKeycloakContextData => { const context = useContext(KeycloakContext); if (!context) { throw new Error('useKeycloak must be used within a KeycloakProvider'); } return context; };
问题根源分析
- 实例初始化时机错误:
keycloakClient.tsx在模块顶层用await fetch获取配置,模块加载时异步请求可能未完成就创建实例;页面刷新或热重载时模块会重新加载,导致实例被重新创建,丢失之前的认证状态。 - 认证后未恢复状态:从Keycloak跳转回应用后,没有在应用初始化阶段调用
init()恢复认证状态,仅在登录按钮点击时调用,导致authenticated属性未被正确更新。 - 状态变化未同步:Keycloak实例的
authenticated状态是异步更新的,但当前Provider未监听状态变化并通知组件,导致组件始终拿到初始的undefined值。
解决方案
1. 修正Keycloak实例创建方式
将配置获取和实例创建分离,避免模块加载时的异步问题:
修改keycloakClient.tsx:
import Keycloak, { KeycloakConfig } from 'keycloak-js'; export const getKeycloakConfig = async (): Promise<KeycloakConfig> => { const response = await fetch('/api/auth/props'); const data = await response.json(); return { url: data.auth_server_url, realm: data.realm, clientId: data.client_name }; }; export const createKeycloakInstance = (config: KeycloakConfig) => new Keycloak(config);
2. 改进Provider,跟踪状态变化
在Provider内部处理实例初始化和状态监听,确保组件能响应认证状态变化:
修改keycloakProvider.tsx:
import { useState, useEffect } from 'react'; import Keycloak from 'keycloak-js'; import { getKeycloakConfig, createKeycloakInstance } from './keycloakClient'; import KeycloakContext from './keycloakContext'; export const KeycloakProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [keycloakClient, setKeycloakClient] = useState<Keycloak | null>(null); const [authenticated, setAuthenticated] = useState<boolean | undefined>(undefined); useEffect(() => { const initKeycloak = async () => { const config = await getKeycloakConfig(); const client = createKeycloakInstance(config); // 用check-sso模式自动检查登录状态 const isAuthenticated = await client.init({ onLoad: 'check-sso', redirectUri: `${window.location.origin}/internal`, checkLoginIframe: false, pkceMethod: 'S256' }); setAuthenticated(isAuthenticated); setKeycloakClient(client); // 监听状态变化,实时同步 client.onAuthSuccess = () => setAuthenticated(true); client.onAuthError = () => setAuthenticated(false); client.onAuthLogout = () => setAuthenticated(false); }; initKeycloak(); }, []); // 实例初始化完成前显示加载 if (!keycloakClient) return <div>加载中...</div>; const contextValue = { keycloakClient, authenticated }; return ( <KeycloakContext.Provider value={contextValue}> {children} </KeycloakContext.Provider> ); };
3. 更新useKeycloak和PrivateRoute
修改useKeycloak.tsx的类型定义:
import { useContext } from 'react'; import KeycloakContext from './keycloakContext'; import Keycloak from 'keycloak-js'; interface IKeycloakContextData { keycloakClient: Keycloak; authenticated: boolean | undefined; } export const useKeycloak = (): IKeycloakContextData => { const context = useContext(KeycloakContext); if (!context) { throw new Error('useKeycloak must be used within a KeycloakProvider'); } return context; };
更新PrivateRoute,处理初始化状态:
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => { const { authenticated } = useKeycloak(); // 初始化中显示加载 if (authenticated === undefined) return <div>加载中...</div>; if (!authenticated) return <Navigate to="/login" replace />; return children; };
4. 修正Login组件登录逻辑
直接调用Keycloak的login方法,不再重复初始化:
const { keycloakClient } = useKeycloak(); const doLogin = () => { keycloakClient.login({ redirectUri: `${window.location.origin}/internal` }); };
关键说明
- 用
check-sso模式初始化,应用启动时自动检查用户认证状态,避免仅在登录时才初始化。 - 监听Keycloak的认证事件,实时同步状态,确保组件能响应状态变化。
- 将实例创建和初始化放在Provider内部,避免模块重复加载导致实例丢失。
内容的提问来源于stack exchange,提问作者chronos
相关产品推荐
相关产品推荐

