如何在 JWT 中嵌入并提取 RSA 公钥(使用 jwt-go 实现)

2026-01-31 00:00:00 作者:花韻仙語

本文详解如何安全地将 rsa 公钥以 pem 字符串形式嵌入 jwt 的 claims 中,并在解析时动态还原为 *rsa.publickey,实现基于公钥分发的灵活验签逻辑。适用于多租户、密钥轮换或去中心化签名验证场景。

在标准 JWT 实践中,签名验证依赖服务端预置的公钥(如 jwt.Parse(..., func() { return publicKey })),但该方式缺乏灵活性:无法支持动态密钥切换、多租户独立密钥,或客户端自主选择验证方。一种常见误解是直接将 *big.Int 类型的 N/E 字段存入 claims——这不仅会因 JSON 序列化导致精度丢失(big.Int 被截断为 int64),更严重的是破坏了 JWT 的信任模型:若公钥本身可被任意篡改并嵌入 token,则签名完全失去防伪意义。

✅ 正确做法是:*将公钥序列化为标准 PEM 格式字符串(Base64 编码的 ASN.1 DER 结构),存入 claims;解析时再从该字符串反序列化为 `rsa.PublicKey**。PEM 是文本安全、标准兼容且无精度损失的表示方式,且jwt-go提供了开箱即用的jwt.ParseRSAPublicKeyFromPEM()` 辅助函数。

以下是完整可运行示例(基于 github.com/dgrijalva/jwt-go v3.x):

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "log"

    jwt "github.com/dgrijalva/jwt-go"
)

func main() {
    // 1. 生成 RSA 密钥对(生产环境请使用 ≥2048 位)
    privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        log.Fatal("生成私钥失败:", err)
    }

    // 2. 构建 token 并嵌入 PEM 格式公钥
    token := jwt.New(jwt.SigningMethodRS256)
    token.Claims = jwt.MapClaims{
        "username": "victorsamuelmd",
        "exp":      time.Now().Add(time.H

our).Unix(), // ✅ 关键:将公钥 Marshal 为 PKIX DER,再编码为 PEM 字符串 "public_key_pem": pemEncodePublicKey(&privateKey.PublicKey), } tokenString, err := token.SignedString(privateKey) if err != nil { log.Fatal("签名 token 失败:", err) } fmt.Println("已生成 token:", tokenString) // 3. 解析 token 并动态提取公钥验签 parsedToken, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { // 验证签名算法是否符合预期 if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("意外的签名方法: %v", t.Header["alg"]) } // 从 claims 中读取 PEM 字符串并解析为 *rsa.PublicKey claims, ok := t.Claims.(jwt.MapClaims) if !ok { return nil, fmt.Errorf("claims 类型断言失败") } pemStr, ok := claims["public_key_pem"].(string) if !ok { return nil, fmt.Errorf("claims 中未找到 public_key_pem 字段或类型错误") } return jwt.ParseRSAPublicKeyFromPEM([]byte(pemStr)) }) if err != nil { log.Fatal("解析/验签失败:", err) } if !parsedToken.Valid { log.Fatal("token 验证失败:签名无效或已过期") } fmt.Println("✅ token 验证成功!Payload:", parsedToken.Claims) } // pemEncodePublicKey 将 *rsa.PublicKey 转为 PEM 格式字符串 func pemEncodePublicKey(pub *rsa.PublicKey) string { bytes, err := x509.MarshalPKIXPublicKey(pub) if err != nil { panic("公钥序列化失败: " + err.Error()) } pemBlock := &pem.Block{ Type: "RSA PUBLIC KEY", Bytes: bytes, } return string(pem.EncodeToMemory(pemBlock)) }

⚠️ 重要注意事项

  • 安全性警示:此方案仅适用于可信上下文(如内部微服务通信、受控 API 网关)。若 token 可能被恶意第三方截获并重放,嵌入公钥会削弱“唯一可信签发者”模型——攻击者可伪造含自己公钥的 token。此时应坚持传统方式:服务端维护权威公钥池,通过 kid(Key ID)字段索引。
  • 性能考量:每次解析都需 PEM 解析和 ASN.1 解码,比直接使用内存公钥稍慢。高并发场景建议缓存已解析的公钥(以 PEM 字符串为 key)。
  • 密钥长度:示例使用 2048 位;生产环境推荐 3072 或 4096 位以满足长期安全要求。
  • 库迁移提示:dgrijalva/jwt-go 已归档,新项目推荐迁移到 golang-jwt/jwt/v5,其 API 更清晰(如 jwt.WithValidMethods, jwt.WithValidator),且原生支持 func(token *Token) (any, error) 形式的 KeyFunc。

总结:通过 PEM 字符串嵌入公钥是一种实用的动态验签技术,它平衡了灵活性与标准兼容性。关键在于理解其适用边界——它不是替代传统公钥管理的银弹,而是特定架构下的有力补充。

猜你喜欢

联络方式:

400 9058 355

邮箱:8955556@qq.com

Q Q:8955556

微信二维码
在线咨询 拨打电话

电话

400 9058 355

微信二维码

微信二维码