Envoy 限流描述符扩展详解:从 JWT 中提取 Claim 构建 Rate Limit Descriptor
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
导读
本篇文章围绕 Envoy 新增的限流描述符扩展envoy.rate_limit_descriptors.jwt_claim展开,讲解如何在路由级限流(Rate Limit)配置中,直接从 HTTP 请求头或 Cookie 中携带的 JWT 提取指定 claim(如sub、iss)作为限流描述符(descriptor)的值,从而按用户/租户维度做精细化限流。读完本文你将掌握该扩展的完整配置字段、取值优先级逻辑、与jwt_authn过滤器配合的安全边界,以及对应的底层源码实现与测试验证。
一、扩展背景:为什么需要从 JWT 提取限流描述符
Envoy 的全局速率限制(global rate limiting)通过路由配置中的RateLimit规则生成描述符(descriptor),再交给外部的限流服务(如 gRPC 限流服务)进行计数与判定。传统的限流动作(action)包括source_cluster、destination_cluster、request_headers、remote_address、generic_key等,定义在 route_components.proto 的RateLimit.Action中。
对于"按用户限流"这类场景,最自然的做法是把用户身份信息(如 JWT 中的sub或issclaim)拼进描述符。但在该扩展出现之前,要从 JWT 中取 claim 值,通常需要借助jwt_authn过滤器把 claim 写入 header 或动态元数据,再通过request_headers或metadata描述符动作间接实现。
envoy.rate_limit_descriptors.jwt_claim扩展提供了一条更直接的路径:无需任何额外过滤器,直接在限流描述符动作中指定要读取的 header/cookie、要提取的 claim 名,Envoy 解析 JWT 后把 claim 值作为描述符值。根据官方变更记录(changelog 条目),这一能力尤其适合 JWT 校验由其他组件完成的场景——例如应用自身校验,或上游 mTLS 认证服务校验——而 Envoy 只需要基于 claim 值做限流。
二、扩展与配置模型
2.1 扩展标识
该扩展的完整名称(factory name)为:
envoy.rate_limit_descriptors.jwt_claim其配置类型为envoy.extensions.rate_limit_descriptors.jwt_claim.v3.Descriptor,定义于 jwt_claim.proto,属于envoy.rate_limit_descriptors扩展类别。限流描述符扩展通过RateLimit.Action中的extension字段(core.v3.TypedExtensionConfig)接入,见 route_components.proto 的说明。
2.2 注册与实现结构
从源码结构看,该扩展由三部分组成:
- 工厂与实现类:config.h、config.cc;
- Bazel 构建目标:BUILD(
envoy_cc_extension,依赖//source/common/jwt:jwt_lib、//envoy/ratelimit:ratelimit_interface等); - 单元测试:config_test.cc。
工厂类JwtClaimDescriptorFactory继承RateLimit::DescriptorProducerFactory,通过REGISTER_FACTORY宏注册,name()返回envoy.rate_limit_descriptors.jwt_claim(见 config.cc)。
三、配置字段详解
Descriptor消息共定义 7 个字段(proto 中预留至第 8 号字段),逐一说明如下(字段约束均来自 jwt_claim.proto):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
descriptor_key | string | 是 | 描述符条目中的 key,最小长度 1 |
header_name | string | 二选一 | 携带 JWT 的 HTTP 头名,如authorization;必须符合 HTTP header 名规范 |
cookie | string | 二选一 | 携带 JWT 的 Cookie 名,如auth_token;从请求的Cookie头中查找 |
value_prefix | string | 否 | 解析前需要剥离的值前缀,如"Bearer "(含末尾空格),用于Authorization: Bearer <token>场景 |
claim_name | string | 是 | 要提取的 JWT claim 名,支持点号分隔的嵌套 claim,如claim.nested.key;最小长度 1 |
default_value | string | 否 | 兜底值:当 header/cookie 缺失、JWT 结构无效或 claim 缺失/非字符串时使用 |
skip_if_absent | bool | 否 | 为 true 时,在上述缺失场景且未配置default_value的情况下,跳过该描述符条目而非中止整个描述符;默认 false |
关键约束:header_name与cookie必须且只能设置一个。该约束由 proto 校验规则配合工厂层双重保证——在 config.cc 中,createDescriptorProducerFromProto会显式检查config.header_name().empty() == config.cookie().empty(),若两者同时为空或同时非空,直接返回错误"jwt_claim descriptor: exactly one of header_name or cookie must be set"。
3.1 最小可运行配置示例
actions: - extension: name: jwt_claim_descriptor typed_config: "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.jwt_claim.v3.Descriptor descriptor_key: my_descriptor_name header_name: authorization value_prefix: "Bearer " claim_name: sub该配置生成形如("my_descriptor_name", "<sub claim 值>")的描述符条目(描述符条目的形式定义见 jwt_claim.proto 中的 code-block)。这正是单元测试ExtractsSimpleClaim验证的用例(见 config_test.cc)。
3.2 从 Cookie 提取
actions: - extension: name: jwt_claim_descriptor typed_config: "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.jwt_claim.v3.Descriptor descriptor_key: my_descriptor_name cookie: auth_token claim_name: sub对应测试ExtractsClaimFromCookie:请求Cookie: other=1; auth_token=<jwt>; more=2时,扩展通过Http::Utility::parseCookieValue精确解析出auth_token的值(见 config_test.cc 与 config.cc)。
四、取值优先级与行为规则
从实现JwtClaimDescriptor::populateDescriptor(config.cc)可以梳理出完整的行为链:
尝试提取 claim 值 ├── 成功 → 生成 (descriptor_key, claim_value),返回 true └── 失败(header/cookie 缺失、前缀不匹配、JWT 解析失败、claim 缺失或非字符串) ├── default_value 非空 → 生成 (descriptor_key, default_value),返回 true └── default_value 为空 ├── skip_if_absent = true → 跳过该条目,返回 true(不中止整个描述符) └── skip_if_absent = false → 返回 false(中止整个描述符,不向限流服务发送)4.1 提取步骤的底层实现
extractClaimValue(config.cc)按顺序执行:
- 取原始值:配置了
cookie则调用Http::Utility::parseCookieValue解析;否则读取 header(取第一个匹配值)。空值视为缺失。 - 剥离前缀:若配置了
value_prefix,用absl::StartsWith检查原始值是否以该前缀开头;不匹配则视为缺失(不会尝试解析)。匹配则移除前缀。这解释了为何 cookie 场景通常不需要配置前缀(测试ExtractsClaimFromCookieWithValuePrefix也验证了 cookie + 前缀的组合行为)。 - 解析 JWT:调用
JwtVerify::Jwt::parseFromString(来自//source/common/jwt公共库),解析失败(非结构合法 JWT)视为缺失。 - 提取嵌套 claim:通过
JwtVerify::StructUtils::GetValue按点号分隔路径查找 claim,例如nested.key对应 JSON 中的{"nested":{"key":...}}。 - 字符串化:
claimValueAsString仅接受字符串类型的 claim 值;数字、布尔、数组、对象等一律按"缺失"处理(config.cc)。空字符串 claim 会被视为"存在",生成值为空的描述符条目(测试ExtractsEmptyStringClaim验证了这一点,即使skip_if_absent: true也不影响空字符串 claim 生成条目)。
4.2 行为规则速查表
| 场景 | 结果 |
|---|---|
| header/cookie 缺失,无 default_value | 无 default_value 时中止整个描述符(或 skip 单条目) |
| 值前缀不匹配 | 视为缺失,同上 |
JWT 结构非法(如not-a-jwt) | 视为缺失,同上 |
| claim 缺失或非字符串 | 视为缺失,同上 |
| claim 值为空字符串 | 生成(key, "")条目(总是"存在") |
| claim 值正常(字符串) | 生成(key, value)条目 |
五、安全边界:务必先完成 JWT 签名校验
这是该扩展最重要的一条使用红线(jwt_claim.proto 中有完整的安全警告):
该扩展不校验 JWT 签名。任何一方都可以伪造或使用过期 JWT,并随意构造 claim 值,这些值会被直接用作限流描述符值。攻击者可以轻松绕过或操纵基于 claim 的限流——例如冒充其他 subject,或每次请求轮换伪造的 claim 值以完全绕过限流。
测试ForgedJwtStillExtractsUnverifiedClaim专门演示了这一行为:一个任意构造、签名部分无意义的 token,其 claim 值照样被提取(config_test.cc)。测试辅助函数makeJwt也直接以"sig"作为签名段,进一步佐证该扩展不关心签名有效性。
因此官方文档明确要求:
- 不要用该扩展做访问控制、按信任级别差异化限流,或任何与授权判断相关的决策,除非能证明请求路径上的其他环节已经完成了签名校验;
- 推荐做法:如果需要 Envoy 自己先验证 JWT 再限流,应把
envoy.filters.http.jwt_authn过滤器放在限流过滤器之前,并通过request_headers或metadata描述符动作(而非本扩展)来构建描述符。jwt_authn过滤器在拒绝非法 token 后,本扩展(或描述符动作)才会执行。
典型的合法部署形态是:JWT 校验由应用或上游 mTLS 认证服务完成,Envoy 侧的 jwt_claim 扩展只负责"读取已可信的 claim 值用于限流"。
六、配置文件校验与常见错误
工厂在配置加载时会进行 proto 校验(MessageUtil::downcastAndValidate),两类常见错误会直接导致配置加载失败(对应测试见 config_test.cc):
- 同时设置
header_name和cookie→ 报错jwt_claim descriptor: exactly one of header_name or cookie must be set; - 两者都未设置→ 同样的报错。
此外descriptor_key、claim_name为必填(min_len: 1),header_name/cookie/value_prefix需符合 HTTP header 名/值规范。
七、测试覆盖一览
该扩展的单元测试覆盖了绝大多数行为分支,可作为理解语义的权威参考(config_test.cc):
ExtractsSimpleClaim:普通字符串 claim 提取;ExtractsEmptyStringClaim:空字符串 claim 生成空值条目;ExtractsNestedClaim:嵌套 claim(nested.key);MissingHeaderNoDefaultAbortsDescriptor:缺失且无兜底时中止描述符;MissingHeaderWithSkipIfAbsentSkipsDescriptor:skip_if_absent: true时跳过单条目,不影响同 action 列表中其他条目;DefaultValueUsedWhenClaimAbsent/DefaultValueUsedWhenClaimIsAbsentFromValidJwt/MalformedJwtUsesDefaultValue:default_value兜底分支;NonStringCustomClaimTreatedAsAbsent:数字 claim 按缺失处理;DefaultValueUsedWhenNestedClaimHasScalarIntermediateValue:嵌套路径中间节点为标量(非对象)时按缺失处理;ValuePrefixMismatchTreatedAsAbsent/NoValuePrefixConfigured:前缀匹配语义;ExtractsClaimFromCookie/ExtractsClaimFromCookieWithValuePrefix:Cookie 来源提取;RejectsConfigWithBothHeaderAndCookie/RejectsConfigWithNeitherHeaderNorCookie:配置校验;ForgedJwtStillExtractsUnverifiedClaim:不校验签名的事实佐证。
八、总结
envoy.rate_limit_descriptors.jwt_claim为 Envoy 的全局限流体系补齐了"直接从 JWT 取 claim 作为描述符值"的能力,配置简洁、无额外过滤器依赖,特别适合 JWT 校验已由外部组件完成的架构。使用时务必牢记其安全边界:它只做结构解析,不做签名验证——将签名校验前置到jwt_authn过滤器或可信上游服务,才能安全地基于 claim 值实施按用户/租户维度的限流策略。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考