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

React中Keycloak令牌交换式模拟登录问题求助

Keycloak令牌交换模拟登录前端实现问题解决

问题描述

通过令牌交换方式获取模拟登录的令牌后,执行checkLoginIframe()时,login-status-iframe.html因检测不到KEYCLOAK_SESSION或KEYCLOAK_SESSION_LEGACY Cookie,将会话状态设为"changed",需解决前端侧正确实现Keycloak模拟登录的问题。

使用版本:

  • Keycloak Server 15.1.0
  • keycloak-js 16.1.1
  • @react-keycloak/web 3.4.0

核心原因

Keycloak的checkLoginIframe机制依赖服务器设置的会话Cookie(KEYCLOAK_SESSION/KEYCLOAK_SESSION_LEGACY)验证会话有效性。直接通过令牌交换获取令牌的方式,不会触发Keycloak在前端设置这些Cookie,导致iframe检测不到会话状态,误判为会话变更。

解决方案

1. 禁用checkLoginIframe(简单直接)

如果不需要实时检测会话状态变更,可直接在初始化选项中关闭checkLoginIframe,避免触发iframe检测逻辑:

async function impersonate() {
  const impersonatorTokens = await getImpersonatorTokens();
  const impersonatedTokens = await getImpersonatedTokens(impersonatorTokens?.access_token);

  setInitOptions(!!impersonatedTokens?.access_token && !!impersonatedTokens?.refresh_token ? {
    token: impersonatedTokens?.access_token,
    refreshToken: impersonatedTokens?.refresh_token,
    checkLoginIframe: false // 禁用iframe检测
  } : undefined);
}

2. 手动注入session_state并同步会话

如果需要保留iframe检测,需从令牌中解析出session_state并设置到Keycloak实例,确保iframe能识别会话:

async function impersonate() {
  const impersonatorTokens = await getImpersonatorTokens();
  const impersonatedTokens = await getImpersonatedTokens(impersonatorTokens?.access_token);

  if (!impersonatedTokens?.access_token || !impersonatedTokens?.refresh_token) return;

  // 从access_token解析session_state
  const parseJwt = (token: string) => {
    try {
      return JSON.parse(atob(token.split('.')[1]));
    } catch (e) {
      return null;
    }
  };
  const decodedToken = parseJwt(impersonatedTokens.access_token);
  const sessionState = decodedToken?.session_state;

  // 手动设置Keycloak实例的sessionState
  keycloak.sessionState = sessionState;

  setInitOptions({
    token: impersonatedTokens.access_token,
    refreshToken: impersonatedTokens.refresh_token,
    checkLoginIframe: true
  });
}

3. 触发Keycloak会话同步(推荐)

调用keycloak.init时传入令牌并设置onLoad: 'check-sso',触发Keycloak与服务器的会话同步,自动生成所需Cookie:

async function impersonate() {
  const impersonatorTokens = await getImpersonatorTokens();
  const impersonatedTokens = await getImpersonatedTokens(impersonatorTokens?.access_token);

  if (!impersonatedTokens?.access_token || !impersonatedTokens?.refresh_token) return;

  // 直接初始化Keycloak,触发会话同步
  await keycloak.init({
    token: impersonatedTokens.access_token,
    refreshToken: impersonatedTokens.refresh_token,
    onLoad: 'check-sso',
    checkLoginIframe: true
  });
}

4. 客户端与请求配置调整

  • Keycloak客户端配置:在客户端react-auth的Web Origins中添加前端应用域名(如http://localhost:3000),允许跨域Cookie设置。
  • axios请求配置:跨域场景下需开启withCredentials,确保请求携带Cookie:
async function token(data?: any, authorization?: string) {
  const url = "http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/token";

  const config = {
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      ...!!authorization && { Authorization: authorization }
    },
    withCredentials: true // 跨域携带Cookie
  };

  let response;
  try {
    response = await axios.post(url, data, config);
  } catch (error) {
    console.log(error);
  }
  return response?.data;
}

修改后的完整代码

import { useState } from "react";
import Keycloak from "keycloak-js";
import { AuthClientInitOptions } from "@react-keycloak/core";
import { ReactKeycloakProvider } from "@react-keycloak/web";
import axios from 'axios';

const config = "./keycloak.json";

const keycloak = new (Keycloak as any)(config);

function handleKeycloakOnEvent(eventType: any, error: any) {
  console.log("eventType = \"%s\" error = %o", eventType, error);
}

function handleKeycloakOnTokens(tokens: any) {
  console.log("tokens = %o", tokens);
}

function App() {
  const [initOptions, setInitOptions] = useState<AuthClientInitOptions | undefined>();

  async function impersonate() {
    const impersonatorTokens = await getImpersonatorTokens();
    const impersonatedTokens = await getImpersonatedTokens(impersonatorTokens?.access_token);

    if (!impersonatedTokens?.access_token || !impersonatedTokens?.refresh_token) return;

    const parseJwt = (token: string) => {
      try {
        return JSON.parse(atob(token.split('.')[1]));
      } catch (e) {
        return null;
      }
    };
    const decodedToken = parseJwt(impersonatedTokens.access_token);
    const sessionState = decodedToken?.session_state;
    keycloak.sessionState = sessionState;

    setInitOptions({
      token: impersonatedTokens.access_token,
      refreshToken: impersonatedTokens.refresh_token,
      checkLoginIframe: false
    });
  }
  
  function getImpersonatorTokens() {
    return token({
      client_id: "react-auth",
      grant_type: "password",
      username: "myuser",
      password: "myuser"
    });
  }
  
  function getImpersonatedTokens(accessToken?: string) {
    return token({
      client_id: "react-auth",
      grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
      requested_subject: "testuser",
      subject_token: accessToken,
      requested_token_type: "urn:ietf:params:oauth:token-type:refresh_token",
      audience: "react-auth"
    }, `Bearer ${accessToken}`);
  }

  async function token(data?: any, authorization?: string) {
    const url = "http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/token";
  
    const config = {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        ...!!authorization && { Authorization: authorization }
      },
      withCredentials: true
    };
  
    let response;
    try {
      response = await axios.post(url, data, config);
    } catch (error) {
      console.log(error);
    }
    return response?.data;
  }

  return (
    <div>
      <ReactKeycloakProvider
        initOptions={initOptions}
        authClient={keycloak}
        onEvent={handleKeycloakOnEvent}
        onTokens={handleKeycloakOnTokens}
      >
        <input type="button" value="Impersonate" onClick={impersonate} />
        <input type="button" value="keycloak" onClick={() => console.log(keycloak)} />
        <input type="button" value="login" onClick={() => keycloak.login()} />
        <input type="button" value="logout" onClick={() => keycloak.logout()} />
      </ReactKeycloakProvider>
    </div>
  );
}

export default App;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 18:34:59