runningwater hai 8 meses
pai
achega
be3cb1cbb4
Modificáronse 4 ficheiros con 244 adicións e 0 borrados
  1. 20 0
      config/jwt.go
  2. 1 0
      go.mod
  3. 2 0
      go.sum
  4. 221 0
      pkg/jwt/jwt.go

+ 20 - 0
config/jwt.go

@@ -0,0 +1,20 @@
+package config
+
+import "github.com/runningwater/gohub/pkg/config"
+
+func init() {
+	config.Add("jwt", func() map[string]any {
+		return map[string]any{
+
+			// 签名密钥 使用 config.GetString("app.key")
+			// "signing_key":  "",
+			// 过期时间,单位:分钟,默认 2 小时
+			"expire_time": config.Env("JWT_EXPIRE_TIME", 120),
+			// 刷新 Token 的最大过期时间,单位:分钟,默认 2 个月
+			"max_refresh_time": config.Env("JWT_MAX_REFRESH_TIME", 86400),
+
+			// 调试模式下的过期时间
+			"debug_expire_time": 86400,
+		}
+	})
+}

+ 1 - 0
go.mod

@@ -6,6 +6,7 @@ require (
 	github.com/KenmyZhang/aliyun-communicate v0.0.0-20180308134849-7997edc57454
 	github.com/gin-gonic/gin v1.10.0
 	github.com/go-redis/redis v6.15.9+incompatible
+	github.com/golang-jwt/jwt/v5 v5.2.2
 	github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
 	github.com/mojocn/base64Captcha v1.3.8
 	github.com/spf13/cast v1.7.1

+ 2 - 0
go.sum

@@ -43,6 +43,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
 github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

+ 221 - 0
pkg/jwt/jwt.go

@@ -0,0 +1,221 @@
+package jwt
+
+import (
+	"errors"
+	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	jwtpkg "github.com/golang-jwt/jwt/v5"
+	"github.com/runningwater/gohub/pkg/app"
+	"github.com/runningwater/gohub/pkg/config"
+	"github.com/runningwater/gohub/pkg/logger"
+)
+
+var (
+	ErrTokenExpired           error = errors.New("令牌已过期")
+	ErrTokenNotValidYet       error = errors.New("令牌尚未生效")
+	ErrTokenMalformed         error = errors.New("令牌格式错误")
+	ErrTokenInvalid           error = errors.New("令牌无效")
+	ErrTokenExpiredMaxRefresh error = errors.New("令牌已过期,无法刷新")
+	ErrHeaderEmpty            error = errors.New("需要认证才能访问!")
+	ErrHeaderMalformed        error = errors.New("请求头格式错误")
+)
+
+// JWT 结构体,包含签名密钥和刷新 Token 的最大过期时间
+type JWT struct {
+	// 签名密钥,用于生成和验证 JWT 签名,读取 app.key
+	SigningKey []byte
+
+	// 刷新 Token 的最大过期时间
+	MaxRefresh time.Duration
+}
+
+// JWTCustomClaims 自定义的 JWT 载荷结构体,包含用户 ID、用户名 和过期时间
+type JWTCustomClaims struct {
+	// 用户 ID
+	UserID string `json:"user_id"`
+	// 用户名
+	UserName string `json:"user_name"`
+	// 过期时间
+	ExpireTime int64 `json:"expire_time"`
+
+	// 标准的 JWT 载荷字段
+	// Audience aud : 受众
+	// Issuer iss : 签发者
+	// Not Before nbf : 生效时间
+	// Issued At iat : 签发时间
+	// JWT ID jti : JWT ID 编号
+	// Subject sub : 主题
+	// Expires At exp : 过期时间
+	// 标准的 JWT 载荷字段
+	jwtpkg.RegisteredClaims
+}
+
+func NewJWT() *JWT {
+	return &JWT{
+		SigningKey: []byte(config.GetString("app.key")),
+		MaxRefresh: time.Duration(config.GetInt64("jwt.max_refresh_time")) * time.Minute,
+	}
+}
+
+// ParseToken 解析 Token
+func (j *JWT) ParseToken(c *gin.Context) (*JWTCustomClaims, error) {
+
+	// 1. 获取请求头中的 Token
+	tokenString, err := j.getTokenFromHeader(c)
+	if err != nil {
+		return nil, err
+	}
+
+	// 2. 调用 jwt 库解析用户传入的 token
+	token, err := j.parseTokenString(tokenString)
+
+	// 3. 解析出错, 未报错证明是合法的 token
+	if err != nil {
+		return nil, err
+		// validationErr, ok := err.(*jwtpkg.ValidationError)
+		// if ok {
+		// 	switch validationErr.Errors {
+		// 	case jwtpkg.ValidationErrorMalformed:
+		// 		return nil, ErrTokenMalformed
+		// 	case jwtpkg.ValidationErrorExpired:
+		// 		return nil, ErrTokenExpired
+		// 	default:
+		// 		return nil, ErrTokenInvalid
+		// 	}
+		// }
+
+		// }
+		// return nil, ErrTokenInvalid
+	}
+
+	if claims, ok := token.Claims.(*JWTCustomClaims); ok && token.Valid {
+		return claims, nil
+	}
+	return nil, ErrTokenInvalid
+
+}
+
+// IssueToken 签发 Token
+func (j *JWT) IssueToken(userID, userName string) string {
+
+	// 1. 构造用户 claims 信息(负荷)
+	expireTime := j.expireAtTime()
+	claims := JWTCustomClaims{
+		userID,
+		userName,
+		expireTime.Unix(),
+		jwtpkg.RegisteredClaims{
+			NotBefore: jwtpkg.NewNumericDate(app.TimenowInTimezone()), // 生效时间
+			IssuedAt:  jwtpkg.NewNumericDate(app.TimenowInTimezone()), // 签发时间
+			ExpiresAt: jwtpkg.NewNumericDate(expireTime),              // 过期时间
+			Issuer:    config.GetString("app.name"),                   // 签发者
+			Subject:   userID,                                         // 主题
+			Audience:  []string{userName},                             // 受众
+		},
+	}
+	// 2. 根据 claims 生成 Token
+	token, err := j.generateToken(claims)
+	if err != nil {
+		logger.LogIf(err)
+		return ""
+	}
+	return token
+}
+
+// RefreshToken 刷新 Token
+func (j *JWT) RefreshToken(c *gin.Context) (string, error) {
+	// 1. 从 Header 里获取 token
+	tokenString, err := j.getTokenFromHeader(c)
+	if err != nil {
+		return "", err
+	}
+	// 2. 调用 jwt 库解析用户传入的 token
+	token, err := j.parseTokenString(tokenString)
+	// 3. 解析出错, 未报错证明是合法的 token
+	if err != nil {
+		// validationErr, ok := err.(*jwtpkg.ValidationError)
+		// // 满足刷新条件:只是单一的报错 ValidationErrorExpired
+		// if !ok || validationErr.Errors != jwtpkg.ValidationErrorExpired {
+		// 	return "", err
+		// }
+		if errors.Is(err, jwtpkg.ErrTokenExpired) {
+			// 过期了,返回错误
+			return "", err
+		}
+	}
+
+	// 4. 解析 claims
+	claims := token.Claims.(*JWTCustomClaims)
+
+	// 5. 判断是否过了最大允许刷新的时间
+	x := app.TimenowInTimezone().Add(-j.MaxRefresh).Unix()
+	if claims.IssuedAt.Unix() > x {
+		// 修改过期时间
+		claims.ExpiresAt = jwtpkg.NewNumericDate(j.expireAtTime())
+		return j.generateToken(*claims)
+	}
+
+	// 6. 超过最大允许刷新的时间,返回错误
+	return "", ErrTokenExpiredMaxRefresh
+}
+
+// generateToken 生成 Token, 内部使用,外部请调用 IssueToken
+func (j *JWT) generateToken(claims JWTCustomClaims) (string, error) {
+
+	// 创建一个新的 Token 对象,使用 HMAC SHA256 签名方法
+	token := jwtpkg.NewWithClaims(jwtpkg.SigningMethodHS256, claims)
+
+	return token.SignedString(j.SigningKey)
+}
+
+// expireAtTime 计算过期时间
+func (j *JWT) expireAtTime() time.Time {
+	timeNow := app.TimenowInTimezone()
+
+	var expireTime int64
+	if config.GetBool("app.debug") {
+		expireTime = config.GetInt64("jwt.debug_expire_time")
+	} else {
+		expireTime = config.GetInt64("jwt.expire_time")
+	}
+
+	expire := time.Duration(expireTime) * time.Minute
+
+	return timeNow.Add(expire)
+}
+
+// getTokenFromHeader 获取请求头中的 Token
+// Authorization: Bearer Token
+// 从请求头中获取 Token,格式为 "Bearer Token"
+func (j *JWT) getTokenFromHeader(c *gin.Context) (string, error) {
+	// 获取请求头中的 Token
+	authHeader := c.Request.Header.Get("Authorization")
+	if authHeader == "" {
+		return "", ErrHeaderEmpty
+	}
+
+	// 检查请求头格式是否正确
+	// Bearer Token
+	// Bearer 后面有一个空格
+	parts := strings.SplitN(authHeader, " ", 2)
+	if len(parts) < 2 || parts[0] != "Bearer" {
+		return "", ErrHeaderMalformed
+	}
+
+	// 返回 Token 字符串
+	return parts[1], nil
+}
+
+// parseTokenString 解析 Token 字符串
+func (j *JWT) parseTokenString(tokenString string) (*jwtpkg.Token, error) {
+
+	return jwtpkg.ParseWithClaims(
+		tokenString,
+		&JWTCustomClaims{},
+		func(token *jwtpkg.Token) (any, error) {
+			return j.SigningKey, nil
+		},
+	)
+}