Posted in

Go JWT常见失效场景大起底:签名篡改、时钟漂移、密钥轮换断裂…你中了几个?

第一章:JWT 基础与 Go 生态中的标准实践

JSON Web Token(JWT)是一种紧凑、自包含的开放标准(RFC 7519),用于在各方之间安全地传输声明(claims)。其典型结构由三部分组成:Header(含签名算法与类型)、Payload(含注册声明如 expiss,以及自定义声明)、Signature(对前两部分 Base64Url 编码后使用密钥签名)。JWT 的无状态性与可验证性使其成为现代 API 认证与授权的事实标准。

在 Go 生态中,github.com/golang-jwt/jwt/v5 是当前官方推荐的 JWT 库(v5 版本已从旧版 dgrijalva/jwt-go 迁移并修复了关键安全漏洞)。它严格遵循 RFC 规范,支持 HS256、RS256、ES256 等多种签名算法,并提供清晰的错误分类(如 jwt.ErrTokenExpiredjwt.ErrTokenMalformed)。

生成一个 HS256 签名的 JWT 示例:

package main

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

func main() {
    // 构建声明载荷
    claims := jwt.MapClaims{
        "sub": "user_123",
        "email": "alice@example.com",
        "exp": time.Now().Add(2 * time.Hour).Unix(), // 必须为 int64 时间戳
    }

    // 创建 token 实例,指定签名方法
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    // 使用密钥签名(生产环境应使用环境变量或密钥管理服务)
    secret := []byte("super-secret-key-change-in-prod")
    signedString, err := token.SignedString(secret)
    if err != nil {
        panic(err)
    }

    fmt.Println("Generated JWT:", signedString)
}

验证 JWT 时需显式指定密钥函数并校验错误类型:

验证阶段 推荐做法
解析 token 使用 jwt.Parse() 并传入 jwt.KeyFunc
错误处理 区分 *jwt.ValidationError 类型以响应不同失败原因(如过期、签名无效)
声明校验 启用 VerifyExpVerifyIat 等选项,或手动检查 token.Claims.(jwt.MapClaims)

Go 社区普遍采用中间件模式集成 JWT 验证,例如在 Gin 框架中通过 gin.HandlerFunc 提取 Authorization: Bearer <token> 并调用 token.Valid()。始终避免硬编码密钥,优先使用 os.Getenv("JWT_SECRET")gopkg.in/yaml.v3 加载配置。

第二章:签名篡改攻防全景解析

2.1 JWT 签名机制原理与 HS256/RS256 数学本质剖析

JWT 签名并非加密,而是可验证的完整性证明:Header 和 Payload 经 Base64Url 编码拼接后,输入签名算法生成固定长度摘要,并与密钥绑定。

对称签名:HS256 的 HMAC 构造

import hmac, hashlib
message = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0"
secret = b"my-secret-key"
signature = hmac.new(secret, message, hashlib.sha256).digest()  # 输出32字节

hmac.new(key, msg, digestmod) 本质是两次 SHA-256 嵌套哈希(IPAD/OPAD 变换),安全性依赖密钥保密性与抗碰撞性。

非对称签名:RS256 的 RSA-PKCS#1 v1.5

组件 作用 数学基础
私钥签名 对 SHA-256 摘要执行 S ≡ H^d mod N 模幂运算,离散对数难解
公钥验签 验证 H ≡ S^e mod N 是否匹配原始摘要 依赖 RSA 陷门单向函数
graph TD
    A[JWT Header.Payload] --> B[SHA-256 → H]
    B --> C[HS256: HMAC-SHA256 H with shared key]
    B --> D[RS256: RSA-Sign H with private key]
    C --> E[对称可信域]
    D --> F[公钥可公开验签]

2.2 Go 中 jwt-go 库签名验证绕过漏洞复现(CVE-2020-26160)

该漏洞源于 jwt-go v3.x 在解析 JWT 时未严格校验 alg 字段,当攻击者传入 "alg": "none" 且签名为空字符串时,库误判为已验证签名,跳过 HMAC/RSASSA 校验。

漏洞触发条件

  • JWT Header 中 alg: "none"(不区分大小写)
  • Signature 部分为空或仅含 .(如 eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjMifQ.
  • 使用 ParseWithClaims(..., jwt.SigningMethodNone) 未显式禁用

复现代码片段

tokenString := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjMifQ."
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
    return []byte("secret"), nil // 此回调实际未被调用
})
// err == nil && token.Valid == true → 验证绕过!

逻辑分析jwt-goSigningMethodNone 场景下直接返回 nil 错误并设 Valid=true,完全忽略密钥回调函数;[]byte("secret") 参数虽传入但未参与任何计算。

攻击向量 是否触发绕过 原因
alg: "none" SigningMethodNone 被启用
alg: "NONE" 库内部字符串比较不区分大小写
alg: "nOnE" 同上
alg: "HS256" 进入标准 HMAC 校验流程

2.3 自定义签名验证器实现:强制算法白名单与密钥绑定校验

为杜绝弱算法滥用与密钥混淆风险,需在验签环节植入双重硬性约束。

核心校验逻辑

  • 首先比对 alg 头字段是否属于预设白名单(如 RS256, ES384
  • 其次验证 JWT 的 kid 是否与当前上下文绑定的密钥 ID 完全一致

算法白名单配置

SUPPORTED_ALGS = {"RS256", "ES384", "PS512"}  # 仅允许 FIPS 186-4 / RFC 8017 合规算法

此集合在应用启动时冻结,运行时不可修改;alg 字段若不在其中,直接拒绝解析,不进入密钥加载流程。

密钥绑定校验流程

graph TD
    A[解析JWT Header] --> B{alg ∈ SUPPORTED_ALGS?}
    B -- 否 --> C[401 Unauthorized]
    B -- 是 --> D[提取 kid]
    D --> E{kid == expected_kid?}
    E -- 否 --> C
    E -- 是 --> F[加载对应密钥并验签]

支持的算法与密钥类型映射

算法 密钥类型 最小密钥长度
RS256 RSA Public Key 2048 bit
ES384 EC Public Key secp384r1
PS512 RSA Public Key 3072 bit

2.4 利用 go-jose 构建抗篡改的双签 JWT 流程(HS256 + EdDSA)

双签名 JWT 通过组合对称与非对称签名,兼顾性能与强身份绑定:HS256 快速验证请求来源,EdDSA(Ed25519)锚定不可抵赖的私钥持有者。

签名策略设计

  • HS256 使用服务共享密钥保护传输完整性
  • EdDSA 使用用户专属私钥实现行为溯源
  • 两签名并存于同一 JWT 的 signatures 数组(JWS JSON Serialization)

关键代码示例

// 构建双签名 JWS 对象
jws := jose.SigningKey{
    Algorithm: jose.HS256,
    Key:       []byte("shared-secret"),
}
jwsEd := jose.SigningKey{
    Algorithm: jose.EdDSA,
    Key:       userPrivKey, // *ed25519.PrivateKey
}

// 使用 Compact Serialization 不支持多签,必须用 JSON Serialization
obj, _ := jose.NewJSONWebSignature(jose.Signature{KeyID: "hs256"}, jose.Signature{KeyID: "eddsa"})

此处 NewJSONWebSignature 接收多个 jose.Signature,底层将生成含 signatures: [...] 的 JSON 格式 JWT。KeyID 用于运行时区分验证链路;jose.EdDSA 要求密钥为 crypto.Signer 接口实现(如 *ed25519.PrivateKey)。

验证流程示意

graph TD
    A[解析 JWT JSON 序列] --> B{ signatures[0].header.alg == HS256? }
    B -->|是| C[用共享密钥验 HS256]
    B -->|否| D[用公钥验 EdDSA]
    C --> E[双签均通过 → 授权]
    D --> E
签名类型 性能 抗密钥泄露能力 适用场景
HS256 弱(共享密钥) 服务间短期通信
EdDSA 强(私钥独占) 用户操作审计溯源

2.5 红蓝对抗实战:从 Burp Suite 插件到 Go 编写的签名篡改检测探针

红蓝对抗中,API 签名机制常成绕过焦点。传统 Burp Suite 插件(如 Signer)依赖 Java 反射动态重签,但存在启动延迟与 JVM 依赖瓶颈。

核心演进动因

  • 签名逻辑嵌入客户端(如 HMAC-SHA256 + timestamp + nonce)
  • 攻击者通过篡改请求体后重放,导致服务端验签失败却仍被处理(若校验前置缺失)

Go 探针设计亮点

// sigprobe/main.go
func VerifySignature(r *http.Request, secret string) bool {
    sig := r.Header.Get("X-Signature")
    body, _ := io.ReadAll(r.Body)
    r.Body = io.NopCloser(bytes.NewBuffer(body)) // 恢复 Body 供后续 handler 使用
    expected := hmacSha256(fmt.Sprintf("%s%s", body, r.URL.Query().Encode()), secret)
    return hmac.Equal([]byte(sig), []byte(expected))
}

逻辑分析:探针在 http.Handler 中间件层拦截,先读取并缓存原始 body(避免 r.Body 被消耗),再拼接 query 参数参与签名计算;hmac.Equal 防时序攻击。secret 应从环境变量注入,禁止硬编码。

对比维度

维度 Burp 插件方案 Go 探针方案
启动耗时 ≥800ms(JVM 加载)
签名覆盖粒度 单请求手动触发 全量自动中间件校验
graph TD
    A[HTTP Request] --> B{Go 探针中间件}
    B --> C[读取Body+Query]
    C --> D[计算HMAC签名]
    D --> E[比对X-Signature头]
    E -->|不匹配| F[返回401]
    E -->|匹配| G[透传至业务Handler]

第三章:时钟漂移引发的失效链深度追踪

3.1 NTP 同步偏差、容器时区隔离与 JWT nbf/exp 时间语义冲突

时间语义错位的根源

当宿主机 NTP 同步延迟达 ±200ms,而容器内未挂载 /etc/localtimeTZ=UTC,应用读取 System.currentTimeMillis() 与 JWT 解析时的 nbf(not before)校验可能跨毫秒边界失效。

典型校验失败场景

// JWT 验证逻辑片段(使用 java-jwt)
try {
    Algorithm algorithm = Algorithm.HMAC256("secret");
    JWTVerifier verifier = JWT.require(algorithm)
        .acceptLeeway(60) // 关键:容忍60秒时钟漂移
        .build();
    DecodedJWT jwt = verifier.verify(token); // 若系统时间快于NTP源2s,nbf校验直接抛异常
} catch (TokenExpiredException e) { /* ... */ }

acceptLeeway(60) 显式补偿时钟偏差,避免因 NTP 暂态抖动或容器启动瞬时时间跳跃导致合法 token 被拒。

容器化部署建议

  • ✅ 挂载宿主机 /etc/timezone/etc/localtime 只读
  • ✅ 启动时执行 ntpd -q -p /var/run/ntpd.pid 或使用 chrony sidecar
  • ❌ 禁止在镜像中硬编码 date -s
组件 默认行为 风险
Docker 共享宿主机 clock 时区文件未挂载 → UTC 偏移误判
Kubernetes Pod 内无自动 NTP 同步 多节点间 drift > 500ms
Spring Security JWT 无 leeway 默认值 nbf 校验严格到毫秒级

3.2 Go time.Now().UTC() 在多时区服务中的陷阱与 jwt.WithValidator 定制化时间窗口

时区错觉:time.Now().UTC() 并非万能解药

在分布式 JWT 验证中,仅调用 time.Now().UTC() 无法规避系统时钟漂移与跨地域 NTP 同步误差。若服务节点未严格校时,exp 校验可能提前失败或延迟拒绝。

自定义时间验证器的必要性

使用 jwt.WithValidator 可注入带容错的时间比较逻辑:

validator := jwt.WithValidator(func(claims jwt.Claims) error {
    now := time.Now().UTC()
    // 允许 5 秒时钟偏差(双向)
    if claims.ExpiresAt().Before(now.Add(-5 * time.Second)) {
        return errors.New("token expired")
    }
    if claims.NotBefore().After(now.Add(5 * time.Second)) {
        return errors.New("token not active yet")
    }
    return nil
})

逻辑分析:该验证器以 UTC() 为基准,但引入 ±5s 容忍窗口;Add(-5*time.Second) 确保即使本地时钟快 4.9s,仍判定过期;Add(5*time.Second) 容忍慢钟场景。参数 5 * time.Second 应根据集群 NTP 精度(建议 ≤100ms)和 SLA 调整。

多时区服务典型误差源对比

误差来源 典型偏差 是否被 UTC() 消除
本地时区设置 ✅(已归一)
NTP 同步漂移 ±50–500ms
容器启动时钟偏移 ±1–3s
虚拟机时钟退化 累积秒级
graph TD
    A[JWT签发] --> B[客户端携带Token]
    B --> C{服务端验证}
    C --> D[time.Now.UTC()]
    D --> E[±5s 容错比较]
    E --> F[通过/拒绝]

3.3 分布式系统中基于逻辑时钟(Lamport Timestamp)的 JWT 有效期协同方案

在跨服务、多副本部署场景下,物理时钟漂移导致 exp 字段校验不一致。Lamport 逻辑时钟为 JWT 提供全局单调递增的时效锚点,替代依赖 NTP 同步的 Unix 时间戳。

逻辑时间注入 JWT Payload

// 生成含 Lamport 时间戳的 JWT
long lamportTime = clock.increment(); // 全局单例逻辑时钟
String jwt = Jwts.builder()
    .claim("l_exp", lamportTime + 300) // 逻辑过期:当前值+5分钟逻辑时长
    .claim("l_iat", lamportTime)
    .signWith(key).compact();

l_expl_iat 均为逻辑时间戳(非毫秒),服务校验时仅需比较本地逻辑时钟值,规避时钟不同步风险。

校验流程

  • 所有节点维护本地 lamportClock
  • 验证 JWT 时:if (localClock.get() >= jwt.getClaim("l_exp")) → 拒绝
  • 跨节点消息携带最新逻辑时间,驱动时钟同步。
字段 类型 说明
l_iat long 签发时本地逻辑时钟值
l_exp long 计算得出的逻辑过期阈值
graph TD
    A[Service A 签发 JWT] -->|l_iat=120, l_exp=150| B[Service B 接收]
    B --> C{localClock.get() ≥ 150?}
    C -->|否| D[接受请求]
    C -->|是| E[拒绝并刷新逻辑时钟]

第四章:密钥轮换断裂与演进式密钥管理

4.1 对称密钥轮换导致的“旧 Token 拒绝风暴”:Go 中 jwt.Keyfunc 动态密钥加载实践

当对称密钥轮换时,若所有服务实例未同步更新 []byte 密钥,已签发的旧 JWT 将因 Keyfunc 返回不匹配密钥而批量验证失败——即“拒绝风暴”。

核心问题根源

  • JWT 验证依赖 jwt.Keyfunc 每次调用返回当前有效密钥;
  • 静态密钥变量无法响应运行时密钥变更;
  • 无缓存/版本控制时,高频请求加剧密钥不一致放大效应。

动态密钥加载实现

var keyStore = &sync.Map{} // key: version (string), value: []byte

func dynamicKeyFunc(token *jwt.Token) (interface{}, error) {
    ver, ok := token.Header["kid"].(string)
    if !ok || ver == "" {
        return nil, fmt.Errorf("missing kid in token header")
    }
    if key, loaded := keyStore.Load(ver); loaded {
        return key.([]byte), nil
    }
    return nil, fmt.Errorf("unknown key version: %s", ver)
}

逻辑说明:token.Header["kid"] 提供密钥标识,sync.Map 支持并发安全的多版本密钥热加载;keyStore.Load() 避免全局锁,支持灰度切换。参数 ver 必须由签发方严格注入,且与密钥生命周期对齐。

密钥版本演进策略

版本 状态 生效时间 用途
v1 deprecated 2024-05-01 仅接受验证
v2 active 2024-05-02 签发+验证
graph TD
    A[JWT 请求到达] --> B{解析 Header.kid}
    B --> C[查 keyStore]
    C -->|命中| D[返回对应密钥]
    C -->|未命中| E[返回错误]

4.2 非对称密钥生命周期管理:X.509 证书自动续期 + JWKS 端点动态发现(go-jose + cert-manager 集成)

现代身份系统需同时保障 TLS 通信安全与 JWT 签名校验可信性,二者密钥生命周期必须协同演进。

自动续期与 JWKS 同步机制

cert-manager 通过 Certificate 资源签发 X.509 证书,并借助 Secret 触发 JWKS 生成器更新:

# cert-manager Certificate 资源片段
spec:
  secretName: jwt-signing-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer

该 Secret 被 go-jose 初始化的 jwk.Set 动态监听,调用 jwk.FromRaw() 从 PEM 解析公钥并注入内存缓存。

JWKS 端点发现流程

graph TD
  A[客户端请求 /keys] --> B{JWKS Handler}
  B --> C[读取最新 Secret]
  C --> D[jwk.NewSet().AddKey()]
  D --> E[JSON-serialize + cache-control]

关键参数对照表

参数 作用 示例值
cache-control 控制 JWKS 响应缓存行为 public, max-age=300
kid 绑定证书序列号与 JWK ID sha256/abc123...

此设计消除手动轮换风险,实现密钥、证书、JWKS 三者原子级一致性。

4.3 多租户场景下密钥分片策略:基于 kid 的密钥路由与 jwk.Set 并发安全缓存

在高并发多租户系统中,JWT 验证需快速、隔离、线程安全地加载租户专属密钥。核心在于解耦密钥发现与加载:通过 kid(Key ID)精准路由至对应租户的 JWK Set,并利用 github.com/lestrrat-go/jwx/v2/jwk.Set 的并发安全特性实现零锁缓存。

密钥路由逻辑

func resolveJWKSet(ctx context.Context, kid string) (jwk.Set, error) {
    tenantID := extractTenantIDFromKid(kid) // 如 "prod-abc123-RSA256-01"
    set, ok := jwkCache.Load(tenantID)       // sync.Map[string]jwk.Set
    if ok {
        return set.(jwk.Set), nil
    }
    // 懒加载:从租户专属 KMS 或配置中心拉取
    set, err := fetchTenantJWKSet(ctx, tenantID)
    if err != nil {
        return nil, err
    }
    jwkCache.Store(tenantID, set)
    return set, nil
}

kid 命名需嵌入租户标识(如 prod-abc123-RSA256-01),extractTenantIDFromKid 提取 abc123jwkCachesync.Map,避免全局锁,适配高频读+低频写场景。

缓存策略对比

策略 线程安全 租户隔离性 内存开销 适用场景
全局单 jwk.Set 单租户
每租户独立 sync.Map 中等租户量
jwk.Set + context.WithValue 请求级临时覆盖
graph TD
    A[JWT Header.kid] --> B{解析 tenantID}
    B --> C[查 jwkCache]
    C -->|命中| D[返回 jwk.Set]
    C -->|未命中| E[异步加载+Store]
    E --> D

4.4 密钥撤销支持:Go 实现 JWT jti 与 Redis Bloom Filter 联合黑名单快速判定

传统 JWT 黑名单依赖 Redis Set 存储全部已撤销 jti,内存开销随撤销量线性增长。本方案采用 Bloom Filter + 精确回查 的二级判定机制,在毫秒级响应下实现内存减省 92%(百万级 jti 仅需 ~1.2MB)。

核心设计原则

  • jti 作为唯一撤销标识,写入前经 SHA-256 哈希归一化为 32 字节定长 key
  • Redis 中部署 bf.blacklist Bloom Filter(误差率 0.001,容量 10M)
  • 布隆过滤器返回 true 时,触发二次精确校验(SISMEMBER jwt:blacklist:jti <hash>

Go 客户端关键逻辑

// 初始化布隆过滤器客户端(使用 github.com/redis/go-redis/v9 + github.com/tilinna/bloom)
func NewRevocationChecker(rdb *redis.Client) *RevocationChecker {
    return &RevocationChecker{
        rdb:   rdb,
        bloom: bloom.NewWithEstimates(10_000_000, 0.001), // 容量 & 误判率
    }
}

// 快速判定:先布隆过滤,再精确回查
func (rc *RevocationChecker) IsRevoked(ctx context.Context, jti string) (bool, error) {
    hash := fmt.Sprintf("%x", sha256.Sum256([]byte(jti))) // 归一化哈希
    if !rc.bloom.Test([]byte(hash)) {                      // 布隆过滤:false → 安全放行
        return false, nil
    }
    // 布隆返回 true → 可能被撤销,需精确验证
    return rc.rdb.SIsMember(ctx, "jwt:blacklist:jti", hash).Result()
}

逻辑分析IsRevoked 首先通过本地 Bloom Filter 快速排除 99.9% 未撤销令牌(无网络 IO),仅对布隆“疑似命中”者发起一次 Redis SISMEMBER 查询。hash 参数确保 jti 内容不可预测且长度可控;ctx 支持超时与取消,避免阻塞。

性能对比(100 万 jti 撤销数据)

方案 内存占用 平均查询延迟 误判率 精确性
Redis Set ~120 MB 0.8 ms 0%
Bloom + Set ~1.2 MB 0.35 ms 0.1% ✅(二级校验保障)
graph TD
    A[JWT 解析出 jti] --> B[SHA-256 哈希归一化]
    B --> C{Bloom Filter Test}
    C -->|false| D[立即放行:未撤销]
    C -->|true| E[Redis SISMEMBER 精确查询]
    E -->|true| F[拒绝访问]
    E -->|false| G[放行:布隆误判]

第五章:面向生产环境的 JWT 健壮性治理建议

密钥生命周期强制轮换机制

在金融级API网关(如基于Spring Cloud Gateway + Redis的部署)中,我们落地了JWT签名密钥的双版本滚动策略:主密钥(HS256-2024Q3-A)与备用密钥(HS256-2024Q3-B)并行加载。通过Kubernetes ConfigMap热更新+应用层密钥解析器动态路由,实现密钥切换零停机。实际案例显示,某支付系统在2024年7月12日完成密钥轮换后,旧token仍可验证至TTL自然过期(最长15分钟),新签发token全部使用B密钥,审计日志完整记录每次/jwks.json请求来源IP与User-Agent。

面向失败的Token解析容错设计

以下为Go语言服务端JWT校验核心逻辑片段,包含三重防护:

func ValidateToken(raw string) (*Claims, error) {
    // 1. 防止空白攻击
    if strings.TrimSpace(raw) == "" {
        return nil, errors.New("empty token")
    }
    // 2. 头部校验(拒绝非标准alg)
    header, _ := jwt.ParseHeader(raw)
    if header.Alg != "HS256" && header.Alg != "RS256" {
        metrics.Inc("jwt_invalid_alg")
        return nil, fmt.Errorf("unsupported alg: %s", header.Alg)
    }
    // 3. 载荷预解析(避免JSON炸弹)
    payload := extractPayload(raw)
    if len(payload) > 8192 {
        metrics.Inc("jwt_payload_too_large")
        return nil, errors.New("payload exceeds 8KB limit")
    }
    // ...后续标准解析
}

生产级Token黑名单策略对比

方案 实现复杂度 内存开销 时效性 适用场景
Redis Set(JWT ID) 毫秒级 短期会话注销(
Bloom Filter + Redis 极低 秒级延迟 高频登录风控(>10k QPS)
数据库时间戳比对 秒级 合规审计强依赖场景

某电商大促期间采用Bloom Filter方案,在单节点Redis集群上支撑每秒12万次token吊销检查,误判率控制在0.003%(通过布隆过滤器容量与哈希函数数量调优实现)。

审计驱动的Token元数据增强

在用户登录成功后,除标准JWT Claims外,强制注入以下生产必需字段:

  • x-src-ip: 请求客户端真实IP(经X-Forwarded-For链路清洗)
  • x-device-fp: 设备指纹哈希(SHA256(ua+screen+timezone))
  • x-session-id: 关联后端SessionID(用于全链路追踪)

该设计使安全团队可在ELK中执行如下查询定位异常行为:
jwt.x-src-ip : "203.0.113.*" AND jwt.x-device-fp : "a1b2c3*" | stats count() by jwt.sub

多租户场景下的Issuer隔离实践

在SaaS平台中,为避免租户间token伪造,实施三级Issuer命名规范:
https://api.{tenant-id}.example.com/v1/auth
结合Nginx配置强制校验:

location /api/ {
    auth_request /_jwt_validate;
    auth_request_set $issuer $upstream_http_x_jwt_issuer;
    if ($issuer !~ "^https://api/[a-z0-9\-]+\.example\.com/v1/auth$") {
        return 403;
    }
}

用代码写诗,用逻辑构建美,追求优雅与简洁的极致平衡。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注