滑动续期、网络错误归类、API Key 激活乐观锁与生效提示
This commit is contained in:
@@ -17,7 +17,71 @@
|
||||
`bumpTokenVersionTx`(或 `RevokeSessions`)使旧 JWT 失效,并按第 6 节携带
|
||||
`TokenProof`、接续操作者会话。因子写入与版本递增不得拆成两个独立提交。
|
||||
|
||||
## 4. 「至少一种登录方式」不变量
|
||||
滑动续期(`MaybeRenew`)是唯一的非敏感换发:剩余有效期 < `renewThreshold`
|
||||
且请求方法为 GET/HEAD 时,经 `X-Renewed-Token` / `X-Renewed-Expires-At`
|
||||
响应头换发**同 jti、同版本**新令牌并延长会话行。写请求不得预生成续期头:
|
||||
handler 可能撤销会话或递增版本,导致响应到达时头中令牌已经失效。续期不 bump
|
||||
版本、不新建会话行;新增登录方式不得绕过此约定自行签发长时令牌。
|
||||
|
||||
## 4. 场景:滑动续期响应头
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
已鉴权 GET/HEAD 请求携带的 JWT 剩余有效期低于 `renewThreshold` 时触发。
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
```go
|
||||
func (s *AuthService) MaybeRenew(
|
||||
ctx context.Context, token string,
|
||||
) (renewed string, expires time.Time, ok bool)
|
||||
```
|
||||
|
||||
成功时响应携带 `X-Renewed-Token` 与 RFC3339 格式的
|
||||
`X-Renewed-Expires-At`;客户端只在 HTTP 2xx 时应用。
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- 新 JWT 沿用原 `jti + Ver`,基础 TTL 仍为 24h。
|
||||
- 写响应不携带滑动续期头;敏感写接口以响应体中的新会话为准。
|
||||
- `user_sessions.expires_at` 更新成功后才能返回 `ok=true`;数据库失败不得发头。
|
||||
- 无会话行的兼容期旧 JWT 可续期,但不得新建会话行。
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
| 条件 | 结果 |
|
||||
| --- | --- |
|
||||
| GET/HEAD 且剩余 <12h、持久化成功 | 返回双响应头 |
|
||||
| GET/HEAD 且剩余 ≥12h | 无续期头 |
|
||||
| POST/PUT/PATCH/DELETE | 无续期头 |
|
||||
| JWT 解析或会话延期失败 | `ok=false`,无续期头 |
|
||||
| 非 2xx 响应意外携带续期头 | 客户端忽略,不写 Store |
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good:列表 GET 续期后,会话行过期时间与新 JWT 对齐。
|
||||
- Base:普通写请求由原 JWT 完成,不做滑动续期。
|
||||
- Bad:在 `c.Next()` 前为写请求签头,handler 随后 bump 版本或 Logout。
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- 路由测试断言临期 GET 返回头、临期敏感 POST 不返回头。
|
||||
- 服务测试先把会话行缩短,再断言续期确实延长该行。
|
||||
- 注入数据库更新失败,断言 Token/expiry 为空且 `ok=false`。
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
```go
|
||||
// Wrong:所有鉴权请求都预生成续期头
|
||||
maybeRenewToken(c, auth, token)
|
||||
|
||||
// Correct:只读请求才允许滑动续期
|
||||
if allowsSessionRenewal(c.Request.Method) {
|
||||
maybeRenewToken(c, auth, token)
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 「至少一种登录方式」不变量
|
||||
|
||||
密码登录禁用要求至少一种在当前 `EffectiveAppURL` 下**可实际登录**的免密方式
|
||||
(`usablePasswordlessTx`);地址为空时 Passkey、钱包和 OAuth 均不可用。禁用期间移除
|
||||
@@ -28,11 +92,11 @@
|
||||
- **复用 `UserIdentity` 表**(如钱包,provider+subject 唯一)→ 走 `identityCountTx` 自动参与,零改动;
|
||||
- **独立表**(如 Passkey 的 `UserPasskey`)→ 参照 `passkeyCountTx` 增加计数入口,并在开启门槛(`hasAnyPasswordless`)与两个 `ensureNot*` 检查里对称接入;api 层把 `ErrLastIdentity` 映射 409。
|
||||
|
||||
## 5. 挑战/状态的一次性消费
|
||||
## 6. 挑战/状态的一次性消费
|
||||
|
||||
挑战-响应类流程(OAuth state、WebAuthn challenge、SIWE nonce)一律进程内 `map + mutex + TTL + gc`,取用即删(参考 `oauthPending` / `passkeyPending` / `walletPending`);不落库、不引 Redis。TTL 5-10 分钟。
|
||||
|
||||
## 6. 场景:已鉴权敏感事务的提交屏障
|
||||
## 7. 场景:已鉴权敏感事务的提交屏障
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
|
||||
Reference in New Issue
Block a user