Skip to content

会话与登出 ​

本页说明用户如何从 wicket 登出、宿主登出页需要做什么,并如实列出 v0.1.4 中单点登出相关能力的实现程度。结论是:基于 /connect/endsession 的登出可以工作;前通道、后通道通知和 checksession 在 v0.1.4 中实际不会生效,尽管发现文档宣称支持。

本页的请求与结果来自在 v0.1.4 上实际运行的实验程序(在 授权码 + PKCE 的环境上继续操作)。

实现程度一览 ​

能力发现文档v0.1.4 实际
end_session_endpoint(/connect/endsession)列出可用:校验参数后重定向到宿主登出页
post_logout_redirect_uri + state—可用,需配合 id_token_hint
frontchannel_logout_supportedtrue不生效:实测登出上下文中的 SignOutIFrameUrl 为空,不会触发前通道通知
backchannel_logout_supportedtrue未实现:发送通知的服务是空实现,且没有调用方
check_session_iframe(/connect/checksession)列出页面可访问,但授权响应不返回 session_state,无法使用
服务端会话 session.Store—可写入记录;吊销需宿主配合(见下文)

前通道无法生效的直接原因是:用户会话里“登录过哪些客户端”的列表不会跨请求保留,登出时这个列表为空。

1. 配置 ​

go
opts.UserInteraction.LogoutUrl = "/account/logout" // 默认是空字符串,必须设置
// LogoutIdParameter 默认 "logoutId"

web.PostLogoutRedirectUris = []string{"http://127.0.0.1:8080/signed-out"}

2. 客户端发起登出 ​

text
GET /connect/endsession
    ?id_token_hint=eyJhbGciOiJSUzI1NiIs...
    &post_logout_redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fsigned-out
    &state=bye

GET 与 POST(表单)均可。wicket 校验后重定向到宿主登出页(实测):

text
HTTP/1.1 302 Found
Location: /account/logout?logoutId=ZXlKRGNtVmhkR1ZrSWpveE56azBOVE01TXpFeUxD...
参数说明
id_token_hint之前签发的 id_token,用来识别客户端;不校验有效期,过期的也可以用。若当前浏览器已登录且 sub 不同,请求被拒绝
post_logout_redirect_uri仅在带 id_token_hint 且地址在该客户端 PostLogoutRedirectUris 中时生效;不匹配时静默忽略(实测登出上下文中该字段为空)
state只有 post_logout_redirect_uri 生效时才会被附加到回跳地址
ui_locales透传给登出页

不带任何参数也能访问,此时直接重定向到 /account/logout,没有 logoutId。

3. 宿主登出页 ​

登出页用 logoutId 读取上下文,清除宿主的登录 Cookie,然后决定是否回跳客户端:

go
app.HandleFunc("/account/logout", func(w http.ResponseWriter, r *http.Request) {
	sp := hosting.ScopeFrom(r.Context())
	lr, err := godi.GetRequired[services.InteractionService](sp).
		GetLogoutContext(r.Context(), r.URL.Query().Get("logoutId"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// lr.ShowSignoutPrompt() 为 true(没有可识别的客户端)时,建议先让用户确认,
	// 防止第三方页面诱导登出。

	if err := godi.GetRequired[auth.Service](sp).SignOut(r, w,
		wicket.DefaultCookieAuthenticationScheme, nil); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	if lr.PostLogoutRedirectUri != "" {
		http.Redirect(w, r, lr.PostLogoutRedirectUri, http.StatusFound)
		return
	}
	// 渲染“已登出”页面;lr.SignOutIFrameUrl 非空时应以隐藏 iframe 嵌入(v0.1.4 中总为空)
})

实测登出上下文:ClientId 为 web-app,PostLogoutRedirectUri 为 http://127.0.0.1:8080/signed-out?state=bye(state 已附加好),SignOutIFrameUrl、SubjectId、SessionId 均为空,ClientIds 为空列表。登出页执行 SignOut 后,再访问 /connect/authorize 会重新跳转到登录页。

登出要由宿主完成

/connect/endsession 本身不会清除任何 Cookie 或会话,它只负责校验参数并把用户交给登出页。真正的登出发生在你调用 auth.Service.SignOut(即你的 Cookie 处理器的 SignOut)时。

4. 前通道、后通道与 checksession ​

为了让你评估影响,这里说明 v0.1.4 中的现状:

  • 前通道:Client.FrontChannelLogoutUri 可以配置,/connect/endsession/callback?endSessionId=... 端点也已注册。回调页本身能工作:wicket 的集成测试直接写入一份通知上下文时,它会为每个客户端输出一个带 iss、sid 参数的隐藏 iframe。但在真实的登出流程中,这份上下文需要“会话 ID + 登录过的客户端列表”,而两者在 v0.1.4 中都是空的(无论是否启用服务端会话,实测都一样),所以登出页拿到的 SignOutIFrameUrl 为空,前通道通知不会发出。
  • 后通道:Client.BackChannelLogoutUri 可以配置,cfg.SetBackChannelLogoutHttpClient 也存在,但负责发送登出令牌的服务直接返回成功、不发送任何请求,也没有代码调用它。
  • checksession:GET /connect/checksession 返回会话检查 iframe 页面,读取名为 wicket.session 的 Cookie。但授权响应中没有 session_state 参数,客户端无从比较;该 Cookie 也只在启用服务端会话时才会写入。

需要单点登出时,目前的做法是由宿主自行维护“用户登录过哪些客户端”,在登出页中通知这些客户端(例如调用各客户端自己的登出接口)。客户端侧应以较短的访问令牌有效期为主,不要依赖 wicket 的登出通知。

5. 服务端会话(session.Store) ​

启用后,每次登录(UserSession.CreateSessionId,即开发用 Cookie 处理器在 SignIn 中调用的方法)都会在 session.Store 中写入一条记录,并下发 wicket.session Cookie。

go
opts.ServerSideSessions.Enabled = true
// 默认:SessionLifetime 14 天;CookieName "wicket.session";
// CookieSecure true;CookieSameSite None。本地 http 调试时把 CookieSecure 设为 false。

cfg.SetSessionStore(myStore) // 实现 session.Store;开发时可用 session.NewMemoryStore(clock)

session.Store 提供按会话 ID 的增删改查,以及 GetSessionsBySubjectID、DeleteSessionsBySubjectID(按用户批量吊销)和 DeleteExpired(回收空间,由宿主定时调用——wicket 不自起后台 goroutine)。

v0.1.4 中会话记录不会自动生效

实测启用服务端会话后:

  • 登录时会写入记录(SubjectID 正确),但记录的 ClientIDs 始终为空;
  • 后续请求不会从 Cookie 恢复会话,签发的令牌中没有 sid,因此令牌校验时的会话检查从不触发;
  • 服务端会话记录不参与浏览器登录态的判断:按用户删除会话(DeleteSessionsBySubjectID)本身不会让浏览器登出,已签发的令牌和刷新令牌也不受影响。

浏览器是否“已登录”完全由你的 Cookie 处理器的 Authenticate 决定。

要让“按用户吊销”对浏览器登录真正生效,让 Cookie 处理器在 Authenticate 时检查会话是否还在。在开发用处理器(见 授权码 + PKCE)基础上:保存 CreateSessionId 返回的会话 ID,每次认证时查一次存储。以下是需要改动的片段(示意:同样的逻辑已在实验程序中以普通 map 的写法运行验证,这里的 sync.Map 写法未单独编译):

go
type ticket struct {
	p   *claims.ClaimsPrincipal
	sid string // CreateSessionId 的返回值
}

// SignIn 中:
sid, err := h.session.CreateSessionId(h.r.Context(), p, props)
if err != nil {
	return err
}
tickets.Store(id, ticket{p: p, sid: sid})

// Authenticate 中,取到 ticket 之后:
if h.sessions != nil && t.sid != "" {
	if _, err := h.sessions.GetSession(h.r.Context(), t.sid); err != nil {
		return auth.NoResult(), nil // 会话已被吊销或过期:视为未登录
	}
}

// 处理器工厂中注入存储:
// h.sessions = godi.GetRequired[session.Store](p)

实测加上这项检查后,调用 DeleteSessionsBySubjectID(ctx, "carol") 删除 1 条会话,carol 的浏览器再次访问 /connect/authorize 就被重定向到登录页。对应地,在 SignOut 中调用 h.sessions.DeleteSession(ctx, t.sid) 可以在登出时同步清理记录(这一步为示意,未单独运行)。

注意这只影响浏览器登录态;已签发的访问令牌在过期前仍然有效,刷新令牌的处理见 刷新令牌。

相关阅读 ​

以 Apache License 2.0 发布 · 文档内容 © 2026 Wicket 作者