golang-jwt v5 入门

golang-jwt v5 入门

一句话总结

golang-jwt/jwt/v5 是 Go 最主流的 JWT 库,三步走:签发 → 解析 → 校验。

bash 复制代码
go get github.com/golang-jwt/jwt/v5

⚠️ v5 相比 v4 改动较大:StandardClaimsRegisteredClaims,时间字段全部用 *jwt.NumericDate


一、JWT 是什么(30 秒回顾)

一个 JWT 长这样(三段,用 . 连接):

bash 复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9    ← Header(base64)
.eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDB9      ← Payload(base64)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV    ← Signature
内容
Header 算法(HS256 / RS256...)
Payload Claims(用户信息 + 过期时间等)
Signature 用密钥对前两段签名

服务端只用密钥校验签名,不需要存 Token(无状态)。

⚠️ JWT 是 base64 编码不是加密,任何人都能解开看 Payload,敏感数据别放。


二、最小可运行示例

2.1 签发 Token

go 复制代码
import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var jwtKey = []byte("my-secret-key-32bytes-min......")

func GenerateToken(userID int64) (string, error) {
    claims := jwt.MapClaims{
        "user_id": userID,
        "exp":     time.Now().Add(24 * time.Hour).Unix(),
        "iat":     time.Now().Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

2.2 解析 + 校验

go 复制代码
func ParseToken(tokenStr string) (int64, error) {
    token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
        // 校验算法是否为预期,防止 alg=none 攻击
        if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method")
        }
        return jwtKey, nil
    })
    if err != nil {
        return 0, err
    }
    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok || !token.Valid {
        return 0, errors.New("invalid token")
    }
    return int64(claims["user_id"].(float64)), nil // JSON 数字默认 float64
}

三、推荐:自定义 Claims 结构体

MapClaims 用起来要类型断言,强类型 Claims 才优雅

go 复制代码
type MyClaims struct {
    UserID int64  `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims          // 嵌入标准字段(exp/iat/iss...)
}

func Generate(userID int64, role string) (string, error) {
    claims := MyClaims{
        UserID: userID,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
            Issuer:    "my-app",
            Subject:   fmt.Sprint(userID),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

func Parse(tokenStr string) (*MyClaims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &MyClaims{}, func(t *jwt.Token) (any, error) {
        return jwtKey, nil
    })
    if err != nil {
        return nil, err
    }
    claims, ok := token.Claims.(*MyClaims)
    if !ok || !token.Valid {
        return nil, errors.New("invalid token")
    }
    return claims, nil
}

ParseWithClaims + 自定义结构体 = 直接拿到强类型字段。


四、RegisteredClaims(标准字段)

字段 缩写 含义
Issuer iss 签发者
Subject sub 主题(一般是用户 ID)
Audience aud 接收方
ExpiresAt exp 过期时间
NotBefore nbf 生效时间
IssuedAt iat 签发时间
ID jti Token 唯一 ID(黑名单用)

时间字段都是 *jwt.NumericDate,用 jwt.NewNumericDate(time.Time) 构造。

exp 和签名校验由库自动验证,过期 / nbf 未到都会返回错误。


五、常用签名算法

算法 说明 密钥类型
HS256/384/512 HMAC + SHA,对称密钥 []byte(≥ 32 字节推荐)
RS256/384/512 RSA,非对称 私钥签 / 公钥验
ES256/384/512 ECDSA,更短 椭圆曲线密钥
EdDSA Ed25519,最现代

单服务用 HS256 就够,多服务 / 第三方验签用 RS256。

go 复制代码
// HS256
token.SignedString([]byte("secret"))

// RS256
priv, _ := jwt.ParseRSAPrivateKeyFromPEM(privPEM)
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.SignedString(priv)

// 验签时返回公钥
pub, _ := jwt.ParseRSAPublicKeyFromPEM(pubPEM)
jwt.ParseWithClaims(s, &MyClaims{}, func(t *jwt.Token) (any, error) {
    return pub, nil
})

六、错误处理

v5 用 errors.Is 判断具体错误:

go 复制代码
import "github.com/golang-jwt/jwt/v5"

_, err := Parse(tokenStr)
switch {
case errors.Is(err, jwt.ErrTokenExpired):
    // 过期
case errors.Is(err, jwt.ErrTokenNotValidYet):
    // 还没到生效时间
case errors.Is(err, jwt.ErrTokenMalformed):
    // 格式错误
case errors.Is(err, jwt.ErrTokenSignatureInvalid):
    // 签名错误
case err != nil:
    // 其他
}

七、解析选项(v5 新增)

go 复制代码
token, err := jwt.ParseWithClaims(s, &MyClaims{}, keyFunc,
    jwt.WithLeeway(5*time.Second),                    // 时间容差
    jwt.WithValidMethods([]string{"HS256"}),          // 限定算法(防 alg=none)
    jwt.WithIssuer("my-app"),                         // 校验 iss
    jwt.WithAudience("api.x.com"),                    // 校验 aud
    jwt.WithExpirationRequired(),                     // 必须有 exp
)

WithValidMethods 强烈建议加上,否则有被攻击者切换算法的风险。


八、Gin 中间件示例

go 复制代码
func JWTAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        h := c.GetHeader("Authorization")
        if !strings.HasPrefix(h, "Bearer ") {
            c.AbortWithStatusJSON(401, gin.H{"err": "no token"})
            return
        }
        tokenStr := strings.TrimPrefix(h, "Bearer ")

        claims, err := Parse(tokenStr)
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"err": err.Error()})
            return
        }
        c.Set("userID", claims.UserID)
        c.Set("role", claims.Role)
        c.Next()
    }
}

// 使用
r.GET("/me", JWTAuth(), func(c *gin.Context) {
    uid := c.GetInt64("userID")
    c.JSON(200, gin.H{"userID": uid})
})

九、Refresh Token 双 Token 模式

短 access + 长 refresh:

go 复制代码
// 登录返回两个
access  := generate(userID, 15*time.Minute, "access")
refresh := generate(userID, 30*24*time.Hour, "refresh")

// 刷新接口:拿 refresh 换新 access(refresh 也可一次性)

注销 / 提前作废 token:把 jti 存 Redis 黑名单,每次校验先查。


十、常见坑

现象 原因
token contains an invalid number of segments token 字符串拼错 / 加了 Bearer 前缀没去掉
signature is invalid 密钥不一致 / 算法不匹配
过期了还能用 没设 exp;或服务器时间漂移大(用 WithLeeway
claims["user_id"].(int64) panic JSON 数字默认 float64,要 int64(v.(float64)),或用强类型 Claims
任何人都能伪造 token 你把密钥提交进了 Git / 写在前端
HS256 密钥太短 HS256 至少 32 字节,越随机越好
多服务共用密钥泄漏风险大 改用 RS256,公钥分发出去

十一、安全最佳实践

  1. 密钥放环境变量 / KMS,绝不入库
  2. 必须校验算法WithValidMethods([]string{"HS256"})
  3. 必须有 exp:access 短(15min)、refresh 长(7-30 天)
  4. HTTPS 传输,不要塞进 URL query
  5. 敏感数据别放 Payload :JWT 是 base64,不是加密
  6. 要支持注销 → 配合 Redis 黑名单(存 jti / userID + 签发时间)
  7. 不要重复签发:登录后只发一对,旧的失效

十二、一句话记忆

golang-jwt v5 三步:NewWithClaims 造 → SignedString 签 → ParseWithClaims 验。

  • 用强类型 Claims + 内嵌 RegisteredClaims
  • 时间字段用 jwt.NewNumericDate(t)
  • 解析必加 WithValidMethods 防算法切换攻击
  • JWT 不是加密,敏感数据别放 Payload
相关推荐
carson9551 小时前
基于springboot和vue的文本文件上传下载在线编辑功能
后端
n8n1 小时前
Spring AI 提示词工程进阶:System / User / Assistant 角色、Prompt Template 动态拼装与多角色人设切换
后端
步行cgn1 小时前
Spring Boot 主入口类上的 @Enable 和 @Scan 注解详解
java·spring boot·后端
Lyra_Infra2 小时前
云效主机部署场景下 Python 服务生命周期问题复盘
后端·python
程序员cxuan2 小时前
GPT images 2.5 一手实测,这也太颠了。。。
后端·程序员
山岚的运维笔记2 小时前
mysql 专业笔记 -- 第 17 章:连接:连接三个具有相同名称 ID 的表
运维·数据库·笔记·后端·学习·mysql·dba
遨翔在知识的海洋里3 小时前
nest(3)- filter和Interceptor
后端
挽安6213 小时前
若依微服务 Nacos 2.x 控制台配置列表为空?一个 tenant_id 字段引发的血案
后端
周杰伦fans3 小时前
ASP.NET Core Identity 从入门到实战:常见问题与解决方案
后端·asp.net