golang-jwt v5 入门
一句话总结
golang-jwt/jwt/v5是 Go 最主流的 JWT 库,三步走:签发 → 解析 → 校验。
bash
go get github.com/golang-jwt/jwt/v5
⚠️ v5 相比 v4 改动较大:
StandardClaims→RegisteredClaims,时间字段全部用*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,公钥分发出去 |
十一、安全最佳实践
- 密钥放环境变量 / KMS,绝不入库
- 必须校验算法 :
WithValidMethods([]string{"HS256"}) - 必须有
exp:access 短(15min)、refresh 长(7-30 天) - HTTPS 传输,不要塞进 URL query
- 敏感数据别放 Payload :JWT 是 base64,不是加密
- 要支持注销 → 配合 Redis 黑名单(存 jti / userID + 签发时间)
- 不要重复签发:登录后只发一对,旧的失效
十二、一句话记忆
golang-jwt v5 三步:
NewWithClaims造 →SignedString签 →ParseWithClaims验。
- 用强类型
Claims+ 内嵌RegisteredClaims- 时间字段用
jwt.NewNumericDate(t)- 解析必加
WithValidMethods防算法切换攻击- JWT 不是加密,敏感数据别放 Payload