142 lines
3.7 KiB
Go
142 lines
3.7 KiB
Go
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
|
||
}
|