From e4ec85c17677d24e439b0bb84c66751e4c5f9b98 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Thu, 12 Mar 2026 02:47:32 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20clearAllSessions=20=E6=94=B9=E7=94=A8=20?= =?UTF-8?q?SCAN=20=E6=9F=A5=E6=89=BE=20session=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=B8=85=E7=90=86=E6=97=A0=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:登录时只写了 auth:session_id:{sessionId} (Redis SET), 从未写入 auth:user_sessions:{userId} sorted set, 导致 clearAllSessions 用 ZRange 永远返回空,session 根本没被清除。 修复:改用 SCAN auth:session_id:* 遍历所有 session key, 按 value 匹配 userId 找出该用户的全部 session 后删除, 同时清理关联的 device cache key。 Co-Authored-By: claude-flow --- .../user/kickOfflineByUserDeviceLogic.go | 87 ++++++++++++---- .../logic/public/user/unbindDeviceLogic.go | 98 +++++++++++++------ 2 files changed, 137 insertions(+), 48 deletions(-) diff --git a/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go b/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go index abd02a1..fe7472d 100644 --- a/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go +++ b/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go @@ -3,6 +3,8 @@ package user import ( "context" "fmt" + "strconv" + "strings" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" @@ -46,33 +48,78 @@ func (l *KickOfflineByUserDeviceLogic) KickOfflineByUserDevice(req *types.KickOf return nil } -// clearAllSessions 清除指定用户的所有会话 +// clearAllSessions 清除指定用户的所有会话(通过 SCAN 查找,不依赖 sorted set) func (l *KickOfflineByUserDeviceLogic) clearAllSessions(userId int64) { - sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) - sessions, err := l.svcCtx.Redis.ZRange(l.ctx, sessionsKey, 0, -1).Result() - if err != nil { - l.Errorw("获取用户会话列表失败", - logger.Field("user_id", userId), - logger.Field("error", err.Error()), - ) - return - } + sessionSet := make(map[string]struct{}) - if len(sessions) == 0 { - return - } - - pipe := l.svcCtx.Redis.TxPipeline() - for _, sessionID := range sessions { - if sessionID == "" { - continue + userIDText := strconv.FormatInt(userId, 10) + pattern := fmt.Sprintf("%s:*", config.SessionIdKey) + var cursor uint64 + for { + keys, nextCursor, scanErr := l.svcCtx.Redis.Scan(l.ctx, cursor, pattern, 200).Result() + if scanErr != nil { + l.Errorw("扫描会话键失败", logger.Field("user_id", userId), logger.Field("error", scanErr.Error())) + break } + for _, sessionKey := range keys { + value, getErr := l.svcCtx.Redis.Get(l.ctx, sessionKey).Result() + if getErr != nil || value != userIDText { + continue + } + sessionID := strings.TrimPrefix(sessionKey, config.SessionIdKey+":") + if sessionID == "" || strings.HasPrefix(sessionID, "detail:") { + continue + } + sessionSet[sessionID] = struct{}{} + } + cursor = nextCursor + if cursor == 0 { + break + } + } + + deviceKeySet := make(map[string]struct{}) + devicePattern := fmt.Sprintf("%s:*", config.DeviceCacheKeyKey) + cursor = 0 + for { + keys, nextCursor, scanErr := l.svcCtx.Redis.Scan(l.ctx, cursor, devicePattern, 200).Result() + if scanErr != nil { + l.Errorw("扫描设备会话映射失败", logger.Field("user_id", userId), logger.Field("error", scanErr.Error())) + break + } + for _, deviceKey := range keys { + sessionID, getErr := l.svcCtx.Redis.Get(l.ctx, deviceKey).Result() + if getErr != nil { + continue + } + if _, exists := sessionSet[sessionID]; exists { + deviceKeySet[deviceKey] = struct{}{} + } + } + cursor = nextCursor + if cursor == 0 { + break + } + } + + if len(sessionSet) == 0 { + return + } + + sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) + pipe := l.svcCtx.Redis.TxPipeline() + for sessionID := range sessionSet { pipe.Del(l.ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sessionID)) pipe.Del(l.ctx, fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sessionID)) + pipe.ZRem(l.ctx, sessionsKey, sessionID) } pipe.Del(l.ctx, sessionsKey) - if _, err = pipe.Exec(l.ctx); err != nil { + for deviceKey := range deviceKeySet { + pipe.Del(l.ctx, deviceKey) + } + + if _, err := pipe.Exec(l.ctx); err != nil { l.Errorw("清理会话缓存失败", logger.Field("user_id", userId), logger.Field("error", err.Error()), @@ -81,6 +128,6 @@ func (l *KickOfflineByUserDeviceLogic) clearAllSessions(userId int64) { l.Infow("[KickOffline] 管理员踢设备-清除所有Session", logger.Field("user_id", userId), - logger.Field("count", len(sessions)), + logger.Field("count", len(sessionSet)), ) } diff --git a/internal/logic/public/user/unbindDeviceLogic.go b/internal/logic/public/user/unbindDeviceLogic.go index 327ae9e..9e32057 100644 --- a/internal/logic/public/user/unbindDeviceLogic.go +++ b/internal/logic/public/user/unbindDeviceLogic.go @@ -3,6 +3,8 @@ package user import ( "context" "fmt" + "strconv" + "strings" "time" "github.com/perfect-panel/server/internal/config" @@ -202,15 +204,8 @@ func (l *UnbindDeviceLogic) logoutUnbind(userInfo *user.User, device *user.Devic } } - // 6. Kick 设备 + 更新在线状态 + // 6. Kick 设备(关闭 WebSocket,客户端会收到 kicked 消息) l.svcCtx.DeviceManager.KickDevice(device.UserId, device.Identifier) - device.Online = false - if updateErr := l.svcCtx.UserModel.UpdateDevice(l.ctx, device); updateErr != nil { - l.Errorw("update device online status failed", - logger.Field("device_id", device.Id), - logger.Field("error", updateErr.Error()), - ) - } // 7. 清除该用户所有 session(旧 token 全部失效) l.clearAllSessions(userInfo.Id) @@ -266,33 +261,80 @@ func (l *UnbindDeviceLogic) collectFamilyMemberIDs(userID int64) []int64 { return memberIDs } -// clearAllSessions 清除指定用户的所有会话 +// clearAllSessions 清除指定用户的所有会话(通过 SCAN 查找,不依赖 sorted set) func (l *UnbindDeviceLogic) clearAllSessions(userId int64) { - sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) - sessions, err := l.svcCtx.Redis.ZRange(l.ctx, sessionsKey, 0, -1).Result() - if err != nil { - l.Errorw("获取用户会话列表失败", - logger.Field("user_id", userId), - logger.Field("error", err.Error()), - ) - return - } + sessionSet := make(map[string]struct{}) - if len(sessions) == 0 { - return - } - - pipe := l.svcCtx.Redis.TxPipeline() - for _, sessionID := range sessions { - if sessionID == "" { - continue + // SCAN 所有 session key,找出属于该用户的 + userIDText := strconv.FormatInt(userId, 10) + pattern := fmt.Sprintf("%s:*", config.SessionIdKey) + var cursor uint64 + for { + keys, nextCursor, scanErr := l.svcCtx.Redis.Scan(l.ctx, cursor, pattern, 200).Result() + if scanErr != nil { + l.Errorw("扫描会话键失败", logger.Field("user_id", userId), logger.Field("error", scanErr.Error())) + break } + for _, sessionKey := range keys { + value, getErr := l.svcCtx.Redis.Get(l.ctx, sessionKey).Result() + if getErr != nil || value != userIDText { + continue + } + sessionID := strings.TrimPrefix(sessionKey, config.SessionIdKey+":") + if sessionID == "" || strings.HasPrefix(sessionID, "detail:") { + continue + } + sessionSet[sessionID] = struct{}{} + } + cursor = nextCursor + if cursor == 0 { + break + } + } + + // SCAN 设备缓存 key,找出关联的 + deviceKeySet := make(map[string]struct{}) + devicePattern := fmt.Sprintf("%s:*", config.DeviceCacheKeyKey) + cursor = 0 + for { + keys, nextCursor, scanErr := l.svcCtx.Redis.Scan(l.ctx, cursor, devicePattern, 200).Result() + if scanErr != nil { + l.Errorw("扫描设备会话映射失败", logger.Field("user_id", userId), logger.Field("error", scanErr.Error())) + break + } + for _, deviceKey := range keys { + sessionID, getErr := l.svcCtx.Redis.Get(l.ctx, deviceKey).Result() + if getErr != nil { + continue + } + if _, exists := sessionSet[sessionID]; exists { + deviceKeySet[deviceKey] = struct{}{} + } + } + cursor = nextCursor + if cursor == 0 { + break + } + } + + if len(sessionSet) == 0 { + return + } + + sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) + pipe := l.svcCtx.Redis.TxPipeline() + for sessionID := range sessionSet { pipe.Del(l.ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sessionID)) pipe.Del(l.ctx, fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sessionID)) + pipe.ZRem(l.ctx, sessionsKey, sessionID) } pipe.Del(l.ctx, sessionsKey) - if _, err = pipe.Exec(l.ctx); err != nil { + for deviceKey := range deviceKeySet { + pipe.Del(l.ctx, deviceKey) + } + + if _, err := pipe.Exec(l.ctx); err != nil { l.Errorw("清理会话缓存失败", logger.Field("user_id", userId), logger.Field("error", err.Error()), @@ -301,7 +343,7 @@ func (l *UnbindDeviceLogic) clearAllSessions(userId int64) { l.Infow("退出登录-清除所有Session", logger.Field("user_id", userId), - logger.Field("count", len(sessions)), + logger.Field("count", len(sessionSet)), ) }