- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
本文以 Grafana Mimir 仓库内 vendored 的 golang-jwt 官方版本历史文档(VERSION_HISTORY.md)为骨架,系统梳理这套 Go 生态最主流的 JWT 库从 1.0.0 到 4.0.0 的演进脉络,并结合仓库中实际携带的 v5.3.1 源码(位于vendor/github.com/golang-jwt/jwt/v5/)逐一印证各次大版本变更在今日 API 中的落点。读完本文,你将理解 JWT 库历史上每一次破坏性变更背后的设计动机、安全修复(含 CVE-2020-26160)的来龙去脉,以及 Mimir 引入的 v5 新 API(Claims 接口、Validator、ParserOption)应当如何使用。
一、文档定位:一份记录库自身进化的"活化石"
VERSION_HISTORY.md是 golang-jwt 库作者刻意保留的版本档案:它的开头明确说明"以下版本历史仅为历史目的而保留",各版本更细的变更细节需要查看对应 release 的 changelog。这份文档的价值不在于罗列 commit,而在于它完整记录了库从单文件起步到成熟的过程中,每一个 API 设计决策的取舍——这些决策正是今天 v5 代码形态的成因。
在 Mimir 仓库中,[go.mod](https://link.gitcode.com/i/8d69f5b9fb098ad59a91420a11a250b8)第 143 行声明了github.com/golang-jwt/jwt/v5 v5.3.1 // indirect,即该库以间接依赖形式被 Mimir 引入,并完整 vendored 到vendor/github.com/golang-jwt/jwt/v5/目录下(共 20 余个 Go 源文件)。这意味着 Mimir 构建时实际使用的是这份 v5 源码,而版本历史文档正是理解这套源码设计来路的钥匙。
二、版本演进总览:从 1.0.0 到 4.0.0 的四次跃迁
| 版本 | 核心主题 | 里程碑内容 |
|---|---|---|
| 1.0.0 | 首个版本化发布 | 支持创建、签名、解析、校验 JWT;仅支持 RS256 与 HS256 |
| 1.0.1 / 1.0.2 | 稳定性修复 | 修复 RS256 传入非法 key 时的 panic;修复从证书解析公钥的 bug |
| 2.0.0 | 密钥类型泛化 | RSA/HMAC 实现重构;KeyFunc返回interface{};SigningMethodHS256变为*SigningMethodHMAC |
| 2.3.0 / 2.4.0 | 算法与解析扩展 | 新增 ECDSA、RSA PSS;引入Parser类型与"合法算法白名单" |
| 2.5.0 / 2.6.0 / 2.7.0 | 安全性与易用性 | 增加none算法(并刻意"劝退");暴露ValidationError内部错误;过期错误携带时长信息 |
| 3.0.0 | API 大重构 | 引入Claims接口;ParseWithClaims;ParseFromRequest移入request子包;签名方法注册表线程安全 |
| 3.2.0 / 3.2.1 | 拆分解析与校验 | 新增ParseUnverified;导入路径改为github.com/golang-jwt/jwt;修复 CVE-2020-26160 |
| 3.2.2 / 4.0.0 | 现代化与模块化 | 支持 EdDSA/Ed25519;优化内存分配;引入 Go Modules,v4保持与v3.x.y向后兼容 |
三、1.x 时代:定义基础能力(RS256 与 HS256)
1.0.0 是库的第一个版本化发布,确立了"创建、签名、解析、验证 JWT"四大能力,签名算法仅有两类:RS256(RSA-SHA256,非对称)与 HS256(HMAC-SHA256,对称)。
1.0.1 修复了 RS256 签名方法收到非法 key 时的 panic;1.0.2 修复了从证书解析公钥的 bug,并围绕 RS256 的密钥解析补充了大量测试,同时对 RS256 实现做了无功能变更的重构。这三条记录确立了该库后续一贯的工程风格:安全兜底(不 panic)+ 测试先行。
这一风格延续至今。在 v5 源码中,rsa_utils.go提供的ParseRSAPrivateKeyFromPEM/ParseRSAPublicKeyFromPEM正是 1.0.2 中反复打磨的那段密钥解析逻辑的最终形态。
四、2.x 时代:密钥类型泛化与算法家族扩张
4.1 2.0.0:为什么KeyFunc要从[]byte改成interface{}?
2.0.0 是第一次破坏性发布,文档给出了两个动机:
- 拓宽签名算法:并非所有算法的密钥都有统一的标准磁盘表示形式,硬性要求
[]byte过于局限; - 支持预解析 token 复用:当应用用少量密钥解析大量 token 时,预解析的密钥对象可被复用。
于是引入了三项关键 API 调整:
KeyFunc返回类型从func(t *jwt.Token) ([]byte, error)改为func(t *jwt.Token) (interface{}, error);- 类型
SigningMethodHS256重命名为SigningMethodHMAC,SigningMethodRS256重命名为SigningMethodRSA,具体尺寸(256/384/512)变为该类型的实例; SigningMethod.Sign/Verify的密钥参数统一改为interface{},并公开ParseRSAPrivateKeyFromPEM与ParseRSAPublicKeyFromPEM辅助方法。
文档特别说明:为保持兼容,RSA 算法仍接受[]byte,但同时也接受*rsa.PublicKey与*rsa.PrivateKey。这意味着集成方主要改动只是KeyFunc的签名。
源码印证:在 v5 中,Keyfunc的定义位于 token.go,返回值正是any(即interface{}的别名),且支持返回VerificationKeySet(一组密钥)。Token.SignedString也接受any类型的密钥:
type Keyfunc func(*Token) (any, error) type VerificationKey interface { crypto.PublicKey | []uint8 } type VerificationKeySet struct { Keys []VerificationKey }4.2 2.1.0 与 2.2.0:小步修复
- 2.1.0 补上了 2.0.0 遗漏的兼容性变更:
Token.SignedString的密钥参数从[]byte改为interface{}; - 2.2.0 让
Parse在收到nil的Keyfunc时优雅返回"已解析的 token + 错误",而不是 panic。这一行为在 v5 的 parser.go 中依然存在:keyFunc == nil时直接返回ErrTokenUnverifiable。
4.3 2.3.0~2.4.0:算法扩展与 Parser 抽象
- 2.3.0:新增 ECDSA 系列签名方法(ES256/384/512),并新增 RSA PSS 系列(需 Go 1.4+)。
- 2.4.0:引入
Parser类型,把解析参数配置化——这是 v5 中Parser+ParserOption体系的雏形。当时已支持:- 指定合法的签名方法列表,列表之外的算法一律拒绝(这正是今天
WithValidMethods的前身,用于防御算法混淆攻击); - 选择用
json.Number替代float64解析 token 中的数字(对应今天的WithJSONNumber)。
- 指定合法的签名方法列表,列表之外的算法一律拒绝(这正是今天
4.4 2.5.0~2.7.0:安全警示与易用性
- 2.5.0:实现了
none签名方法。文档原话是"You shouldn't use this. The API tries to make this clear"——API 刻意把"使用 none"设计得极不自然。在 v5 中这一设计演化得更加严谨:只有传入UnsafeAllowNoneSignatureType这个魔法常量作为密钥,none方法才被允许,且签名必须为空,见 none.go。同时,解析以BEARER开头的 token 时会给出更友好的错误提示。 - 2.6.0:在
ValidationError中暴露内部错误(inner error),并修复UseJSONNumber标志下的校验问题。 - 2.7.0:
jwt命令行工具新增-show选项(只解码不验证);过期 token 的错误文本现在包含"已过期多久"的信息。
五、3.0.0:Claims 接口革命
3.0.0 是文档着墨最多的一次破坏性发布,其重构思路深刻影响了今天的 v5 架构:
- 放弃 RSA 的
[]byte密钥便利:为防止"签名方法与密钥类型不匹配"导致的安全漏洞,RSA 算法不再接受[]byte密钥; Token.Claims类型改为Claims接口:默认值为MapClaims(map[string]interface{}的别名),用户自定义 claims 成为可能;- 新增
ParseWithClaims:带第三个Claims参数,替代Parse用于自定义类型解码; ParseFromRequest移入request子包:配合新接口Extractor从 HTTP 请求中提取 JWT 字符串;- 签名方法注册表线程安全:
RegisterSigningMethod内部使用sync.RWMutex保护全局 map,这一设计在 v5 的 signing_method.go 中原样保留; - 校验错误更细化:错误类型从位掩码(bitmask)体系逐渐过渡;
ValidationError新增属性保存 keyfunc / JSON 解析返回的原始错误。
在 v5 中,Claims接口的最终形态是一组取值器(getter),将"claims 的存储表示"与"校验逻辑"彻底解耦:
// claims.go type Claims interface { GetExpirationTime() (*NumericDate, error) GetIssuedAt() (*NumericDate, error) GetNotBefore() (*NumericDate, error) GetIssuer() (string, error) GetSubject() (string, error) GetAudience() (ClaimStrings, error) }两种内置实现各司其职:
- MapClaims:
map[string]any的别名,默认类型,适合快速处理任意 claims; - RegisteredClaims:按 RFC 7519 第 4.1 节注册的七个标准字段(
iss、sub、aud、exp、nbf、iat、jti)的结构体实现,典型用法是嵌入到自定义 claims 结构体中。
六、3.2.x:安全修复与路径更名
6.1 3.2.1:导入路径变更与 CVE-2020-26160
3.2.1 发生两件大事:
- 导入路径变更:
github.com/dgrijalva/jwt-go改为github.com/golang-jwt/jwt,官方在 MIGRATION_GUIDE 中给出替换建议; - 安全修复:
VerifyAudience中string与[]string的类型混淆问题被修复,对应CVE-2020-26160。该漏洞属于典型的"类型混淆导致校验绕过"——攻击者可通过构造特殊类型的audclaim 绕过受众校验。
源码印证:这一修复在 v5 中已进化为 types.go 中的ClaimStrings类型:它既能反序列化单个字符串,也能反序列化字符串数组,对错误类型一律返回ErrInvalidType,从类型层面杜绝了混淆的可能。
6.2 3.2.0:拆分"解析"与"校验"
- 新增
ParseUnverified:只解析 token、不做签名验证,便于"先取值、后在其他环节验证"的场景(v5 中该方法位于 parser.go,文档强调"除非你清楚自己在做什么,否则不要用"); - HMAC 签名方法在密钥类型错误时返回
ErrInvalidKeyType而非笼统的ErrInvalidKey; request.ParseFromRequest支持WithClaims、WithParser等修饰符选项。
6.3 3.2.2:EdDSA 与无效日期防御
- 新增 EdDSA / Ed25519 支持(v5 中为 ed25519.go 的
SigningMethodEdDSA,alg值为EdDSA); - 重要的安全修复:当
exp、iat、nbf的校验并非必需、且 claim 内容非法(非数字/非日期)时,此前可能出现问题,本次予以修复。这正是 v5 中MapClaims.parseNumericDate对非float64/json.Number类型返回ErrInvalidType的由来; - 优化了内存分配。
七、4.0.0 与 v5:Mimir 实际使用的形态
4.0.0 引入了 Go Modules 支持,并承诺与v3.x.y向后兼容。而 Mimir 仓库 vendored 的是更高版本的v5.3.1,其迁移指南见 MIGRATION_GUIDE.md。v5 相比 3.x/4.x 的核心变化,正是前面各版本演进方向的"集大成":
7.1 校验逻辑收拢进Validator
v5 将所有 claims 校验从各 claims 类型中抽出,统一放进 validator.go 的Validator结构体。Parser在解析后自动调用它,校验顺序固定为:exp→nbf→iat(可选)→aud→iss→sub→ 自定义ClaimsValidator.Validate()。
7.2 ParserOption:可组合的校验选项
所有校验开关都以函数式选项暴露(见 parser_option.go),对应版本历史中"可配置解析参数"这一线索的最终形态:
| 选项 | 作用 |
|---|---|
WithValidMethods([]string) | 算法白名单,防御算法混淆攻击(源自 2.4.0 的 Parser) |
WithLeeway(time.Duration) | 时间类 claim 的时钟偏移容忍窗口 |
WithExpirationRequired()/WithNotBeforeRequired() | 将exp/nbf从可选变为必选 |
WithIssuedAt() | 启用iat校验(v5 起默认不校验 iat,因其按 RFC 仅具参考意义) |
WithAudience(...)/WithAllAudiences(...) | 期望的受众(aud),前者命中其一即可,后者要求全部命中 |
WithIssuer(iss)/WithSubject(sub) | 期望的签发者 / 主体 |
WithJSONNumber() | 用json.Number解析数字(源自 2.6.0 的修复) |
WithoutClaimsValidation() | 跳过 claims 校验(仅限明确知晓后果的场景) |
WithTimeFunc(func() time.Time) | 注入时间函数,主要用于测试 |
WithStrictDecoding()/WithPaddingAllowed() | 控制 base64url 解码的严格模式与是否允许 padding(源自 3.1.0 的SkipClaimsValidation与 2.4.0 的解析配置) |
典型组合用法:
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, keyFunc, jwt.WithValidMethods([]string{"HS256", "EdDSA"}), jwt.WithLeeway(30*time.Second), jwt.WithAudience("mimir", "prometheus"), jwt.WithIssuer("https://issuer.example.com"), )7.3 自定义 claims 与扩展校验
v5 支持通过ClaimsValidator接口追加应用级校验,且无法因自定义Validate()而绕过标准校验(错误会被追加,而非替代):
type MyCustomClaims struct { Foo string `json:"foo"` jwt.RegisteredClaims } func (m MyCustomClaims) Validate() error { if m.Foo != "bar" { return errors.New("must be foobar") } return nil }7.4 错误体系
v5 将错误整理为 errors.go 中一组可判定的哨兵错误,覆盖签名无效(ErrTokenSignatureInvalid)、token 过期(ErrTokenExpired)、受众/签发者/主体无效、claim 缺失(ErrTokenRequiredClaimMissing)等场景,并支持errors.Is/errors.Join风格的多错误展开。这也回应了版本历史中"错误信息更细化、更具体"的持续改进。
7.5 签名算法全景
结合版本历史与 v5 源码,当前库内可用的签名方法及其密钥类型如下:
算法(alg) | 实现文件 | 密钥要求(签名/验证) |
|---|---|---|
| HS256 / HS384 / HS512 | hmac.go | []byte(对称) |
| RS256 / RS384 / RS512 | rsa.go | *rsa.PrivateKey/*rsa.PublicKey |
| PS256 / PS384 / PS512 | rsa_pss.go | *rsa.PrivateKey/*rsa.PublicKey |
| ES256 / ES384 / ES512 | ecdsa.go | *ecdsa.PrivateKey/*ecdsa.PublicKey |
| EdDSA | ed25519.go | ed25519.PrivateKey/ed25519.PublicKey |
| none | none.go | 仅UnsafeAllowNoneSignatureType常量(不推荐) |
其中none与 EdDSA 分别对应版本历史 2.5.0 与 3.2.2 的引入记录;HMAC 实现自 2.0.0 起即为统一的SigningMethodHMAC类型。
八、对 Mimir 使用者的启示
虽然 Mimir 将 jwt 库作为间接依赖使用,但了解其版本历史对排查问题仍有直接帮助:
- 安全基线:CVE-2020-26160 与 3.2.2 的日期校验修复意味着不要回退到低于 3.2.1 / 3.2.2 的版本;当前 vendored 的 v5.3.1 已包含全部历史安全修复。
- 校验默认行为:v5 默认不校验
iat、aud、iss、sub,也不强制要求exp存在。若业务需要对aud做白名单,必须显式传入WithAudience——这是 3.2.1 修复后保持的安全姿势。 - 算法白名单:解析不受信任来源的 token 时,务必使用
WithValidMethods固定算法集合,这正是 2.4.0 引入白名单机制的初衷。 - 防御 none 攻击:
none算法自 2.5.0 起就被刻意设计为"难用",v5 中必须显式传入魔法常量才可启用;任何业务代码都不应使用它。
九、总结
VERSION_HISTORY.md表面上是一份 changelog,实质上是一部 JWT 库的 API 设计史:从 1.0.0 的 RS256/HS256 双算法,到 2.0.0 的密钥类型泛化,再到 3.0.0 的Claims接口革命,最终沉淀为 Mimir 仓库中 vendored 的 v5 形态——统一的Claimsgetter 接口、集中的Validator、可组合的ParserOption、类型安全的错误体系与完整的算法家族。理解这条演进线索,就能明白 v5 API 中每一个"看起来奇怪"的设计(如none的魔法常量、默认不校验iat、aud白名单需显式声明)都是历史教训的产物。读者可在 vendor/github.com/golang-jwt/jwt/v5/ 目录下对照源码继续深入,也可结合 MIGRATION_GUIDE.md 完成从旧版本向 v5 的实际迁移。
- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
相关推荐
remark42 依赖的 golang-jwt/jwt(jwt-go)版本演进全史:从 1.0.0 到 v5 的 API 变迁与安全修复
remark42 依赖的 golang jwt/jwt(jwt go)版本演进全史:从 1.0.0 到 v5 的 API 变迁与安全修复 导读 本文以 rema
后端前端golang-jwt/jwt(jwt-go)版本演进全解析:从 1.0.0 到 v5 的 API 变迁与安全修复
golang jwt/jwt(jwt go)版本演进全解析:从 1.0.0 到 v5 的 API 变迁与安全修复 golang jwt/jwt (社区通称 jw
网络安全kOps 仓库中的 golang-jwt/jwt(jwt-go)版本演进史:从 v1.0.0 到 v5 的 API 变迁与安全修复全解
kOps 仓库中的 golang jwt/jwt(jwt go)版本演进史:从 v1.0.0 到 v5 的 API 变迁与安全修复全解 本文以当前仓库中 ven
云原生集群管理运维IaC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考