场景

三个高频症状,看起来都像「登出没生效」,实际断在不同环节:

  1. 用户在应用 A 点退出,随后访问应用 B,仍然是登录状态。
  2. 用户从应用点退出后被带回 Keycloak,却停在 “You are logged out” 页面,没有跳回 post_logout_redirect_uri
  3. 走 oauth2-proxy 的应用里,访问 /oauth2/sign_out 后再打开应用,直接免登又进去了。

适用:Keycloak 26.x 的 OIDC 客户端,浏览器应用或 oauth2-proxy 之类的认证网关;想弄清 RP-Initiated Logout 与 Back-Channel / Front-Channel Logout 各自负责哪一层会话。

不适用:SAML 客户端(SAML Single Logout 用 Redirect/POST binding,参数语义与 OIDC 完全不同,见 SAML 2.0 协议详解);仍在用 Admin URL 回调格式的旧 Keycloak Java adapter——Keycloak 文档明确这种回调格式不是 OIDC 标准,只被 legacy adapter 或 Elytron WildFly OIDC adapter 支持。

会话分层、Token 刷新与吊销的整体模型见 IAM 会话管理;本文只解决「为什么这个客户端没被通知」和「怎么验证通知到了」。

四种登出机制覆盖的范围不同

机制入口/规范由谁发起能覆盖什么覆盖不到什么
Session Management前端 JS 会话检测(OP iframe)浏览器轮询依赖该机制的前端应用会话服务端会话、非 JS 客户端
RP-Initiated Logoutend_session_endpoint用户点应用里的退出当前浏览器 SSO 会话,并向挂在该会话上的客户端传播登出其他浏览器/设备的会话
Back-Channel Logout客户端 backchannel.logout.urlKeycloak 服务端直连不依赖浏览器,可覆盖非当前浏览器的会话(管理员吊销场景)未配置 URL 的客户端;未真正实现端点的客户端
Front-Channel Logout客户端 frontchannel.logout.url浏览器 iframe同一个正在被登出的浏览器会话管理员发起的吊销(没有对应浏览器会话)

第一条判断规则:会话残留通常不是 Keycloak 没登出,而是那个客户端根本没有被通知。 Keycloak 客户端帮助文本写得很直白:如果既没有配置 Backchannel logout URL,也没有配置 Admin URL,登出请求不会发给这个客户端;此时应用侧会话会保留到 access token 过期,之后客户端尝试刷新 Token 会失败,因为 Keycloak 的 SSO 会话已经结束。

第二条规则来自 Keycloak 文档:管理员从 Admin Console 或 admin REST API 登出某用户时,可以传播 back-channel logout;front-channel 做不到,因为 front-channel 只能在「正在被登出的那个浏览器会话」里触发。

最容易踩的坑:front / back channel 是互斥开关

Keycloak 客户端的 “Front channel logout” 是一个开关,它决定下面显示哪一组字段:

  • 开关 ON:只显示 Front-Channel Logout URL(不填则回落到客户端的 base/home URL)与 Front-channel logout session required。
  • 开关 OFF:才显示 Backchannel logout URL、Backchannel logout session required、Backchannel logout revoke offline sessions。

所以「Backchannel logout URL 明明填了,客户端却收不到 POST」的最常见原因,就是开关还停在 ON——配置面板把你引导到了 front-channel 分支。Admin UI 里两种 URL 不会同时出现,这是设计行为,不是界面残缺。

界面字段客户端属性作用
Front channel logout(开关)frontchannelLogout决定用浏览器 iframe 还是服务端后台调用
Front-Channel Logout URLfrontchannel.logout.urliframe 请求目标;不填回落到 base URL
Front-channel logout session requiredfrontchannel.logout.session.requirediframe 请求是否携带 sidiss 参数
Backchannel logout URLbackchannel.logout.urlLogout Token 的 POST 目标
Backchannel logout session requiredbackchannel.logout.session.requiredLogout Token 是否包含 sid
Backchannel logout revoke offline sessionsbackchannel.logout.revoke.offline.tokensLogout Token 是否带 revoke_offline_access 事件
Logout confirmationlogout.confirmation.enabled登出后是否插入 “You are logged out” 确认页(默认关闭)
Valid post logout redirect URIspost.logout.redirect.uris允许的登出回跳地址白名单

选型建议:能接收 POST 的服务端应用和认证网关用 back-channel(Keycloak 文档也认为它比 front-channel 更可靠,不依赖浏览器);纯前端 SPA 若已依赖 Session Management,可以不配专用登出 URL。规范层面的硬约束是:RP 的 back-channel logout URI 必须能被 OP 直接访问,藏在只有浏览器可达的 NAT/防火墙后面就无法工作(Back-Channel Logout 1.0 明确列为该机制的局限)。

最小配置

Keycloak 端

  1. 需要立刻登出的客户端:客户端 → Logout settings → 关闭 Front channel logout → 填 Backchannel logout URL,例如 https://app.example.com/oidc/backchannel-logout
  2. 需要精确到单一会话:Backchannel logout session required = ON(Logout Token 带 sid)。sid 存在时客户端只登出对应会话;无 sid 时按 iss + sub 登出该用户在这个 RP 上的所有会话。
  3. 需要离线会话一并失效:Backchannel logout revoke offline sessions = ON。
  4. 发起登出的客户端要登记回跳地址:post.logout.redirect.uris。取值语义为——+ 或留空表示复用 Valid redirect URIs;- 表示不允许任何登出回跳;支持 http://example.com/* 这类简单通配,也支持相对路径(相对 client root URL;未设 root URL 时相对 Keycloak 根地址)。
  5. 不要往登出回跳地址里塞 OIDC 保留参数。Keycloak 默认对 post_logout_redirect_uri 也执行 forbidden params 检查,statecodeerrorsession_stateiss 等都会导致校验失败;需要传递状态请用登出请求自身的 state 参数,Keycloak 会原样带回。仅在明确需要兼容旧行为时才通过客户端属性 allow.oidc.params.in.redirect.uris 放开。

客户端应用端:Logout Token 的校验要点

Back-Channel Logout 1.0 对 Logout Token 的要求是可以直接当验收清单用的:

  • 必须验签(与 ID Token 同一套密钥),可选加密;
  • issaudiatexpjtievents 均为 REQUIRED;events 必须包含成员 http://schemas.openid.net/event/backchannel-logout,这是「这是一张登出令牌」的唯一声明方式;
  • subsid 至少存在一个,可以同时存在;
  • nonce 被规范明令禁止(PROHIBITED),目的是让 Logout Token 无法在伪造的认证响应里被当作 ID Token 使用(cross-JWT confusion)。校验时把它当作「出现即拒」的字段,而不是可选字段。

oauth2-proxy 端:sign_out 不等于 IdP 登出

官方 Endpoints 文档写得很清楚:/oauth2/sign_out 只清除 oauth2-proxy 自己的 Cookie,用户仍登录在认证提供方,再次访问应用可能自动免登。要连 IdP 会话一起结束,必须把浏览器也送到 end_session_endpoint

# 1) rd 参数,必须 URL 编码,指向 IdP 的 end_session_endpoint
https://app.example.com/oauth2/sign_out?rd=https%3A%2F%2Fidp.example.com%2Frealms%2Fcorp%2Fprotocol%2Fopenid-connect%2Flogout%3Fpost_logout_redirect_uri%3Dhttps%253A%252F%252Fapp.example.com%252Flogged-out

# 2) 或使用响应头
GET /oauth2/sign_out HTTP/1.1
X-Auth-Request-Redirect: https://idp.example.com/realms/corp/protocol/openid-connect/logout?post_logout_redirect_uri=https://app.example.com/logged-out

两个配套参数缺一不可:

  • --whitelist-domain:目标域名必须加进白名单,且要写「域名[:端口]」,不要带 scheme。官方文档特别提示「写 localhost:8081 而不是 http://localhost:8081」,写错时跳转会被静默忽略——这正是「配置看起来对,但就是没跳」的常见原因。
  • {id_token} 占位符:可把当前会话的 ID Token 注入跳转 URL,用来携带 id_token_hint.../logout?id_token_hint={id_token}&post_logout_redirect_uri=https://app.example.com/logged-out

如果登出由服务端触发、不需要浏览器参与,可用 --backend-logout-url(同样支持 {id_token})在清理本地会话时直接请求登出端点。

为什么 Keycloak 会返回 400 或停在确认页

下面是 Keycloak LogoutEndpoint 的实际判定,症状可以直接对照:

现象触发条件处理
400,日志 Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used.带了 post_logout_redirect_uri,但既没有 id_token_hint 也没有 client_id两者至少补一个,优先补 id_token_hint
400,日志 Parameter client_id is different than the client for which ID Token was issued.client_idid_token_hint 指向不同客户端统一为同一客户端
400 Invalid redirect uripost_logout_redirect_uri 不在该客户端的 Valid post logout redirect URIs 内,或含被禁止的参数登记回跳地址;去掉 state 等保留参数
被强制要求确认登出未带 id_token_hint;或 id_token_hint 所属客户端在 Realm 中查不到;或浏览器当前会话与 id_token_hint 的会话状态不一致带上当前会话的 id_token_hint
停在 “You are logged out” 不自动跳转客户端开启了 logout.confirmation.enabled(Keycloak 26.5 引入):确认页给的是继续跳转的链接/按钮,而不是自动 302关闭该开关;或接受用户多点一次
无提示直接跳回,跳过了确认浏览器无会话、无 id_token_hint,但有 client_id 且回跳地址有效属预期行为,无需处理

规范层面的两点补充,能解释不少「行为不一致」的抱怨:

  • id_token_hint 在 RP-Initiated Logout 中是 RECOMMENDED,不是可有可无。OP 在 ID Token 已过期时仍应接受它,只要其 aud/sid 表明存在当前或近期会话;反之,若 sid 与当前及近期会话都不匹配,OP 应视为可疑请求并可以拒绝执行。
  • 未提供 id_token_hint,或 ID Token 不属于当前会话时,OP 必须询问用户是否同时登出 OP。因此「不带任何参数的 /logout 静默登出所有应用」在规范预期里并不成立。

管理员吊销会话时,谁会收到通知

Admin Console 的 “Sign out all active sessions” 对应 admin REST 的 POST /admin/realms/{realm}/users/{id}/logout。它的实际行为是:逐个用户会话触发 back-channel 登出,并把该用户的 not-before 设为当前时间,使已签发 Token 失效。

两个前提条件容易漏:目标客户端必须配了 backchannel logout URL(或旧的 Admin URL);只配 front-channel 的客户端不会收到任何通知。改完之后要在两处对账——Keycloak 的 LOGOUT 事件,以及客户端侧的 logout_token POST 记录;只看到其中一处,说明链路没走完。

验证清单

# 1. 确认登出端点存在(以实际 Discovery 返回为准,不要凭版本猜路径)
curl -s https://idp.example.com/realms/corp/.well-known/openid-configuration | jq .end_session_endpoint

# 2. 触发登出,观察状态码与 Location,以及会话 Cookie 是否被清除
curl -sS -D- -o /dev/null \
  "https://idp.example.com/realms/corp/protocol/openid-connect/logout?client_id=app-a&post_logout_redirect_uri=https://app.example.com/logged-out" | head -20

# 3. 确认客户端侧真的收到 Logout Token(按实际日志路径调整)
kubectl logs -n app deploy/app-a --tail=50 | grep -i "backchannel\|logout_token"

# 4. 只看字段、不做验签:确认 events 存在、sid 是否存在、nonce 不存在
echo "$LOGOUT_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{iss,aud,sub,sid,jti,events,nonce}'

第 4 步只用于核对字段,不能替代验签与 iss/aud/时间窗口校验。逐项确认顺序建议:Discovery 端点 → 回跳白名单 → 客户端 URL 与 channel 开关 → 客户端端点可达性与校验逻辑。

常见错误对照表

症状证据根因修复
应用 B 会话残留客户端无任何登出请求记录该客户端没配 backchannel logout URL,也没有 Admin URL配置 URL(并确保从 Keycloak 可达)
配了 Backchannel URL 仍不发请求Keycloak 侧无出站请求Front channel logout 开关仍为 ON关闭开关,切到 back-channel 分支
管理员吊销后应用仍可访问logout_token,仅令牌过期后失效校验只覆盖了 front-channel 客户端关键应用改用 back-channel,或依赖短 access token 作为兜底
/oauth2/sign_out 后又自动免登只见 _oauth2_proxy 被清,没有到 IdP 的跳转sign_out 不清 IdP 会话rd 参数或 X-Auth-Request-Redirect
配了 rd 但仍不跳转浏览器无到 IdP 的请求--whitelist-domain 缺目标域名,或写成了带 scheme 的 URL改为 idp.example.comidp.example.com:8443
登出后停在 “You are logged out”页面含继续跳转按钮logout.confirmation.enabled 已开启按产品预期关闭或保留
登出请求稳定 400日志含上述三条错误文本之一参数组合非法或回跳地址未登记对照上一节逐条修正

如果登出链路改动后登录也开始失败,先检查是否误删了共用配置:Keycloak 的 client 与 redirect URI 校验同时服务登录与登出,Valid Redirect URIs、Audience mapper 被一并清空时,症状会表现为登录报错而非登出报错。

回滚

登出链路涉及三层独立开关,回滚时一次只回退一层,每步后重跑上面的验证清单:

  1. Keycloak 客户端配置:把 Front channel logout 开关恢复原值,清空新增的 logout URL,移除 post.logout.redirect.uris 中新增条目。
  2. 网关参数:移除 rd/X-Auth-Request-Redirect 的改动与 --whitelist-domain 新增项,必要时回退到上一个 Deployment 版本(保留一份改动前的 ConfigMap 便于对照)。
  3. 应用端点:先保留 backchannel logout 端点但改为记录日志不执行登出,确认没有误登出与错误告警后再决定是否下线。

回滚判据是「登录恢复 + 登出行为回到改动前」,不是「没有报错」。仅把配置改回去而不验证,等于不知道是否真的恢复了。

常见问题(IAM 单点登出)

IAM 单点登出(SLO)和普通登出有什么区别?

普通登出只清应用自己的会话;SLO 要求把登出传播到同一 SSO 会话上的其他应用与 IdP 会话本身。在 OIDC 里,RP-Initiated Logout 负责「用户主动退出」这条路径,Back-Channel / Front-Channel Logout 负责「IdP 主动通知各应用」。

Keycloak 能同时对同一个客户端启用 Front-Channel 和 Back-Channel 吗?

不能。客户端上的 Front channel logout 开关决定使用哪一组配置,两种 URL 字段在同一客户端上互斥显示;同时需要两种行为时应拆分客户端,或让应用自行处理其中一种。

oauth2-proxy 的 /oauth2/sign_out 为什么登不出 Keycloak?

因为它只清 oauth2-proxy 自己的 Cookie。必须把浏览器继续导向 end_session_endpointrd 参数或 X-Auth-Request-Redirect),并确保 IdP 域名在 --whitelist-domain 中。

登出后 Access Token / Refresh Token 会立即失效吗?

RP-Initiated Logout 结束的是 IdP 的 SSO 会话;已签发的 access token 在其 exp 之前仍是自包含 JWT,是否立即失效取决于资源服务是否做 introspection、是否校验 not-before,或客户端是否撤销 Token。管理员吊销会话会同时设置用户级 not-before,覆盖范围更大。因此不能把「用户点了退出」当成「所有 Token 立刻作废」,高风险接口仍需后端独立校验。

管理员如何强制某个用户下线?

Admin Console 的 “Sign out all active sessions”(admin REST:POST /admin/realms/{realm}/users/{id}/logout),它会逐会话触发 back-channel 登出并设置 not-before。前提是目标客户端配置了 backchannel logout URL,否则应用侧只能等 Token 过期。

关键来源