feat: JWT过期自动重置 + 用户端登录页 + 系统设置前三个真实实现
agent-111 JWT过期重置+系统设置去占位: - ws.ts 识别401/过期 markAuthExpired(清token+停重连+expired态),isTokenExpired;wsParse.ts拆出压行371;scheduleReconnect有token未过期才重连 - api.ts REST 401→auth_expired+清token+引导重登;login走skipAuthExpired密码错不误清 - types.ts WsStatus加expired;TopBar/Nav expired显示登录过期;System.tsx去alert占位改4张事实卡+合并Notice agent-113 用户端登录页: - components/Login.tsx Miuix登录卡(用户名/密码→login()→setAuthToken);App !getAuthToken()→<Login>;AdminApp LoginGate指向 agent-112 系统设置前三个(owner+审计): - auth.APIKey.RevokeAll(吊销全部key);auth.JWTService.RotateSecret(≥32校验+并发安全+新密钥不回显) - gateway admin_system.go: POST revoke-keys/rotate-jwt/backup(VACUUM INTO)/restore(防路径穿越+恢复前自动备份+原子替换+需重启) - wire.go 传DatabasePath;全仓 build/vet/test绿+交叉编译;api-client v0.17系统设置契约 实机验证: 登录(role=owner)+revoke-keys 200+渠道列表含kook; 全仓绿+前端build绿; .env密码仅登录验证不打印
This commit is contained in:
1 parent
7e39153a69
commit
1ebc82fffd
19 files changed
+1164
-245
No files matched your search
+26
-2
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
@@ -28,8 +29,11 @@ type jwtHeader struct {
|
||||
Typ string `json:"typ"`
|
||||
}
|
||||
|
||||
// JWTService signs and verifies HS256 JWTs using a shared secret.
|
||||
// JWTService signs and verifies HS256 JWTs using a shared secret. The secret is
|
||||
// protected by a RWMutex so RotateSecret can swap it while Sign/Verify read it
|
||||
// concurrently (the gateway rotates the secret live from the admin panel).
|
||||
type JWTService struct {
|
||||
mu sync.RWMutex
|
||||
secret []byte
|
||||
log *slog.Logger
|
||||
}
|
||||
@@ -76,10 +80,27 @@ func (s *JWTService) Sign(sub string, role Role, ttl time.Duration) (string, err
|
||||
return "", fmt.Errorf("auth: encode payload: %w", err)
|
||||
}
|
||||
signingInput := headerB64 + "." + payloadB64
|
||||
s.mu.RLock()
|
||||
sig := hmacSign(s.secret, []byte(signingInput))
|
||||
s.mu.RUnlock()
|
||||
return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig), nil
|
||||
}
|
||||
|
||||
// RotateSecret replaces the signing secret with newSecret, invalidating every
|
||||
// token issued under the previous secret. It returns ErrInvalidSecret when
|
||||
// newSecret is shorter than the 32-character minimum. The new secret is never
|
||||
// logged or echoed back to the caller.
|
||||
func (s *JWTService) RotateSecret(newSecret string) error {
|
||||
if len(newSecret) < minSecretLen {
|
||||
return fmt.Errorf("%w: got %d chars", ErrInvalidSecret, len(newSecret))
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.secret = []byte(newSecret)
|
||||
s.mu.Unlock()
|
||||
s.log.Info("jwt secret rotated; previously issued tokens are now invalid")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify validates the token signature, algorithm, role, and expiry, returning
|
||||
// the parsed Claims on success.
|
||||
func (s *JWTService) Verify(token string) (*Claims, error) {
|
||||
@@ -94,7 +115,10 @@ func (s *JWTService) Verify(token string) (*Claims, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: decode signature: %v", ErrMalformedToken, err)
|
||||
}
|
||||
if !hmac.Equal(provided, hmacSign(s.secret, []byte(signingInput))) {
|
||||
s.mu.RLock()
|
||||
expected := hmacSign(s.secret, []byte(signingInput))
|
||||
s.mu.RUnlock()
|
||||
if !hmac.Equal(provided, expected) {
|
||||
return nil, ErrInvalidSignature
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user