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

