You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/libs/jwt/jwt.go

152 lines
3.8 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package jwt
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/veypi/vbase/cfg"
)
var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("token expired")
ErrTokenRevoked = errors.New("token revoked")
)
// Claims JWT声明精简版仅存鉴权必需字段
type Claims struct {
jwt.RegisteredClaims
UserID string `json:"uid"`
Type string `json:"type"` // "access" / "refresh"
Version int64 `json:"ver"` // token 版本号,用于撤销
}
// TokenPair Token对
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
// GenerateTokenPair 生成token对
func GenerateTokenPair(userID string, version int64) (*TokenPair, error) {
accessToken, err := GenerateAccessToken(userID, version)
if err != nil {
return nil, err
}
refreshToken, err := GenerateRefreshToken(userID, version)
if err != nil {
return nil, err
}
return &TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: int(cfg.Global.JWT.AccessExpiry.Seconds()),
}, nil
}
// GenerateAccessToken 生成访问令牌(轻量,仅存 UserID + Version
func GenerateAccessToken(userID string, version int64) (string, error) {
now := time.Now()
claims := Claims{
RegisteredClaims: jwt.RegisteredClaims{
ID: uuid.New().String(), // jti
Issuer: cfg.Global.JWT.Issuer,
Subject: userID,
Audience: jwt.ClaimStrings{"vbase"},
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(cfg.Global.JWT.AccessExpiry)),
},
UserID: userID,
Type: "access",
Version: version,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(cfg.Global.JWT.Secret))
}
// GenerateRefreshToken 生成刷新令牌
func GenerateRefreshToken(userID string, version int64) (string, error) {
now := time.Now()
claims := Claims{
RegisteredClaims: jwt.RegisteredClaims{
ID: uuid.New().String(),
Issuer: cfg.Global.JWT.Issuer,
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(cfg.Global.JWT.RefreshExpiry)),
},
UserID: userID,
Type: "refresh",
Version: version,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(cfg.Global.JWT.Secret))
}
// ParseToken 解析Token
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(cfg.Global.JWT.Secret), nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, ErrInvalidToken
}
// GetJTI 获取Token ID
func GetJTI(tokenString string) (string, error) {
claims, err := ParseToken(tokenString)
if err != nil {
return "", err
}
return claims.ID, nil
}
// IsAccessToken 是否是访问令牌
func IsAccessToken(claims *Claims) bool {
return claims.Type == "access"
}
// IsRefreshToken 是否是刷新令牌
func IsRefreshToken(claims *Claims) bool {
return claims.Type == "refresh"
}
// GetSecret 获取当前 JWT Secret空则回退 Key
func GetSecret() string {
s := cfg.Global.JWT.Secret
if s == "" {
s = string(cfg.Global.Key)
}
return s
}