Documentation
¶
Overview ¶
HS256 对称形态(#86):与 RS256(jwt.go)并列的第二种主流 JWT 形态——共享密钥 HMAC。内网 SSO / 网关注入 / 服务间对等约定场景没有证书分发问题,IdP 与资源服务 约定一个 secret 即可;此前该场景下游各自手搓三段式解析 + HMAC 校验 + claims 校验(alg 混淆、hmac.Equal 常量时间比较、aud 双形态解析等坑每份实现各踩一遍), 库侧一份正确实现收编整个面。
Package jwt 提供 JWT 签发与校验,覆盖两种主流形态:RS256(非对称密钥对, Sign/Parse)与 HS256(对称共享密钥,SymmetricSigner/ParseSymmetric,#86)。 两形态之上另有 access+refresh 双令牌对原语(Manager:成对签发 IssuePair、 分型校验 ParseTyped、refresh 轮换 Refresh,#97)。
两形态共享同一套 SignOption/ParseOption/Claims 与校验语义;每个入口固定自己 的算法(RS256 入口只认 RS256、HS256 入口只认 HS256),密钥形态绑定算法关死 alg 混淆。解析侧校验入口:WithAudience(#66)与 WithExpectedIssuer(#95); 签发侧 aud 出口:WithAudienceClaims(#117)——与 WithAudience 闭环,库签发的 令牌可带 aud,多受众部署不再被迫手搓 payload。
硬契约(与既有消费方互通,不可改):
- RS256 形态 alg 恰为 RS256;HS256 形态 alg 恰为 HS256
- JWT header 只含 {"alg":"<alg>"},不带 typ
- 业务自定义声明平铺进 payload,解析时收集进 Claims.Extra
access+refresh 双令牌对原语(#97):OAuth 事实标准的成对签发/分型校验/refresh 轮换此前没有矩阵级口径,下游(如 zeus go-common/jwt)整包坐在 golang-jwt 上 手搓——alg 检查还是宽 HMAC(接受 HS256/384/512 任意 HMAC)而非本包的 WithValidMethods 单算法钉法,同一矩阵两个 JWT 层连算法口径都不一致。本文件把 token_type 分型声明约定、成对签发、分型校验与 refresh 轮换钩子收编进库层, 平台层退役为薄配置(撤销黑名单等有状态部分仍在平台层)。
Index ¶
- Constants
- Variables
- func MarshalKeyPairPEM(kp *KeyPair) (privPEM, pubPEM string, err error)
- func ParsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error)
- func ParsePublicKey(pemBytes []byte) (*rsa.PublicKey, error)
- func Sign(kp *KeyPair, opts ...SignOption) (string, error)
- type Claims
- type KeyPair
- type Manager
- func (m *Manager) IssuePair(subject string, ttlAccess, ttlRefresh time.Duration, opts ...SignOption) (access, refresh string, err error)
- func (m *Manager) ParseTyped(kind TokenKind, token string, opts ...ParseOption) (*Claims, error)
- func (m *Manager) Refresh(refreshToken string, ttlAccess, ttlRefresh time.Duration, ...) (access, refresh string, err error)
- type ParseOption
- type SignOption
- func WithAudienceClaims(aud ...string) SignOption
- func WithClaim(key string, val any) SignOption
- func WithIssuedAt(t time.Time) SignOption
- func WithIssuer(iss string) SignOption
- func WithJTI(jti string) SignOption
- func WithNoExpiry() SignOption
- func WithNotBefore(t time.Time) SignOption
- func WithNow(f func() time.Time) SignOption
- func WithSubject(sub string) SignOption
- func WithTTL(d time.Duration) SignOption
- type SymmetricSigner
- type TokenKind
Constants ¶
const TokenTypeClaim = "token_type"
TokenTypeClaim 是双令牌对的分型声明名(#97):Manager 签发的每个令牌都自动 携带该声明(值 TokenAccess="access" / TokenRefresh="refresh"),解析后收进 Claims.Extra。业务声明不得占用该键(reservedClaims,#71 同款注入面防护)。
Variables ¶
var ErrInvalidToken = errors.New("jwt: invalid token")
ErrInvalidToken 表示令牌无法解析或校验失败。
Functions ¶
func MarshalKeyPairPEM ¶
MarshalKeyPairPEM 把密钥对编为 PEM(私钥 PKCS#1,公钥 PKIX)。
空值检查对称(#96):Private 与 Public 任一缺失都返回 error——手动构造 KeyPair 是合法用法(从已有私钥文件加载、只用一半做测试),缺 Public 时此前 直接 nil 指针 panic(x509.MarshalPKIXPublicKey 解引用 pub.N),与同包 NewSymmetric/GenerateKeyPair 的「非法输入返回 error」风格不一致。
func ParsePrivateKey ¶
func ParsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error)
ParsePrivateKey 解析 PEM 私钥(PKCS#1 / PKCS#8)。
func ParsePublicKey ¶
ParsePublicKey 解析 PEM 公钥(PKIX / PKCS#1),也接受 X.509 证书。
Types ¶
type Claims ¶
type Claims struct {
gojwt.RegisteredClaims
// Extra 承载业务自定义声明(签发时经 WithClaim 平铺进 payload,解析时收集回来)。
Extra map[string]any
}
Claims 是签发与解析的载荷容器。
func Parse ¶
Parse 校验并解析令牌(RS256-only,缺省零 leeway、不校验 aud/iss)。对称形态 (HS256 共享密钥)见 ParseSymmetric(#86)——两入口共享校验语义。
错误链保留(#70):失败时外层哨兵 ErrInvalidToken 与 gojwt 底层 sentinel (ErrTokenExpired/ErrTokenSignatureInvalid/ErrTokenNotValidYet 等)同时可 errors.Is——业务侧可据此分流「过期→走刷新、签名坏→踢下线」。
已注册声明(iss/sub/aud/jti/exp/iat/nbf)收进 RegisteredClaims;其余进 Extra。 iss/sub/jti 存在但非字符串时拒绝(#67):RFC 7519 §4.1.1/§4.1.2 要求 iss/sub MUST be a String,类型异常即规格违规令牌,fail-closed 而非静默归零放行 (golang-jwt 上游 MapClaims.GetSubject 对类型异常同样返回错误);声明缺失 维持零值不报错——缺失声明的令牌按消费方自身的校验责任走。
func ParseSymmetric ¶ added in v0.5.0
func ParseSymmetric(token string, secret []byte, opts ...ParseOption) (*Claims, error)
ParseSymmetric 校验并解析 HS256 令牌——与 Parse(RS256)同构(#86):同 Claims (注册声明收进 RegisteredClaims、其余进 Extra)、同 ParseOption(WithLeeway/ WithAudience/#66、WithExpectedIssuer/#95)、同错误链(外层哨兵 ErrInvalidToken 与 gojwt 底层 sentinel ErrTokenExpired/ErrTokenSignatureInvalid 等双可 errors.Is,#70)。
alg 混淆防护(#86):入口绑定算法——gojwt.WithValidMethods(["HS256"]) 单算法 白名单,RS256 令牌(无论密钥对真假)在本入口一律拒绝;RS256 的 Parse 同理只认 RS256。密钥形态绑定算法,杜绝「用 HMAC 密钥校验非对称签名」的经典 CVE 形态。
type KeyPair ¶
type KeyPair struct {
Private *rsa.PrivateKey
Public *rsa.PublicKey
}
KeyPair 持有 RS256 的密钥对。
func GenerateKeyPair ¶
GenerateKeyPair 生成 RSA 密钥对(bits < 2048 时按 2048 处理)。
type Manager ¶ added in v0.6.0
type Manager struct {
// contains filtered or unexported fields
}
Manager 是 access+refresh 双令牌管理器:把某一生涯(同一签名密钥)的成对 签发(IssuePair)、分型校验(ParseTyped)与 refresh 轮换(Refresh)集中在 库层。零内部可变状态,可并发使用。
形态与单令牌原语一致——两构造器分别绑定 RS256 / HS256,入口钉死单算法 (WithValidMethods 白名单),Manager 签发/校验的 alg 硬契约与 Sign/Parse、 SymmetricSigner/ParseSymmetric 完全相同。
算法配置口说明(#97 审计注记):**不导出多算法配置口**(HS384/512 等)—— 「密钥形态绑定单一算法」是防 alg 混淆的硬契约,宽 HMAC 白名单(下游手搓层 的现状)正是要收编的反面教材;确需其它算法属新形态,应像 HS256 一样以独立 入口 + 独立密钥形态交付,而不是给现有入口开白名单。
func NewManagerHS256 ¶ added in v0.6.0
NewManagerHS256 用共享密钥构造 HS256 形态 Manager(密钥强度判据与 NewSymmetric 完全一致:≥ 32 字节,RFC 7518 §3.2)。
func NewManagerRS256 ¶ added in v0.6.0
NewManagerRS256 用 RSA 密钥对构造 RS256 形态 Manager。
构造期 fail-fast(与 NewSymmetric 同一哲学):Private 与 Public 任一缺失直接 报错——Sign 只需 Private、Parse 只需 Public,Manager 两者都做,缺一半要到 第一次签发/校验才炸出来毫无收益(#96 同款对称检查纪律)。
func (*Manager) IssuePair ¶ added in v0.6.0
func (m *Manager) IssuePair(subject string, ttlAccess, ttlRefresh time.Duration, opts ...SignOption) (access, refresh string, err error)
IssuePair 成对签发 access+refresh 双令牌:同 subject,各自带 token_type 分型 声明与自动生成的 jti(32 hex,两令牌各自独立,未经显式 WithJTI 覆盖时)。 ttlAccess / ttlRefresh 分别是两个令牌的有效期(必须为正——配置零值透传签出 永不过期令牌是最危险的缺省,与 WithTTL(0) panic(#65)同一纪律,此处以 error 而非 panic 表达:IssuePair 有 error 出口)。
opts 透传既有 SignOption(如 WithIssuer 钉签发者、WithAudienceClaims 钉受众、 WithClaim 带会话数据、WithNow/WithIssuedAt 注入签发时钟),两令牌共享同一份 选项;其中 subject 与 ttl 由位置参数拍板(用户同名选项 last-wins 覆盖之), token_type 由 Manager 盖章(WithClaim("token_type", ...) 直接被 #71 注入面 防护拒绝)。
返回的 access/refresh 都是普通 JWT 字符串:直接用 Parse/ParseSymmetric 也能 解(token_type 落在 Extra),但按分型消费请走 ParseTyped——分型校验正是本 原语的存在意义。
func (*Manager) ParseTyped ¶ added in v0.6.0
ParseTyped 分型校验并解析令牌:在既有校验语义(签名、exp/nbf、可选 aud/iss 钉定,ParseOption 全量透传)之上,额外强制 token_type 分型声明——缺失、非 字符串或与期望分型不符一律拒绝(fail-closed)。错误链与 Parse 一致:外层 ErrInvalidToken 双可 errors.Is。
非 Manager 签发(无 token_type 声明)的令牌在此一律拒绝:分型校验的前提是 令牌自证分型,签名只证明「是我的 key 签的」。
func (*Manager) Refresh ¶ added in v0.6.0
func (m *Manager) Refresh(refreshToken string, ttlAccess, ttlRefresh time.Duration, onRotate func(old *Claims), opts ...ParseOption) (access, refresh string, err error)
Refresh 用合法 refresh 令牌轮换出新令牌对(refresh token rotation):分型 校验旧令牌(token_type=refresh,ParseOption 经 opts 透传)→ 以旧令牌的 身份与会话上下文(sub/iss/aud/业务声明 Extra)签发新对(分型重新盖章、jti 重新生成、exp/iat 重新计算)→ 成功后以旧 refresh 的 Claims 调用 onRotate 轮换钩子(可为 nil;同步调用,旧令牌的 Claims.ID 即待吊销的 jti)。
钩子只负责「旧令牌失效化」的落地(撤销黑名单/存储仍在平台层),库层保证 时序:新对签发成功之后才触发。轮换语义本身是无状态的——旧 refresh 的作废 依赖平台层在钩子里记录 jti 并在后续 ParseTyped 通路外拒绝(库不维护黑名单, #97:有状态撤销是平台层的薄配置职责)。
type ParseOption ¶
type ParseOption func(*parseOptions)
ParseOption 是 Parse 的函数选项。
func WithAudience ¶ added in v0.4.2
func WithAudience(expected ...string) ParseOption
WithAudience 设置期望受众(#66):令牌 aud 命中任一即通过;令牌无 aud 时 拒绝(fail-closed)。不设置时不校验 aud(向后兼容)——签名校验只证明 「是我的 key 签的」,不证明「是签给我的」;同密钥多受众场景必须显式配置。
func WithExpectedIssuer ¶ added in v0.6.0
func WithExpectedIssuer(expected string) ParseOption
WithExpectedIssuer 设置期望签发者(#95):令牌 iss 必须与之严格相等才通过; 令牌无 iss 或 iss 不匹配 ⇒ 拒绝(fail-closed)。不设置时不校验 iss(向后 兼容)——与 WithAudience 同一口径:签名校验只证明「是我的 key 签的」,不 证明「是约定的签发方签的」;共享密钥(HS256)多签发方场景下,任意持钥方 都能签出 iss 为任意值的令牌,必须显式钉住来源。
名字不是 WithIssuer:签发侧已有 WithIssuer(iss) SignOption(写入 iss), Go 不允许同名重载;解析侧取「期望」语义与 WithAudience(期望受众)对称。 上游 golang-jwt 的校验是严格相等 + iss 必须存在(缺失报 ErrTokenRequiredClaimMissing、不匹配报 ErrTokenInvalidIssuer、类型异常报 ErrTokenUnverifiable,均可经错误链 errors.Is,#70)。
type SignOption ¶
type SignOption func(*signOptions)
SignOption 是 Sign 的函数选项。
func WithAudienceClaims ¶ added in v0.7.0
func WithAudienceClaims(aud ...string) SignOption
WithAudienceClaims 设置 aud(受众)(#117):单受众写 string 形态、多受众写 []string 形态,与 claimsFromMap 的解析面(string/数组双收)对称(RFC 7519 §4.1.3 双形态)。不设置时令牌不带 aud(向后兼容);零参与不设置同效(显式 回退默认值);受众含空串直接 panic——空受众是配置零值透传的最常见形态,签出 的令牌在 WithAudience(#66)fail-closed 校验下永不匹配,静默接受等于批量产出 必被拒的令牌(WithTTL(0) panic(#65)同款纪律)。重复调用 last-wins(与 WithIssuer 等注册声明选项同口径)。入参即刻 slices.Clone 拷贝,调用方事后 改写原切片不影响签发结果。
名字不是 WithAudience:解析侧已有 WithAudience(...string) ParseOption(校验 期望受众),Go 不允许同名重载,此处取「写入 aud 声明」语义与之分型(与 WithExpectedIssuer(#95)同款命名分型纪律)。aud 是保留声明(reservedClaims), 不可经 WithClaim 注入(#71)——本选项是 aud 的唯一签发出口。
func WithClaim ¶
func WithClaim(key string, val any) SignOption
WithClaim 追加一个业务自定义声明。
保留声明(iss/sub/aud/exp/nbf/iat/jti,及 #97 起的双令牌分型名 token_type) 不可经 WithClaim 注入,注入时 Sign 直接报错(#71):Sign 先写保留声明、最后 平铺 claims,无条件覆盖会让 WithClaim("exp", ...) 顶掉过期时间、 WithClaim("iss", externalInput) 篡改签发者、WithClaim("token_type", ...) 伪造分型——claim key 若来自配置/请求拼接,就是令牌声明注入面。注册声明请用 WithIssuer/WithSubject/WithAudienceClaims/WithJTI/WithNotBefore 设置, exp/iat 由 WithTTL 与签发时钟派生,token_type 由 Manager(#97)盖章。
func WithIssuedAt ¶ added in v0.4.2
func WithIssuedAt(t time.Time) SignOption
WithIssuedAt 显式设置签发时刻:iat = t、exp = t + ttl,等价于 WithNow( 固定返回 t)。带显式签发时刻的签发面(如消费方 Issue(..., now time.Time) 参数形态)委托本包时用它透传 now,缺省行为不变(#64)。
func WithNoExpiry ¶ added in v0.4.2
func WithNoExpiry() SignOption
WithNoExpiry 显式签发无 exp 的令牌(#65):名字本身即审计信号,grep 得到。 无 exp 令牌一旦泄漏没有自然失效路径,仅供确有轮换/吊销机制的场景;与 WithTTL 并用时后应用的生效。
func WithNow ¶ added in v0.4.2
func WithNow(f func() time.Time) SignOption
WithNow 注入签发时钟:iat = now()、exp = iat + ttl 同钟源(nbf 仍取 WithNotBefore 的绝对时刻)。缺省 time.Now(#64:缺省行为不变);f 为 nil 直接 panic——静默回落真实时钟会让 iat 无声漂移。测试可注入假时钟获得 确定性令牌,不再只能断言 iat≈now 的区间。
func WithTTL ¶
func WithTTL(d time.Duration) SignOption
WithTTL 设置 exp = iat + ttl。d 为 0 直接 panic(#65):显式 0 与「未设置」 不可区分,而最可能的成因是「配置缺失的零值透传」——静默接受等于放行一批 永不过期的凭据,与 uidgen.RandomHex 拒绝空串密钥(#50)同款纪律。确需无 过期令牌请显式 WithNoExpiry。负值不拦(既有消费方用负 ttl 构造已过期令牌 做校验测试)。
type SymmetricSigner ¶ added in v0.5.0
type SymmetricSigner struct {
// contains filtered or unexported fields
}
SymmetricSigner 持有 HS256 共享密钥的签发器(对称形态,与 KeyPair/RS256 并列)。 签发选项与 RS256 形态同一套 SignOption,签发语义完全一致(#86)。
func NewSymmetric ¶ added in v0.5.0
func NewSymmetric(secret []byte) (*SymmetricSigner, error)
NewSymmetric 用共享密钥构造 HS256 签发器。
secret 缺失或长度 < 32 字节(RFC 7518 §3.2 下限)直接报错 fail-fast(#86): 与全仓「配置错误宁可启动即暴露」哲学一致,不能等到首个令牌校验时才见分晓, 也不设「静默接受短密钥」的兼容路径。
func (*SymmetricSigner) Sign ¶ added in v0.5.0
func (s *SymmetricSigner) Sign(opts ...SignOption) (string, error)
Sign 用 HS256 签发令牌:选项与签发语义(缺省 ttl=24h、iat/exp 同钟源、注册 声明注入面防护 #71)与 RS256 的 Sign 完全同一套(#86)。header 硬契约同形态: 只含 {"alg":"HS256"},不带 typ。