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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 07:09:51