滑动续期、网络错误归类、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
|
||||
|
||||
|
||||
@@ -42,6 +42,16 @@ func sanitizeURLError(err error) error {
|
||||
|
||||
**约定**:识别特定语义一律走 `internal/oci/errors.go` 的判定函数(`IsModelUnavailable` / `IsEntityNotFound` / `IsOnDemandUnsupported`),用 `errors.As` 取 `common.ServiceError` 后按 状态码+消息片段 匹配;调用方(探测/网关路由)据此决定「定论、换候选、换渠道、是否计熔断」,不得在业务层散落字符串匹配。
|
||||
|
||||
连接层失败(拿不到 OCI 响应)与服务错误分开归类:`UpstreamNetworkError`
|
||||
识别代理拨号 / SOCKS 握手 / 超时等网络类错误,api 层 `respondError` 据此回
|
||||
502(`code: UpstreamNetwork` + 中文 hint),不落 500「服务器内部错误」;
|
||||
url.Error 的 URL 一律压缩为主机名(防 userinfo / 长查询串外流,呼应上文
|
||||
敏感信息约定)。脱敏必须覆盖嵌套 `url.Error` 及普通拨号错误文本中的
|
||||
`userinfo@host`;`[NET]` 日志记录与 502 响应相同的脱敏详情,不得再打印原始
|
||||
错误链。代理配置的独立 Host 字段拒绝 `@`,用户名/密码只走专用字段。
|
||||
新增网络类特征时扩这个判定函数,不要在 handler 里散判。测试同时断言响应体
|
||||
与捕获日志均不含用户名、密码,并保留一条存量非法 Host 的普通错误文本用例。
|
||||
|
||||
## 外部响应体必须限长,超限报错而非静默截断
|
||||
|
||||
读上游/外部响应体一律 `io.LimitReader(max+1)` 再判长度:恰好读到 max+1 说明超限,返回带上限值的错误;不得截断后当成功继续(截断的 JSON/流式响应会以 200 返回坏数据,2026-07-22 审查 #13,genai_responses.go `readCompatBody`)。
|
||||
|
||||
Reference in New Issue
Block a user