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>
This commit is contained in:
wangjia
2026-06-19 07:34:04 +08:00
parent 2d84bda99a
commit e41085a878
23 changed files with 1248 additions and 74 deletions
+20 -1
View File
@@ -67,10 +67,12 @@ CREATE TABLE IF NOT EXISTS `user_sessions` (
`platform_class` VARCHAR(20) DEFAULT NULL COMMENT 'desktop|mobile|web',
`ip` VARCHAR(64) DEFAULT NULL,
`user_agent` VARCHAR(512) DEFAULT NULL,
`refresh_jti` VARCHAR(64) DEFAULT NULL COMMENT '当前有效 refresh token 的 jti,用于轮换+重用检测',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`revoked_at` DATETIME DEFAULT NULL,
`revoked_reason` VARCHAR(30) DEFAULT NULL COMMENT 'kicked|logout|admin|disabled',
`revoked_reason` VARCHAR(30) DEFAULT NULL COMMENT 'kicked|logout|admin|disabled|reuse|pwd_reset',
`revoked_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '吊销操作人 user_id;系统/自助吊销为 NULL',
`refresh_exp_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_session_sid` (`sid`),
@@ -79,6 +81,23 @@ CREATE TABLE IF NOT EXISTS `user_sessions` (
KEY `idx_session_revoked` (`revoked_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录会话';
-- ------------------------------------------------------------
-- 登录尝试审计(当前只记失败尝试,用于风控/排查;由保留期清理任务定期删旧)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `login_attempts` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`shop_code` VARCHAR(64) DEFAULT NULL,
`username` VARCHAR(50) DEFAULT NULL,
`ip` VARCHAR(64) DEFAULT NULL,
`user_agent` VARCHAR(512) DEFAULT NULL,
`success` TINYINT(1) NOT NULL DEFAULT 0,
`reason` VARCHAR(40) DEFAULT NULL COMMENT 'invalid_shop|invalid_user|bad_password|locked|inactive|platform_not_allowed',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_attempt_user` (`username`, `created_at`),
KEY `idx_attempt_ip` (`ip`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录尝试审计';
-- ------------------------------------------------------------
-- 许可证
-- ------------------------------------------------------------