feat(auth): refresh token 续期(B2) — 不透明哈希+单次旋转+/auth/refresh+/auth/logout
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- auth/refresh.go: RefreshStore(Issue/Rotate/Revoke/RevokeAll/RevokeByUser), SHA-256哈希256-bit随机串, 单次旋转事务(并发重放fail-closed)
- 迁移0008_refresh_tokens(id/user_id/role/token_hash UNIQUE/expires_at/revoked_at + 索引)
- gateway: POST /v1/auth/refresh(旋转/续期) + /v1/auth/logout(单/全设备); login响应增refreshToken
- config: AUTH_REFRESH_TTL(720h)/AUTH_REFRESH_ROTATE_DISABLED; decode.go 支持time.Duration
- rotate-jwt成功后RevokeAll(响应refreshTokensRevoked), revoke-keys不动refresh
- 14测试+真实二进制e2e, build/vet/test绿+交叉编译
- Docs/api-client v0.25 + api-reference §2.3/2.4 同步
This commit is contained in:
AskaEth committed 2026-09-08 21:45:00 +08:00
1 parent 7ecb43bd5d
commit 026b27b310
17 files changed
+1237 -13

No files matched your search

+21
View File
@@ -6,6 +6,7 @@ import (
"reflect"
"strconv"
"strings"
"time"
)
// decodeValue populates the struct pointed to by v. Precedence, highest first:
@@ -73,6 +74,11 @@ func setField(fv reflect.Value, raw string) error {
case reflect.Bool:
return setBool(fv, raw)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
// time.Duration is an int64 kind but is configured as a Go duration
// string (e.g. "720h"), so it must be parsed before the integer path.
if fv.Type() == reflect.TypeOf(time.Duration(0)) {
return setDuration(fv, raw)
}
return setInt(fv, raw)
case reflect.Float32, reflect.Float64:
return setFloat(fv, raw)
@@ -122,6 +128,21 @@ func setFloat(fv reflect.Value, raw string) error {
return nil
}
// setDuration parses a Go duration string (e.g. "720h", "30m") into a
// time.Duration field. An empty value sets the zero duration.
func setDuration(fv reflect.Value, raw string) error {
if strings.TrimSpace(raw) == "" {
fv.SetInt(0)
return nil
}
d, err := time.ParseDuration(raw)
if err != nil {
return fmt.Errorf("invalid duration %q", raw)
}
fv.SetInt(int64(d))
return nil
}
func setStringSlice(fv reflect.Value, raw string) error {
if fv.Type().Elem().Kind() != reflect.String {
return fmt.Errorf("unsupported slice element kind %s", fv.Type().Elem().Kind())