完善掉线诊断并优化二维码加载

This commit is contained in:
yml2213
2026-07-14 16:06:25 +08:00
parent 7b2d94e22d
commit 3a555c27b2
12 changed files with 388 additions and 31 deletions
+55 -5
View File
@@ -14,6 +14,34 @@ const (
var ErrInvalidToken = errors.New("invalid token")
// TokenValidationError 仅携带可安全记录的失败类别,不包含原始令牌或签名信息。
type TokenValidationError struct {
Reason string
err error
}
func (e *TokenValidationError) Error() string {
return "token validation failed: " + e.Reason
}
func (e *TokenValidationError) Unwrap() error {
return e.err
}
// AuthFailureReason 让 HTTP 鉴权层可在不依赖具体认证包的情况下读取失败类别。
func (e *TokenValidationError) AuthFailureReason() string {
return e.Reason
}
// TokenFailureReason 返回适合写入日志的令牌校验失败类别。
func TokenFailureReason(err error) string {
var validationErr *TokenValidationError
if errors.As(err, &validationErr) && validationErr.Reason != "" {
return validationErr.Reason
}
return "invalid"
}
type JWTManager struct {
secret []byte
accessTTL time.Duration
@@ -79,22 +107,44 @@ func (m *JWTManager) ParseSubject(tokenText, expectedType string, expectedSubjec
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenText, claims, func(token *jwt.Token) (any, error) {
if token.Method != m.signingMethod {
return nil, ErrInvalidToken
return nil, newTokenValidationError("signing_method")
}
return m.secret, nil
})
if err != nil || !token.Valid {
return nil, ErrInvalidToken
if err != nil {
return nil, newTokenValidationError(jwtFailureReason(err))
}
if !token.Valid {
return nil, newTokenValidationError("invalid")
}
if claims.TokenType != expectedType {
return nil, ErrInvalidToken
return nil, newTokenValidationError("token_type")
}
if expectedSubjectType != "" && claims.SubjectType != expectedSubjectType {
return nil, ErrInvalidToken
return nil, newTokenValidationError("subject_type")
}
return claims, nil
}
func newTokenValidationError(reason string) error {
return &TokenValidationError{Reason: reason, err: ErrInvalidToken}
}
func jwtFailureReason(err error) string {
switch {
case errors.Is(err, jwt.ErrTokenExpired):
return "expired"
case errors.Is(err, jwt.ErrTokenSignatureInvalid):
return "signature_invalid"
case errors.Is(err, jwt.ErrTokenMalformed):
return "malformed"
case errors.Is(err, jwt.ErrTokenNotValidYet):
return "not_valid_yet"
default:
return "invalid"
}
}
func (m *JWTManager) generate(userID uint64, subject string, subjectType string, tokenType string, tokenVersion int64, ttl time.Duration) (string, error) {
now := time.Now()
claims := Claims{
+62
View File
@@ -0,0 +1,62 @@
package auth
import (
"testing"
"time"
)
func TestParseSubjectReportsSafeFailureReason(t *testing.T) {
manager := NewJWTManager("test-jwt-secret-for-failure-reason")
pair, err := manager.GenerateSubjectPairWithVersion(1, "admin", "admin", 1)
if err != nil {
t.Fatalf("GenerateSubjectPairWithVersion() error = %v", err)
}
tests := []struct {
name string
manager *JWTManager
token string
tokenType string
want string
}{
{
name: "签名不匹配",
manager: NewJWTManager("another-jwt-secret-for-failure-reason"),
token: pair.AccessToken,
tokenType: "access",
want: "signature_invalid",
},
{
name: "令牌类型不匹配",
manager: manager,
token: pair.AccessToken,
tokenType: "refresh",
want: "token_type",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := tt.manager.ParseSubject(tt.token, tt.tokenType, "admin")
if err == nil {
t.Fatal("ParseSubject() error = nil")
}
if got := TokenFailureReason(err); got != tt.want {
t.Fatalf("TokenFailureReason() = %q, want %q", got, tt.want)
}
})
}
t.Run("令牌已过期", func(t *testing.T) {
expiredManager := NewJWTManager("test-jwt-secret-for-expired-token")
expiredManager.accessTTL = -time.Minute
expiredPair, err := expiredManager.GenerateSubjectPairWithVersion(1, "admin", "admin", 1)
if err != nil {
t.Fatalf("GenerateSubjectPairWithVersion() error = %v", err)
}
_, err = expiredManager.ParseSubject(expiredPair.AccessToken, "access", "admin")
if got := TokenFailureReason(err); got != "expired" {
t.Fatalf("TokenFailureReason() = %q, want %q", got, "expired")
}
})
}