e41085a878
会话安全(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>
56 lines
1.7 KiB
Go
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
|
|
}
|