Files
jiu/backend/internal/service/session_cleanup.go
wangjia e41085a878 feat(backend): 会话安全加固 + 授权实时 phase + 首次使用自动试用
会话安全(jti 轮换 / 重用检测 / 改密吊销 / 禁用即时下线 / 清理 / 失败登录落库):
- refresh token 轮换 jti + token-family 重用检测,旧 token 重放即吊销整条会话
- 改密码、停用用户即时吊销其全部活跃会话(revoked_by 审计)
- 中间件 session JOIN user 校验,禁用/删除用户带 token 请求返回 401 USER_DISABLED
- 新增 login_attempts 失败登录落库 + 会话保留期清理 goroutine

授权实时 phase + 心跳回带:
- LicenseGuard 改为按当前 DB 实时计算 phase(30s 每店缓存),续费/过期/被改 ~30s 内对写操作生效,无需重登
- /auth/ping 回带授权概况(ShopInfoView,与 /license/info 同构),客户端一次心跳即刷新横幅/门禁

首次使用自动试用 + code-review 修复:
- 门店首次登录/续期无有效授权时自动签发 30 天 trial(快路径无锁 Count,仅首用走 FOR UPDATE 事务)
- ShopInfo 区分「确无授权」与瞬时 DB 错误,避免误降级
- trial 签发后改为在事务提交后再失效 phase 缓存(修复早于提交的竞态)
- 存量无 sid token 续期纳入显式上限,legacy 会话不再游离于并发配额之外

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 07:34:04 +08:00

56 lines
1.7 KiB
Go

package service
import (
"log"
"time"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/internal/model"
)
// sessionCleanupInterval 清理任务执行周期。
const sessionCleanupInterval = 24 * time.Hour
// StartSessionCleanup 启动后台清理 goroutine:启动即跑一次,之后每 24h 跑一次。
// 删除已撤销/已过期的会话行与过旧的失败登录记录,防止表无限膨胀、IP/UA 长期滞留。
// retentionDays<=0 时视为关闭清理(直接返回,不启动 goroutine)。
func StartSessionCleanup(db *gorm.DB, retentionDays int) {
if retentionDays <= 0 {
log.Printf("[cleanup] session cleanup disabled (retention_days=%d)", retentionDays)
return
}
go func() {
cleanupOnce(db, retentionDays)
ticker := time.NewTicker(sessionCleanupInterval)
defer ticker.Stop()
for range ticker.C {
cleanupOnce(db, retentionDays)
}
}()
}
// cleanupOnce 执行一轮清理,返回各表删除行数(供测试断言)。
func cleanupOnce(db *gorm.DB, retentionDays int) (sessions, attempts int64) {
cutoff := time.Now().AddDate(0, 0, -retentionDays)
r1 := db.Where(
"(revoked_at IS NOT NULL AND revoked_at < ?) OR (refresh_exp_at IS NOT NULL AND refresh_exp_at < ?)",
cutoff, cutoff,
).Delete(&model.UserSession{})
if r1.Error != nil {
log.Printf("[cleanup] purge user_sessions failed: %v", r1.Error)
}
r2 := db.Where("created_at < ?", cutoff).Delete(&model.LoginAttempt{})
if r2.Error != nil {
log.Printf("[cleanup] purge login_attempts failed: %v", r2.Error)
}
if r1.RowsAffected > 0 || r2.RowsAffected > 0 {
log.Printf("[cleanup] purged %d sessions, %d login_attempts (older than %dd)",
r1.RowsAffected, r2.RowsAffected, retentionDays)
}
return r1.RowsAffected, r2.RowsAffected
}