package auth import ( "errors" "time" "github.com/golang-jwt/jwt/v5" ) type Claims struct { Username string `json:"username"` Role string `json:"role"` TokenType string `json:"token_type,omitempty"` // access / refresh,兼容 SyncTV 双令牌模型 jwt.RegisteredClaims } type JWTManager struct { secret []byte ttl time.Duration refreshTTL time.Duration issuer string } func NewJWTManager(secret string, ttl time.Duration) *JWTManager { if secret == "" { secret = "insecure-jwt-secret-change-me" } if ttl == 0 { ttl = 2 * time.Hour } return &JWTManager{secret: []byte(secret), ttl: ttl, refreshTTL: 7 * 24 * time.Hour, issuer: "sync-live"} } func NewJWTManagerWithRefresh(secret string, ttl, refreshTTL time.Duration) *JWTManager { if secret == "" { secret = "insecure-jwt-secret-change-me" } if ttl == 0 { ttl = 2 * time.Hour } if refreshTTL == 0 { refreshTTL = 7 * 24 * time.Hour } return &JWTManager{secret: []byte(secret), ttl: ttl, refreshTTL: refreshTTL, issuer: "sync-live"} } func (j *JWTManager) Sign(username, role string) (string, error) { now := time.Now() claims := Claims{ Username: username, Role: role, TokenType: "access", RegisteredClaims: jwt.RegisteredClaims{ Issuer: j.issuer, Subject: username, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(j.ttl)), NotBefore: jwt.NewNumericDate(now), ID: username + "-access-" + now.Format(time.RFC3339Nano), }, } tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return tok.SignedString(j.secret) } func (j *JWTManager) SignRefresh(username, role string) (string, error) { now := time.Now() claims := Claims{ Username: username, Role: role, TokenType: "refresh", RegisteredClaims: jwt.RegisteredClaims{ Issuer: j.issuer, Subject: username, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(j.refreshTTL)), NotBefore: jwt.NewNumericDate(now), ID: username + "-refresh-" + now.Format(time.RFC3339Nano), }, } tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return tok.SignedString(j.secret) } func (j *JWTManager) Verify(tokenStr string) (*Claims, error) { if tokenStr == "" { return nil, errors.New("empty token") } tok, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) { if t.Method != jwt.SigningMethodHS256 { return nil, errors.New("unexpected signing method") } return j.secret, nil }) if err != nil { return nil, err } claims, ok := tok.Claims.(*Claims) if !ok || !tok.Valid { return nil, errors.New("invalid token") } return claims, nil } func (j *JWTManager) VerifyRefresh(tokenStr string) (*Claims, error) { claims, err := j.Verify(tokenStr) if err != nil { return nil, err } if claims.TokenType != "" && claims.TokenType != "refresh" { return nil, errors.New("not a refresh token") } return claims, nil } func (j *JWTManager) Refresh(refreshToken string) (string, *Claims, error) { claims, err := j.VerifyRefresh(refreshToken) if err != nil { return "", nil, err } newAccess, err := j.Sign(claims.Username, claims.Role) if err != nil { return "", nil, err } newClaims, _ := j.Verify(newAccess) return newAccess, newClaims, nil } func (j *JWTManager) TTL() time.Duration { return j.ttl } func (j *JWTManager) RefreshTTL() time.Duration { return j.refreshTTL } func (j *JWTManager) IssuePair(username, role string) (access, refresh string, err error) { access, err = j.Sign(username, role) if err != nil { return "", "", err } refresh, err = j.SignRefresh(username, role) if err != nil { return "", "", err } return access, refresh, nil }