jwt

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 10, 2026 License: MIT Imports: 11 Imported by: 0

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

View Source
const TokenTypeClaim = "token_type"

TokenTypeClaim 是双令牌对的分型声明名(#97):Manager 签发的每个令牌都自动 携带该声明(值 TokenAccess="access" / TokenRefresh="refresh"),解析后收进 Claims.Extra。业务声明不得占用该键(reservedClaims,#71 同款注入面防护)。

Variables

View Source
var ErrInvalidToken = errors.New("jwt: invalid token")

ErrInvalidToken 表示令牌无法解析或校验失败。

Functions

func MarshalKeyPairPEM

func MarshalKeyPairPEM(kp *KeyPair) (privPEM, pubPEM string, err error)

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

func ParsePublicKey(pemBytes []byte) (*rsa.PublicKey, error)

ParsePublicKey 解析 PEM 公钥(PKIX / PKCS#1),也接受 X.509 证书。

func Sign

func Sign(kp *KeyPair, opts ...SignOption) (string, error)

Sign 用 RS256 签发令牌。缺省 iat = time.Now()、ttl = 24h;可用 WithNow/ WithIssuedAt 注入签发时刻(iat 与 exp 同钟源),WithNoExpiry 显式去掉 exp。 对称形态(HS256 共享密钥)见 SymmetricSigner(#86)。

Types

type Claims

type Claims struct {
	gojwt.RegisteredClaims
	// Extra 承载业务自定义声明(签发时经 WithClaim 平铺进 payload,解析时收集回来)。
	Extra map[string]any
}

Claims 是签发与解析的载荷容器。

func Parse

func Parse(token string, pub *rsa.PublicKey, opts ...ParseOption) (*Claims, error)

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

func GenerateKeyPair(bits int) (*KeyPair, error)

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

func NewManagerHS256(secret []byte) (*Manager, error)

NewManagerHS256 用共享密钥构造 HS256 形态 Manager(密钥强度判据与 NewSymmetric 完全一致:≥ 32 字节,RFC 7518 §3.2)。

func NewManagerRS256 added in v0.6.0

func NewManagerRS256(kp *KeyPair) (*Manager, error)

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

func (m *Manager) ParseTyped(kind TokenKind, token string, opts ...ParseOption) (*Claims, error)

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)。

func WithLeeway

func WithLeeway(d time.Duration) ParseOption

WithLeeway 设置时钟偏移容忍。

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 WithIssuer

func WithIssuer(iss string) SignOption

WithIssuer 设置 iss。

func WithJTI

func WithJTI(jti string) SignOption

WithJTI 设置 jti(令牌 ID)。

func WithNoExpiry added in v0.4.2

func WithNoExpiry() SignOption

WithNoExpiry 显式签发无 exp 的令牌(#65):名字本身即审计信号,grep 得到。 无 exp 令牌一旦泄漏没有自然失效路径,仅供确有轮换/吊销机制的场景;与 WithTTL 并用时后应用的生效。

func WithNotBefore

func WithNotBefore(t time.Time) SignOption

WithNotBefore 设置 nbf。

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 WithSubject

func WithSubject(sub string) SignOption

WithSubject 设置 sub。

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。

type TokenKind added in v0.6.0

type TokenKind string

TokenKind 令牌分型(IssuePair 的产物类型 / ParseTyped 的校验目标)。

const (
	// TokenAccess 访问令牌分型(短 ttl,直接面向资源请求)。
	TokenAccess TokenKind = "access"
	// TokenRefresh 刷新令牌分型(长 ttl,只用于换发新令牌对)。
	TokenRefresh TokenKind = "refresh"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL