package server import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strings" "time" "unicode" "github.com/gin-gonic/gin" ) type administratorSession struct { ID string `json:"-" db:"id"` Key string `json:"key" db:"-"` CreatedAt *time.Time `json:"createdAt" db:"created_at"` ExpiresAt time.Time `json:"expiresAt" db:"expires_at"` ClientIP string `json:"clientIp" db:"client_ip"` UserAgent string `json:"userAgent" db:"user_agent"` Current bool `json:"current" db:"-"` } // A management handle must never disclose the JWT's session identifier. func (s *Server) administratorSessionKey(id string) string { mac := hmac.New(sha256.New, []byte(s.Config.JWTSecret)) mac.Write([]byte("vdi-admin-session-management\x00" + id)) return hex.EncodeToString(mac.Sum(nil)) } func sessionUserAgent(raw string) string { runes := []rune(strings.Map(func(r rune) rune { if unicode.IsControl(r) { return ' ' } return r }, raw)) if len(runes) > 500 { runes = runes[:500] } return string(runes) } func (s *Server) administratorSessions(c *gin.Context) { tx, _ := s.accountTransaction(c, true) if tx == nil { return } defer tx.Rollback() var username string if err := tx.GetContext(c.Request.Context(), &username, "SELECT username FROM administrators WHERE id=$1", c.Param("id")); err != nil { s.dbError(c, err) return } items := []administratorSession{} err := tx.SelectContext(c.Request.Context(), &items, `SELECT x.id,x.expires_at,d.created_at,COALESCE(d.client_ip,'') client_ip,COALESCE(d.user_agent,'') user_agent FROM admin_sessions x LEFT JOIN admin_session_details d ON d.session_id=x.id WHERE x.admin_id=$1 AND x.expires_at>now() ORDER BY x.expires_at DESC,x.id DESC`, c.Param("id")) if err != nil { s.dbError(c, err) return } for i := range items { items[i].Key = s.administratorSessionKey(items[i].ID) items[i].Current = items[i].ID == c.GetString("sessionId") } list(c, items) } func (s *Server) revokeAdministratorSession(c *gin.Context) { key := c.Param("sessionKey") if len(key) != 64 { s.fail(c, 404, "not_found", "登录会话不存在或已失效") return } tx, actor := s.accountTransaction(c, true) if tx == nil { return } defer tx.Rollback() ctx := c.Request.Context() var username string if err := tx.GetContext(ctx, &username, "SELECT username FROM administrators WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil { s.dbError(c, err) return } var ids []string if err := tx.SelectContext(ctx, &ids, "SELECT id FROM admin_sessions WHERE admin_id=$1 AND expires_at>now()", c.Param("id")); err != nil { s.dbError(c, err) return } for _, id := range ids { if !hmac.Equal([]byte(s.administratorSessionKey(id)), []byte(key)) { continue } if id == c.GetString("sessionId") { s.fail(c, 409, "current_session_protected", "当前登录请使用退出登录入口") return } result, err := tx.ExecContext(ctx, "DELETE FROM admin_sessions WHERE id=$1 AND admin_id=$2 AND expires_at>now()", id, c.Param("id")) if err != nil { s.dbError(c, err) return } count, err := result.RowsAffected() if err != nil { s.dbError(c, err) return } if count == 0 { break } err = accountAudit(ctx, tx, actor.Username, "revoke_administrator_session", username+":"+key, c.GetString("requestId")) if err == nil { err = tx.Commit() } if err != nil { s.dbError(c, err) return } c.Status(204) return } s.fail(c, 404, "not_found", "登录会话不存在或已失效") }