批量更新
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
)
|
||||
|
||||
// BackchannelLogoutEvent OIDC 标准登出事件声明
|
||||
const BackchannelLogoutEvent = "http://schemas.openid.net/event/backchannel-logout"
|
||||
|
||||
// logoutTarget 需要收到登出通知的应用
|
||||
type logoutTarget struct {
|
||||
ClientID string
|
||||
Endpoint string
|
||||
Sid string
|
||||
Identity uint64
|
||||
}
|
||||
|
||||
// NotifyBackchannelLogout 单点登出:通知该用户已登录的所有应用。
|
||||
//
|
||||
// 流程:查库收集目标应用(同步)→ 异步逐个 POST logout_token。
|
||||
// 各应用收到后应清除本地会话,否则用户在这边登出了,其他应用仍显示已登录。
|
||||
//
|
||||
// 注意:查库必须在请求上下文内同步完成(beego 全局 Ormer 不适合跨 goroutine 使用),
|
||||
// 异步部分只做 HTTP 通知,不再触碰数据库。
|
||||
func NotifyBackchannelLogout(identityID uint64) {
|
||||
targets, err := collectLogoutTargets(identityID)
|
||||
if err != nil || len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for _, t := range targets {
|
||||
if err := sendLogoutToken(t); err != nil {
|
||||
log.Printf("[auth] 单点登出通知失败 client=%s: %v", t.ClientID, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// collectLogoutTargets 收集该用户当前活跃会话涉及的应用(按 client_id 去重)
|
||||
func collectLogoutTargets(identityID uint64) ([]logoutTarget, error) {
|
||||
var sessions []models.AuthSession
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthSession)).
|
||||
Filter("identity_id", identityID).
|
||||
Filter("revoked", 0).
|
||||
All(&sessions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
targets := make([]logoutTarget, 0)
|
||||
for _, s := range sessions {
|
||||
if s.ClientID == "" || seen[s.ClientID] {
|
||||
continue
|
||||
}
|
||||
var client models.AuthClient
|
||||
if err := models.Orm.QueryTable(new(models.AuthClient)).
|
||||
Filter("client_id", s.ClientID).One(&client); err != nil {
|
||||
continue
|
||||
}
|
||||
if client.BackchannelLogoutURI == nil || strings.TrimSpace(*client.BackchannelLogoutURI) == "" {
|
||||
continue
|
||||
}
|
||||
seen[s.ClientID] = true
|
||||
targets = append(targets, logoutTarget{
|
||||
ClientID: s.ClientID,
|
||||
Endpoint: strings.TrimSpace(*client.BackchannelLogoutURI),
|
||||
Sid: s.Sid,
|
||||
Identity: identityID,
|
||||
})
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
// sendLogoutToken 按 OIDC Back-Channel Logout 规范发送 logout_token
|
||||
func sendLogoutToken(t logoutTarget) error {
|
||||
token, err := jwtutil.SignToken(jwtutil.TokenOptions{
|
||||
Alg: jwtutil.AlgRS256,
|
||||
UserID: int(t.Identity),
|
||||
Subject: fmt.Sprintf("%d", t.Identity),
|
||||
Audience: []string{t.ClientID},
|
||||
ClientID: t.ClientID,
|
||||
Sid: t.Sid,
|
||||
Jti: uuid.NewString(),
|
||||
Events: map[string]interface{}{BackchannelLogoutEvent: map[string]interface{}{}},
|
||||
TTL: 5 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("logout_token", token)
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.PostForm(t.Endpoint, form)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("应用返回状态码 %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user