|
|
@@ -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
|
|
|
+ },
|
|
|
+ )
|
|
|
+}
|