Files
mce/internal/session/memory_store.go
Victor_Jay d203447eb3 feat: 设置页新增登录管理 TAB
- Session 结构体新增 IP、UserAgent、Remark 字段,登录/注册时记录设备信息
- Store 接口新增 ListByUID、DeleteByUIDExclude 方法,MemoryStore 完整实现
- SessionManager 新增 ListByUID、DestroyOtherByUID、UpdateRemark 方法
- 新增 UA 轻量解析器(session/ua.go),提取浏览器/OS/设备类型
- 新增 settings_api_sessions.go,提供列表/踢出/一键踢出/备注 4 个 API
- 控制器层声明 sessionManager 最小接口(ISP),SettingsController 注入依赖
- Auth 中间件注入 sid 到 gin context,支持识别当前会话
- 设置页左侧导航增加登录管理入口,tab 白名单新增 sessions
- 前端实现设备列表展示(浏览器/OS/设备图标/IP/时间)、当前设备标识、备注输入、踢出按钮、一键踢出
- settings.css 新增会话管理全套样式及响应式适配
2026-05-30 23:58:37 +08:00

197 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package session
import (
"log"
"sync"
"time"
)
// MemoryStore 基于内存的会话存储sync.Map + RWMutex适合单实例部署
type MemoryStore struct {
mu sync.RWMutex
sessions map[string]*Session // sid → session
byUID map[uint][]string // uid → []sid用于批量删除
idleTimeout time.Duration // 不记住我:空闲超时
rememberTimeout time.Duration // 记住我:空闲超时
}
// NewMemoryStore 创建内存存储实例
func NewMemoryStore(idleTimeout, rememberTimeout time.Duration) *MemoryStore {
return &MemoryStore{
sessions: make(map[string]*Session),
byUID: make(map[uint][]string),
idleTimeout: idleTimeout,
rememberTimeout: rememberTimeout,
}
}
// Get 获取会话(根据 RememberMe 选择对应的空闲超时检查过期)
func (ms *MemoryStore) Get(id string) (*Session, error) {
ms.mu.RLock()
s, ok := ms.sessions[id]
ms.mu.RUnlock()
if !ok {
return nil, nil
}
// 记住我 → 使用更长的超时
timeout := ms.idleTimeout
if s.RememberMe {
timeout = ms.rememberTimeout
}
if s.IsExpired(timeout) {
ms.Delete(id)
return nil, nil
}
return s, nil
}
// Set 写入会话(同步更新 byUID 索引)
func (ms *MemoryStore) Set(s *Session) error {
ms.mu.Lock()
defer ms.mu.Unlock()
// 如果 sid 已存在且 uid 不同,先从旧 uid 索引移除
if old, ok := ms.sessions[s.ID]; ok && old.UserID != s.UserID {
ms.removeFromUIDIndex(old.UserID, s.ID)
}
ms.sessions[s.ID] = s
ms.byUID[s.UserID] = append(ms.byUID[s.UserID], s.ID)
return nil
}
// Delete 删除会话(同步更新 byUID 索引)
func (ms *MemoryStore) Delete(id string) error {
ms.mu.Lock()
defer ms.mu.Unlock()
s, ok := ms.sessions[id]
if !ok {
return nil
}
ms.removeFromUIDIndex(s.UserID, id)
delete(ms.sessions, id)
return nil
}
// DeleteByUID 删除某用户的所有会话
func (ms *MemoryStore) DeleteByUID(uid uint) error {
ms.mu.Lock()
defer ms.mu.Unlock()
sids, ok := ms.byUID[uid]
if !ok {
return nil
}
for _, sid := range sids {
delete(ms.sessions, sid)
}
delete(ms.byUID, uid)
log.Printf("[MemoryStore] DeleteByUID uid=%d count=%d", uid, len(sids))
return nil
}
// DeleteByUIDExclude 删除某用户的所有会话,但保留 excludeSID
func (ms *MemoryStore) DeleteByUIDExclude(uid uint, excludeSID string) error {
ms.mu.Lock()
defer ms.mu.Unlock()
sids, ok := ms.byUID[uid]
if !ok {
return nil
}
var kept []string
var removed int
for _, sid := range sids {
if sid == excludeSID {
kept = append(kept, sid)
continue
}
delete(ms.sessions, sid)
removed++
}
if len(kept) > 0 {
ms.byUID[uid] = kept
} else {
delete(ms.byUID, uid)
}
log.Printf("[MemoryStore] DeleteByUIDExclude uid=%d removed=%d kept=%d", uid, removed, len(kept))
return nil
}
// ListByUID 列出某用户的所有活跃会话(已过期的会被清理掉)
func (ms *MemoryStore) ListByUID(uid uint) ([]*Session, error) {
ms.mu.RLock()
sids, ok := ms.byUID[uid]
if !ok {
ms.mu.RUnlock()
return nil, nil
}
// 复制 sid 列表避免持锁期间修改
sidCopy := make([]string, len(sids))
copy(sidCopy, sids)
ms.mu.RUnlock()
var result []*Session
for _, sid := range sidCopy {
// Get 会检查过期并自动清理
s, err := ms.Get(sid)
if err != nil || s == nil {
continue
}
result = append(result, s)
}
return result, nil
}
// Cleanup 清理过期会话(根据 RememberMe 选择对应超时)
func (ms *MemoryStore) Cleanup() int {
ms.mu.Lock()
defer ms.mu.Unlock()
var expiredSids []string
for sid, s := range ms.sessions {
timeout := ms.idleTimeout
if s.RememberMe {
timeout = ms.rememberTimeout
}
if s.IsExpired(timeout) {
expiredSids = append(expiredSids, sid)
}
}
for _, sid := range expiredSids {
s := ms.sessions[sid]
ms.removeFromUIDIndex(s.UserID, sid)
delete(ms.sessions, sid)
}
if len(expiredSids) > 0 {
log.Printf("[MemoryStore] Cleanup removed=%d remaining=%d", len(expiredSids), len(ms.sessions))
}
return len(expiredSids)
}
// removeFromUIDIndex 从 byUID 索引中移除指定 sid调用者需持有写锁
func (ms *MemoryStore) removeFromUIDIndex(uid uint, sid string) {
sids := ms.byUID[uid]
for i, s := range sids {
if s == sid {
ms.byUID[uid] = append(sids[:i], sids[i+1:]...)
break
}
}
if len(ms.byUID[uid]) == 0 {
delete(ms.byUID, uid)
}
}
// StartCleanup 启动后台清理 goroutine应在 main 中调用)
func (ms *MemoryStore) StartCleanup(interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
ms.Cleanup()
}
}()
}