4. JWT Header详解

JWT(JSON Web Token)的 Header(头部)是令牌的第一部分,通常是一个 Base64URL 编码的 JSON 对象。它承载了令牌的元数据,包括签名算法、令牌类型等关键信息。服务端在验证 JWT 时,首先解析 Header 以确定使用何种算法进行签名校验。理解 Header 的结构与字段,是正确实现 JWT 鉴权的基础。

4.1 Header 标准字段

JWT 规范(RFC 7519)定义了两个标准字段,所有 JWT 实现都应支持:

字段名 全称 必选 说明
alg Algorithm 是 签名或加密算法名称,如 HS256、RS256、none
typ Type 否(推荐) 令牌媒体类型,固定为 JWT
最佳实践: 即使 typ 字段非强制,也建议始终包含 "typ": "JWT"。这有助于中间件或网关在混合令牌场景中快速识别 JWT,避免与其他令牌格式(如 SAML、Paseto)混淆。

4.1.1 alg 字段详解

alg 是 Header 中唯一必须存在的字段。它告诉验证方该 JWT 使用了哪种签名或加密算法。常见的 alg 取值包括:

  • HS256:HMAC with SHA-256,对称签名算法。
  • HS384:HMAC with SHA-384。
  • HS512:HMAC with SHA-512。
  • RS256:RSA with SHA-256,非对称签名算法。
  • RS384:RSA with SHA-384。
  • RS512:RSA with SHA-512。
  • ES256:ECDSA with P-256 and SHA-256。
  • none:不签名(仅用于调试,生产环境禁止使用)。

4.1.2 typ 字段详解

typ 字段用于声明令牌的媒体类型。根据 RFC 7519,其值应设置为 "JWT"。在某些复合结构中(如 JWE 嵌套 JWS),typ 可以帮助解析器区分内部令牌类型。一个典型的 Header 示例:

{
  "alg": "HS256",
  "typ": "JWT"
}

4.2 常见签名算法:HS256 与 RS256

在 Web 接口安全中,HS256 和 RS256 是最广泛使用的两种签名算法。它们分别代表对称与非对称签名体系,在密钥管理、性能、安全性方面有显著差异。

对比维度 HS256(HMAC-SHA256) RS256(RSA-SHA256)
密钥类型 对称密钥(同一个密钥签名和验证) 非对称密钥(私钥签名,公钥验证)
密钥分发 需安全共享密钥给所有验证方 公钥可公开分发,私钥仅签发方持有
性能 签名和验证都很快(计算量小) 签名较慢,验证较快(但比 HS256 慢)
适用场景 单一服务内部、微服务间信任网络 第三方分发、开放 API、多服务独立验证
安全风险 密钥泄露后所有方均可伪造令牌 私钥泄露风险仅影响签发方
典型 Header "alg":"HS256" "alg":"RS256"
安全警告: 使用 HS256 时,务必确保密钥强度足够(至少 256 位随机字节),并且通过安全的渠道(如 Vault、KMS)分发给所有验证服务。切勿将密钥硬编码在代码仓库中。对于多服务架构,RS256 通常是更安全的选择,因为公钥可以安全地通过公开端点分发。

4.3 自定义 Header

除了标准字段,JWT Header 允许添加自定义字段,用于携带与令牌处理相关的元数据。自定义字段通常以 非冲突命名 为原则,避免与标准字段或未来扩展冲突。常见做法是使用前缀或特定命名空间。

4.3.1 常见自定义字段示例

自定义字段 含义 示例值
kid Key ID,用于标识使用哪把密钥签名 "kid": "rsa-key-v2"
cty Content Type,嵌套 JWT 时声明内部内容类型 "cty": "JWT"
crit Critical,声明哪些 Header 字段必须被理解 "crit": ["exp", "kid"]
jku JWK Set URL,指向公钥集合的 URL "jku": "https://auth.example.com/.well-known/jwks.json"
x5c X.509 Certificate Chain,嵌入证书链 "x5c": ["MIIE..."]

4.3.2 自定义 Header 的 Go 实现示例

在 Go 中使用 github.com/golang-jwt/jwt/v5 库时,可以通过自定义结构体扩展 Header:

package main

import (
    "fmt"
    "github.com/golang-jwt/jwt/v5"
)

// 自定义 Claims 结构体
type CustomClaims struct {
    UserID   string `json:"user_id"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

// 自定义 Header 字段
type CustomHeader struct {
    jwt.Header
    KeyID string `json:"kid,omitempty"`
    AppID string `json:"app_id,omitempty"`
}

func main() {
    // 创建自定义 Header
    header := CustomHeader{
        Header: jwt.Header{
            Algorithm: "RS256",
            Type:      "JWT",
        },
        KeyID: "my-rsa-key-2024",
        AppID: "gateway-v3",
    }

    // 创建 Claims
    claims := CustomClaims{
        UserID: "u-10086",
        Role:   "admin",
        RegisteredClaims: jwt.RegisteredClaims{
            Issuer: "auth-service",
        },
    }

    // 使用自定义 Header 和 Claims 生成 Token
    token := jwt.NewWithClaims(header, claims)
    tokenString, err := token.SignedString(privateKey)
    if err != nil {
        panic(err)
    }
    fmt.Println(tokenString)
}
关键点: 自定义 Header 字段必须被验证方理解,否则应使用 crit 字段声明。如果验证方遇到 crit 中列出的字段但不支持,必须拒绝该令牌。这防止了因忽略关键 Header 字段而导致的安全漏洞。

4.4 JWT Header 结构总览

下图展示了 JWT Header 的标准字段、常见算法选择以及自定义扩展的整体结构:

JWT Header 结构总览 JWT Header 结构总览 JWT Header(JSON 对象) 标准字段 alg(必选) typ(推荐) cty(嵌套时使用) 签名算法 HS256(对称) RS256(非对称) ES256 / EdDSA 自定义字段 kid(密钥标识) crit(关键声明) jku / x5c Header 经 Base64URL 编码后成为 JWT 的第一部分 格式:Base64URL(Header) . Base64URL(Payload) . Signature

4.5 算法选择决策流程

在实际项目中,选择 HS256 还是 RS256 需要根据架构和安全需求权衡。以下决策流程可以帮助你做出选择:

签名算法选择决策流程 签名算法选择决策流程 选择签名算法 多服务验证? 是 RS256 非对称签名 否 HS256 对称签名 注意事项 • HS256:密钥必须安全共享,适合内部微服务(同一信任域) • RS256:公钥可公开,适合开放 API、第三方集成、多服务独立验证 • 生产环境禁止使用 alg: none • 始终验证 alg 字段是否在白名单内,防止算法混淆攻击

4.6 安全最佳实践

  • 算法白名单验证: 在验证 JWT 时,必须检查 alg 字段是否在服务端允许的算法列表中。防止攻击者将 alg 改为 none 或弱算法绕过签名验证。
  • 避免算法混淆攻击: 如果服务端支持 RS256,但攻击者将 alg 改为 HS256 并使用公钥作为 HMAC 密钥签名,可能导致验证通过。解决方案:始终根据 alg 字段选择对应的验证方法,不要混用。
  • 使用 kid 支持密钥轮换: 在 Header 中包含 kid 字段,服务端根据 kid 查找对应的密钥进行验证。这允许在不中断服务的情况下轮换密钥。
  • 最小化自定义字段: 只在 Header 中放置与令牌处理直接相关的元数据。业务数据应放在 Payload 中,而非 Header。
Go 实现提示: 使用 jwt.NewWithClaims() 时,第一个参数可以是实现了 jwt.SigningMethod 接口的对象,也可以是包含 Algorithm 字段的结构体。推荐始终显式指定算法,避免依赖默认值。在验证端,使用 jwt.ParseWithClaims() 并传入自定义的 KeyFunc,在 KeyFunc 中检查 token.Header["alg"] 是否在允许列表中。