jwt v4 项目代码实例

安装jwt包
go get -u github.com/golang-jwt/jwt/v4

pkg/jwt/jwt.go

package jwt

import (
    "errors"
    "github.com/gin-gonic/gin"
    jwtpkg "github.com/golang-jwt/jwt/v4"
    "github.com/sreio/gohub/pkg/app"
    "github.com/sreio/gohub/pkg/config"
    "github.com/sreio/gohub/pkg/logger"
    "strings"
    "time"
)

var (
    ErrTokenExpired           = errors.New("令牌已过期")
    ErrTokenExpiredMaxRefresh = errors.New("令牌已过最大刷新时间")
    ErrTokenMalformed         = errors.New("请求令牌格式有误")
    ErrTokenInvalid           = errors.New("请求令牌无效")
    ErrHeaderEmpty            = errors.New("需要认证才能访问!")
    ErrHeaderMalformed        = errors.New("请求头中 Authorization 格式有误")
)

type JWT struct {
    SignKey    []byte        // 秘钥
    MaxRefresh time.Duration // 刷新token的最大过期时间
}

// UserInfo 自定义用户信息
type UserInfo struct {
    UserID   string `json:"user_id"`
    UserName string `json:"user_name"`
}

// CustomJWTClaims 自定义Payload信息
type CustomJWTClaims struct {
    UserInfo
    ExpireAtTime int64 `json:"expire_time"` // 过期时间

    // StandardClaims 结构体实现了 Claims 接口继承了  Valid() 方法
    // JWT 规定了7个官方字段,提供使用:
    // - iss (issuer):发布者
    // - sub (subject):主题
    // - iat (Issued At):生成签名的时间
    // - exp (expiration time):签名过期时间
    // - aud (audience):观众,相当于接受者
    // - nbf (Not Before):生效时间
    // - jti (JWT ID):编号
    jwtpkg.RegisteredClaims
}

func NewJWT() *JWT {
    return &JWT{
        SignKey:    []byte(config.Get("app.key")),
        MaxRefresh: time.Duration(config.GetInt64("jwt.max_refresh_time")) * time.Minute,
    }
}

// ParserToken 解析 Token,中间件中调用
func (j *JWT) ParserToken(ctx *gin.Context) (*CustomJWTClaims, error) {
    // 从header获取token
    tokenString, err := j.getTokenFromHeader(ctx)
    if err != nil {
        return nil, err
    }

    // 解析token
    token, err := j.parseTokenString(tokenString)
    if err != nil {
        validationErr, ok := err.(*jwtpkg.ValidationError)
        if ok {
            if validationErr.Errors == jwtpkg.ValidationErrorMalformed {
                return nil, ErrTokenMalformed
            } else if validationErr.Errors == jwtpkg.ValidationErrorExpired {
                return nil, ErrTokenExpired
            }
        }
        return nil, ErrTokenInvalid
    }

    if claims, ok := token.Claims.(*CustomJWTClaims); ok && token.Valid {
        return claims, nil
    }
    return nil, ErrTokenInvalid
}

// RefreshToken 更新 Token,用以提供 refresh token 接口
func (j *JWT) RefreshToken(ctx *gin.Context) (string, error) {
    // 1. 从 Header 里获取 token
    tokenString, parseErr := j.getTokenFromHeader(ctx)
    if parseErr != nil {
        return "", parseErr
    }

    // 2. 调用 jwt 库解析用户传参的 Token
    token, err := j.parseTokenString(tokenString)

    // 3. 解析出错,未报错证明是合法的 Token(甚至未到过期时间)
    if err != nil {
        validationErr, ok := err.(*jwtpkg.ValidationError)
        // 满足 refresh 的条件:只是单一的报错 ValidationErrorExpired
        if !ok || validationErr.Errors != jwtpkg.ValidationErrorExpired {
            return "", err
        }
    }

    // 4. 解析 JWTCustomClaims 的数据
    claims := token.Claims.(*CustomJWTClaims)

    // 5. 检查是否过了『最大允许刷新的时间』
    t := app.TimeNowInTimezone().Add(-j.MaxRefresh).Unix()
    // 首次签名时间 > (当前时间 - 最大允许刷新时间)
    if claims.IssuedAt.Unix() > t {
        claims.RegisteredClaims.ExpiresAt = jwtpkg.NewNumericDate(j.expireAtTime())
        return j.createToken(*claims)
    }

    return "", ErrTokenExpiredMaxRefresh
}

// IssueToken 生成  Token,在登录成功时调用
func (j *JWT) IssueToken(info UserInfo) string {
    // 构造自定义Payload信息
    expireTime := j.expireAtTime()
    claims := CustomJWTClaims{
        // 用户信息
        UserInfo: UserInfo{
            UserID:   info.UserID,
            UserName: info.UserName,
        },
        // 过期时间
        ExpireAtTime: expireTime.Unix(),
        RegisteredClaims: jwtpkg.RegisteredClaims{
            NotBefore: jwtpkg.NewNumericDate(app.TimeNowInTimezone()), // 签名生效时间
            IssuedAt:  jwtpkg.NewNumericDate(app.TimeNowInTimezone()), // 首次签名时间(后续刷新 Token 不会更新)
            ExpiresAt: jwtpkg.NewNumericDate(expireTime),              // 签名过期时间
            Issuer:    config.GetString("app.name"),                   // 签名颁发者
        },
    }

    // 根据 claims 生成token对象
    token, err := j.createToken(claims)
    if err != nil {
        logger.LogIf(err)
        return ""
    }
    return token
}

// createToken 创建 Token,内部使用,外部请调用 IssueToken
func (j *JWT) createToken(claims CustomJWTClaims) (string, error) {
    // 使用HS256算法进行token生成
    t := jwtpkg.NewWithClaims(jwtpkg.SigningMethodHS256, claims)
    return t.SignedString(j.SignKey)
}

// token过期时间
func (j *JWT) expireAtTime() time.Time {
    timezone := 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 timezone.Add(expire)
}

// getTokenFromHeader 使用 jwtpkg.ParseWithClaims 解析 Token
// Authorization:Bearer xxxxx
func (j *JWT) getTokenFromHeader(ctx *gin.Context) (string, error) {
    authHeader := ctx.Request.Header.Get("Authorization")
    if authHeader == "" {
        return "", ErrHeaderEmpty
    }

    parts := strings.SplitN(authHeader, " ", 2)
    if len(parts) != 2 || parts[0] != "Bearer" {
        return "", ErrHeaderMalformed
    }
    return parts[1], nil
}

// parseTokenString 解析token
func (j *JWT) parseTokenString(token string) (*jwtpkg.Token, error) {
    return jwtpkg.ParseWithClaims(token, &CustomJWTClaims{}, func(token *jwtpkg.Token) (interface{}, error) {
        return j.SignKey, nil
    })
}

使用方法

token := jwt.NewJWT().IssueToken(jwt.UserInfo{
    UserID:   userModel.GetIDString(),
    UserName: userModel.Name,
})
本帖已被设为精华帖!
本帖由系统于 1年前 自动加精
sreio
讨论数量: 1

感觉这章refreshToken根本就是做样子的,没有一点作用,一般正常流程都是登录成功后生成双 token,一个refreshToken 和 access token,进行请求的时候携带access token,access token过期后使用 refreshToken 获取新的 access token,因为access token时间短,被恶意获取后,损失不会很大,而 refreshToken 不需要频繁的在网络中传递保证了安全性,而这里却没有任何体现,不如直接将 access token 的过期时间变长没有如何区块,没必要使用 refreshToken

2天前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!