Skip to content

feat(auth): refresh token revoked 行复用信号(日志+指标) - #2188

Merged
DeliciousBuding merged 1 commit into
masterfrom
fix/refresh-reuse-signal
Sep 1, 2026
Merged

feat(auth): refresh token revoked 行复用信号(日志+指标)#2188
DeliciousBuding merged 1 commit into
masterfrom
fix/refresh-reuse-signal

Conversation

@DeliciousBuding

Copy link
Copy Markdown
Collaborator

Summary

Service.RefreshToken 命中 已撤销(Revoked==true) 的 refresh token 行时,从静默拒绝升级为「拒绝 + 可观测信号」:slog.Warn(仅 user_id/device_type 维度,不记录 token 本体或 hash)+ 新 plain Counter refresh_token_reuse_total(按 #1441 nil-guard 既有模式在 metrics.Register() 注册,紧邻 G9 RefreshBlacklistCheckErrors)。响应错误码不变(仍 AuthRefreshInvalid),轮换逻辑不动。已撤销行被再次出示是 refresh 链路上唯一可在 DB 侧检测的复用信号(如登出后被窃令牌重放)。

取证(复核属实)

  1. hub-server/internal/repository/refresh_token.go UpsertRefreshTokenON CONFLICT (user_id, device_type, device_id) DO UPDATE token_hash/expires_at/revoked 覆盖旧 hash → 轮换后旧 token 复用在 DB 侧结构性不可检测(本轮不改,见「已知限制」)。
  2. hub-server/internal/service/auth/service.go:55-57(改前):查无 → 静默 AuthRefreshInvalid;查到但 Revoked==true 或过期 → 同一 if 静默拒绝,无日志区分、无指标——复用与正常过期失败在运维面完全不可分辨。
  3. 测试基建:internal/service/auth/ 已有 newMockDB(sqlmock)+ miniredis + testutil.ToFloat64 计数器断言先例(refresh_fail_closed_redis_test.go TestRefreshBlacklistCheckErrorsCounter),新测试完全仿写。

Scope 边界

  • 无级联吊销:命中撤销行只出信号,不触碰该用户任何其它令牌(级联吊销是独立决策项,本轮严禁,见「后续决策项」)。
  • 无 schema 变更:不动 refresh_tokens 表、不动 upsert 语义。
  • 无响应语义变更:客户端仍收到原 AuthRefreshInvalid
  • 不发明新体系:复核确认 auth service/handler 路径无既有 audit 事件约定(service/authhandler/auth.go 零 audit 引用),故只做 warn+指标。
  • 拆分原 Revoked || expired 合并分支:过期(未撤销)路径保持静默、不计入复用计数器(有测试锁定特异性)。

测试证据

新增 hub-server/internal/service/auth/refresh_reuse_signal_test.go(2 用例):

  • TestRefreshToken_RevokedReuseSignal:登出(Logout → revoke UPDATE)后复用同一令牌 → AuthRefreshInvalid 拒绝 + refresh_token_reuse_total 恰 +1 + 无级联(sqlmock 期望在复用查询后耗尽,任何针对该用户其它令牌行的吊销 UPDATE 会以 unexpected Exec 使 ExpectationsWereMet 失败)。
  • TestRefreshToken_ExpiredNoReuseSignal:过期未撤销 → 同错误码但计数器 +0(信号特异性)。

metrics_test.go 按既有惯例补 RefreshTokenReuseTotal nil 检查 + refresh_token_reuse_total 注册断言。

门禁(本地全绿):go vet ./...(hub-server)、go test ./internal/service/auth/ ./internal/repository/ ./internal/handler/ ./internal/metrics/make test(test-edge + test-hub 全量)、verify-doc-ssot.pygit diff --checkverify-ci-gates.pyverify-openapi-contract.pyverify-migration-idempotency.pyverify-test-sleep-ratchet.pyverify-conventions.pyverify-hub-pure-packages.pyverify-hub-layering.pyverify-i18n-deadkeys.pyverify-commit-messages.sh origin/master HEAD

注:本地 verify-hub-lint-ratchet.py 报 gosec G101 findings,在未改动的 master(a91d071,主检出)上同样复现且条数更多(本机 gosec/revive 版本漂移,finding 集不稳定,与本 diff 无关——本 diff 未触碰任何被报文件),以 CI 固定工具链为准。

已知限制(不改,登记在案)

轮换走 UpsertRefreshToken ON CONFLICT DO UPDATE token_hash:同一设备再登录即覆盖旧 hash,轮换后旧 token 的复用在 DB 侧结构性不可检测(仅轮换时写入的 Redis 黑名单在 TTL 窗口内兜底)。本信号只覆盖「登出/显式撤销后行仍存在且被重放」这一可检测子集。彻底修复需 schema 变更(保留旧 hash 或独立复用事件表),属决策项,不在本轮。

后续决策项

  • 级联吊销:命中撤销行时是否吊销该用户全部/同设备族令牌(本轮严禁实现;需先决策误报代价——共享设备、客户端重试风暴下的自锁风险)。
  • 轮换后可检测性:保留旧 hash 的结构性方案(与上一项合并决策)。

关联:#2154 F2 低风险步①。

…_total 计数器)

#2154 F2 低风险步①:只加信号,不做级联吊销(级联是决策项)。

- service/auth RefreshToken:Revoked 行命中时 slog.Warn(user_id/device_type
  维度,不记录 token 本体或 hash)+ refresh_token_reuse_total 计数器,
  响应语义不变(仍 AuthRefreshInvalid),轮换逻辑不动。
- metrics:新 plain Counter 按既有 #1441 nil-guard 模式注册(G9 旁)。
- 测试:登出后复用 → 拒绝 + 计数器恰 +1 + 无级联(sqlmock 期望耗尽证明
  该用户其它令牌未被吊销);过期未撤销路径不计入(信号特异性)。
- 已知限制(不改):轮换走 UpsertRefreshToken ON CONFLICT 覆盖旧 hash,
  轮换后旧 token 复用在 DB 侧结构性不可检测;本信号只覆盖登出撤销行。

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
@DeliciousBuding
DeliciousBuding merged commit e070b26 into master Sep 1, 2026
38 checks passed
@DeliciousBuding
DeliciousBuding deleted the fix/refresh-reuse-signal branch September 1, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant