Apache APISIX jwe-decrypt 插件实战:JWE 令牌解密与明文透传指南
【免费下载链接】apisixThe Cloud-Native API Gateway and AI Gateway项目地址: https://gitcode.com/gh_mirrors/api/apisix
jwe-decrypt是 Apache APISIX 内置的认证(auth)类型插件,它从请求头中读取符合 JWE Compact Serialization 格式的五段式令牌,依据令牌kid字段匹配 Consumer,使用 AES-256-GCM 解密密文,并在转发到上游(Upstream)前将明文写入指定请求头。本文以插件官方文档为主体,结合 jwe-decrypt.lua 源码与 jwe-decrypt.t 测试用例,系统讲解令牌格式、Consumer 与 Route 配置、完整操作步骤及底层实现原理,帮助你在 Route 或 Service 上快速落地“密文入网关、明文出上游”的令牌解密链路。
插件工作原理与适用场景
jwe-decrypt的核心处理流程如下:
- 从请求头(默认
Authorization)读取令牌; - 解析令牌的 JOSE Header,提取其中的
kid; - 用
kid在jwe-decrypt插件的 Consumer 配置中查找对应的凭据(Credential); - 以该 Consumer 配置的 32 字节密钥,对密文执行 AES-256-GCM 解密;
- 将解密得到的明文写入
forward_header指定的请求头,再继续代理请求。
该插件适用于在网关侧统一对上游隐藏 JWE 密文、向上游暴露明文的场景,例如客户端携带 JWE 加密的用户标识或业务载荷访问 API,由网关解密后透传给后端服务消费。你可以在 APISIX 的 Routes 或 Services 上启用该插件。
从源码结构看(apisix/plugins/jwe-decrypt.lua),该插件定义如下:
| 元信息字段 | 值 | 说明 |
|---|---|---|
version | 0.1 | 插件版本 |
priority | 2509 | 插件执行优先级 |
type | auth | 认证类型插件 |
name | jwe-decrypt | 插件名称 |
schema | 路由/服务侧 schema | 见下文 Route/Service 属性 |
consumer_schema | Consumer 侧 schema | 见下文 Consumer 属性 |
priority = 2509意味着它在同类认证插件中拥有较高的执行优先级,会在代理阶段较早介入请求处理;其核心解密逻辑实现在rewrite阶段(jwe-decrypt.lua)。
令牌格式:JWE Compact Serialization
jwe-decrypt接受的令牌使用 JWE Compact Serialization(RFC 7516)五段式结构,由点号(.)分隔:
base64url(header).<empty>.base64url(iv).base64url(ciphertext).base64url(tag)各部分含义:
| 段 | 内容 | 说明 | | -- | ---- | ---- | | 第 1 段 | JOSE Header | 以 base64url 编码的 JSON 对象,包含alg、enc、kid| | 第 2 段 | 加密密钥(Encrypted Key) | 使用dir直接加密算法时为空段 | | 第 3 段 | IV | 初始化向量,base64url 编码 | | 第 4 段 | 密文 | 加密后的载荷,base64url 编码 | | 第 5 段 | 认证标签 | AES-GCM 的认证标签,base64url 编码 |
插件只实现dir密钥管理算法与A256GCM内容加密算法,因此使用标准 JWE 库生成、由dir+A256GCM组合产生的令牌均可被接受。alg或enc被设置为其他值时会被直接拒绝(见下文“算法与格式校验”)。
头部结构
令牌的 JOSE Header 需包含:
{"alg":"dir","enc":"A256GCM","kid":"<consumer-key>"}alg:必须为dir,表示直接使用对称密钥加密;enc:必须为A256GCM,表示 AES-256-GCM 内容加密;kid:Consumer 凭据的key,插件据此定位解密密钥。
IV 必须是每次令牌生成时唯一且随机产生的,严禁在同一个密钥下复用 IV,否则会破坏 AES-GCM 的认证安全性。
配置属性说明
Consumer 侧属性
在 Consumer 上启用jwe-decrypt时,可配置以下属性:
| 名称 | 类型 | 必填 | 默认值 | 合法值 | 描述 |
|---|---|---|---|---|---|
| key | string | 是 | 标识 Consumer 凭据的唯一键,对应令牌头中的kid | ||
| secret | string | 是 | 32 字节 | 共享对称密钥。建议使用 secret 引用,如$env://...或$secret://... | |
| is_base64_encoded | boolean | 否 | false | 若 secret 为 base64url 编码则设为 true;解码后的字节数仍必须为 32 |
Route / Service 侧属性
在 Route 或 Service 上启用jwe-decrypt时,可配置以下属性:
| 名称 | 类型 | 必填 | 默认值 | 合法值 | 描述 |
|---|---|---|---|---|---|
| header | string | 是 | Authorization | 从中读取令牌的请求头 | |
| forward_header | string | 是 | Authorization | 将明文传给上游的请求头名称 | |
| strict | boolean | 否 | true | 为 true 时,请求缺少 JWE 令牌则返回 403 错误;为 false 时,未找到令牌则放行继续 |
从 schema 定义看(jwe-decrypt.lua),header与forward_header为必填项,strict默认true;Consumer 侧key与secret必填,且两者均被声明为encrypt_fields,即开启了字段加密后,密钥与 secret 会在写入 etcd 时被加密存储,避免明文落盘。
secret 长度校验的实现细节
Consumer 配置中的secret要求为 32 字节,因为 A256GCM 使用的正是 256 位(32 字节)密钥。源码中的校验逻辑(jwe-decrypt.lua)值得注意:
- 若
is_base64_encoded为 false,则要求secret字符串长度恰好为 32; - 若
is_base64_encoded为 true,则先做 base64url 解码,解码后的字节数也必须恰好为 32,且 secret 本身必须是合法的 base64url 字符串; - 当
data_encryption.enable_encrypt_fields开启且配置存储于 etcd 时,secret 在落库前已被加密,密文长度会超过 32,因此跳过长度校验。
该行为在 t/plugin/jwe-decrypt.t 中有完整覆盖:TEST 4 验证普通 secret 超长被拒(the secret length should be 32 chars),TEST 5 验证 base64 编码 secret 解码后超长被拒,TEST 6 验证非法 base64url 字符串被拒。
安全注意事项
向后兼容的 AAD 警告
根据 RFC 7516 的要求,标准 JWE 库会将编码后的受保护头部作为 AES-GCM 的附加认证数据(AAD)进行认证,这使得令牌的kid具备防篡改性——任何对头部的修改都会导致认证失败。
为向后兼容,插件同时接受一种旧式令牌:其密文在加密时未将受保护头部作为 AAD(APISIX 自身此前生成令牌的方式)。这类令牌的头部(包括kid)不受认证保护。因此:
- 请使用可信的令牌生成方;
- 优先使用遵循 RFC 7516 的 JWE 库,确保头部被 AAD 覆盖。
从源码看,解密时插件会先按 RFC 7516 方式将o.header(编码后的头部)作为 AAD 尝试解密,失败后再尝试无 AAD 的兼容路径(jwe-decrypt.lua)。测试用例也系统验证了这一双路径行为:
- TEST 31:由独立 JWE 库(python cryptography)生成、认证了受保护头部的令牌,解密成功(200);
- TEST 32:无 AAD 的旧式令牌仍然被接受(200);
- TEST 33:将 RFC 7516 令牌的
kid篡改为持有相同密钥的另一 Consumer 后,因标签不再覆盖头部而解密失败(400),直观印证了头部防篡改能力。
上游传输安全提示
解密后的明文会被写入请求头并转发给上游。需要注意:对于敏感明文,不能仅依赖 HTTPS Upstream——APISIX 对标准 HTTP Upstream 并不校验服务端证书。应通过经过认证、受保护的网络路径转发请求,例如经由会校验上游身份的反向代理或服务网格;同时限制上游的访问范围,并避免记录所配置的转发头。
操作示例一:创建带解密密钥的 Consumer
以下示例演示如何创建携带解密密钥的 Consumer,并为其生成 JWE 令牌。
首先获取 Admin API 密钥并存入环境变量:
admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')通过 Admin API 创建 Consumer
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \ -H "X-API-KEY: ${admin_key}" \ -d '{ "username": "jack", "plugins": { "jwe-decrypt": { "key": "jack-key", "secret": "key-length-should-be-32-chars123" } } }'其中key: "jack-key"将作为令牌头部kid的匹配值,secret为 32 字节的共享对称密钥(示例字符串key-length-should-be-32-chars123恰好 32 个字符)。
通过 ADC 创建 Consumer
将配置写入adc.yaml后同步到网关:
consumers: - username: jack plugins: jwe-decrypt: key: jack-key secret: key-length-should-be-32-chars123adc sync -f adc.yaml通过 Ingress 控制器(Gateway API)创建 Consumer
apiVersion: apisix.apache.org/v1alpha1 kind: Consumer metadata: namespace: aic name: jack spec: gatewayRef: name: apisix plugins: - name: jwe-decrypt config: key: jack-key secret: key-length-should-be-32-chars123kubectl apply -f jwe-consumer-ic.yaml注意:ApisixConsumer(APISIX Ingress Controller 的 CRD)仅通过authParameter字段支持认证类插件,且jwe-decrypt不在其支持类型之列,因此该场景无法通过 APISIX Ingress Controller 完成,只能使用上面的 Gateway API 方式。
生成 JWE 令牌
为 Consumer 生成 JWE 令牌时,可使用任何支持dir直接加密 +A256GCM的标准 JWE 库,以 Consumer 的secret为密钥。令牌结构为:
base64url(header).<empty>.base64url(iv).base64url(ciphertext).base64url(tag)其中 header 为{"alg":"dir","enc":"A256GCM","kid":"<consumer-key>"}。如下令牌将载荷{"uid":10000,"uname":"test"}以密钥jack-key及上述 secret 加密:
eyJraWQiOiJqYWNrLWtleSIsImFsZyI6ImRpciIsImVuYyI6IkEyNTZHQ00ifQ..vi29KBCQKcVmPwTT.VToyPMFbq-ZY05MIpntP1N3AmYeq3zELQ0B6iQ.vuTPG2ODc-DjUTjNCzfA2A注意:若密钥非明文存储而是通过 secret 引用(如$env://...、$secret://...)注入,需在生成令牌时使用引用解析后的真实密钥值。
操作示例二:创建 Route 解密 JWE 数据
以下示例演示如何解密上文生成的 JWE 令牌。创建一个启用jwe-decrypt的 Route,解密Authorization请求头:
通过 Admin API 创建 Route
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ -H "X-API-KEY: ${admin_key}" \ -d '{ "id": "jwe-decrypt-route", "uri": "/anything/jwe", "plugins": { "jwe-decrypt": { "header": "Authorization", "forward_header": "Authorization" } }, "upstream": { "type": "roundrobin", "scheme": "https", "nodes": { "httpbin.org:443": 1 } } }'通过 ADC 创建 Route
services: - name: jwe-decrypt-service routes: - name: jwe-decrypt-route uris: - /anything/jwe plugins: jwe-decrypt: header: Authorization forward_header: Authorization upstream: type: roundrobin scheme: https nodes: - host: httpbin.org port: 443 weight: 1adc sync -f adc.yaml通过 Ingress 控制器(Gateway API)创建 Route
下面的 Gateway API 配置仅使用公共 HTTPBin 与本页展示的非敏感演示载荷。在转发真实解密数据前,应替换为受控的上游,并使用经过认证、受保护的网络路径。APISIX 的 HTTPS Upstream 本身不会校验上游服务端证书,应使用会校验上游身份的代理或服务网格:
apiVersion: v1 kind: Service metadata: namespace: aic name: httpbin-external-domain spec: type: ExternalName externalName: httpbin.org --- apiVersion: apisix.apache.org/v1alpha1 kind: PluginConfig metadata: namespace: aic name: jwe-decrypt-plugin-config spec: plugins: - name: jwe-decrypt config: header: Authorization forward_header: Authorization --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: namespace: aic name: jwe-decrypt-route spec: parentRefs: - name: apisix rules: - matches: - path: type: Exact value: /anything/jwe filters: - type: ExtensionRef extensionRef: group: apisix.apache.org kind: PluginConfig name: jwe-decrypt-plugin-config backendRefs: - name: httpbin-external-domain port: 80kubectl apply -f jwe-decrypt-ic.yaml发起带密文的请求
向 Route 发送请求,在Authorization头中携带 JWE 密文:
curl "http://127.0.0.1:9080/anything/jwe" -H 'Authorization: eyJraWQiOiJqYWNrLWtleSIsImFsZyI6ImRpciIsImVuYyI6IkEyNTZHQ00ifQ..vi29KBCQKcVmPwTT.VToyPMFbq-ZY05MIpntP1N3AmYeq3zELQ0B6iQ.vuTPG2ODc-DjUTjNCzfA2A'响应中Authorization头即为解密后的明文载荷:
{ "args": {}, "data": "", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Authorization": "{\"uid\":10000,\"uname\":\"test\"}", "Host": "127.0.0.1", "User-Agent": "curl/8.1.2", "X-Amzn-Trace-Id": "Root=1-6510f2c3-1586ec011a22b5094dbe1896", "X-Forwarded-Host": "127.0.0.1" }, "json": null, "method": "GET", "origin": "127.0.0.1, 119.143.79.94", "url": "http://127.0.0.1/anything/jwe" }可见明文{"uid":10000,"uname":"test"}被写入了上游请求的Authorization头。测试用例 TEST 22 也验证了同样的效果:上游收到的Authorization头为解密后的明文。
底层处理流程与错误码
令牌提取
插件从conf.header指定的请求头读取令牌(jwe-decrypt.lua)。若令牌以Bearer或bearer前缀开头,会自动剥离前缀后再解析。测试用例 TEST 12/13/14 分别验证了带Bearer、不带前缀、带小写bearer三种写法均可正常解密。
算法与格式校验
rewrite阶段的校验顺序(jwe-decrypt.lua)为:
- 令牌缺失:未取到令牌时,
strict=true返回 403missing JWE token in request;strict=false则直接放行(TEST 10、TEST 26); - 令牌结构非法:五段式解析失败或 header 解码失败,返回 400
JWE token invalid(TEST 11、TEST 15);header 解码后非 JSON 对象(如 JSONnull或标量)同样被拒(TEST 27、TEST 28); - 缺少 kid:返回 400
missing kid in JWE token; - 不支持的算法:
alg非dir或enc非A256GCM时返回 400unsupported alg or enc in JWE token(TEST 34、TEST 35、TEST 37);header 中省略alg/enc的旧令牌仍可兼容接受(TEST 36); - kid 无对应 Consumer:返回 400
invalid kid in JWE token; - 解密失败:返回 400
failed to decrypt JWE token(TEST 24、TEST 30、TEST 33)。
密钥解析与解密
get_secret依据is_base64_encoded决定是否对 secret 做 base64url 解码(jwe-decrypt.lua);解密时先对 IV、密文、标签分别做 base64url 解码,再以resty.aes的 AES-256-GCM 模式执行解密,并优先以编码后的头部作为 AAD,失败后回退到无 AAD 的兼容路径(jwe-decrypt.lua)。
Consumer 查找
get_consumer通过consumer_mod.plugin(plugin_name)与consumer_mod.consumers_kv(plugin_name, consumer_conf, "key")构建kid -> Consumer索引并按key精确匹配(jwe-decrypt.lua)。测试用例 TEST 16 验证了删除 Consumer 后,剩余 Consumer 的令牌仍可正常校验,说明索引随配置变更实时生效。
密钥保护:encrypt_fields 与 secret 引用
Consumer schema 中将key与secret声明为encrypt_fields(jwe-decrypt.lua)。当 conf/config.yaml.example 中的deployment.admin.data_encryption.enable_encrypt_fields设为true且配置存储于 etcd 时,这两个字段在写入配置中心前会被自动加密:
data_encryption: # Data encryption settings. enable_encrypt_fields: true # Whether enable encrypt fields specified in `encrypt_fields` in plugin schema. keyring: # This field is used to encrypt the private key of SSL and the `encrypt_fields` # in plugin schema. - qeddd145sfvddff3 # Set the encryption key. A key of length 16 selects AES-128-CBC and a key - edd1c9f0985e76a2 # of length 32 selects AES-256-CBC; any other length is rejected on startup.测试用例 TEST 7/8 验证了开启加密后,从 etcd 读取到的 Consumer 配置中key与secret均为密文,且原始明文不会出现在日志中。除字段加密外,官方文档也建议通过 secret 引用($env://...、$secret://...)管理密钥,避免密钥明文出现在 Admin API 请求与配置文件中。
总结
jwe-decrypt为 APISIX 提供了一条标准化的 JWE 令牌解密链路:五段式 Compact 令牌 +kid驱动的 Consumer 凭据匹配 + AES-256-GCM 解密 + 明文请求头透传。部署时需把握四个要点:
- 密钥管理:Consumer 的
secret必须是 32 字节,可配合 base64url 编码、encrypt_fields字段加密与 secret 引用三重手段保护; - 令牌生产:优先使用遵循 RFC 7516 的标准 JWE 库生成令牌,使头部进入 AAD 认证范围,避免
kid被篡改的风险; - 路由控制:
strict=true时缺失令牌返回 403,strict=false时放行,可按业务对强制性的要求选择; - 传输安全:明文经请求头转发,敏感场景下应借助代理或服务网格等经过认证的受保护网络路径传输,并避免记录转发头。
如需深入理解实现细节,可继续阅读 apisix/plugins/jwe-decrypt.lua 源码与 t/plugin/jwe-decrypt.t 测试用例(后者覆盖了包括 AAD 兼容、算法拒绝、错误码在内的 30 余个场景),以及配置中心的 data_encryption 配置说明。
【免费下载链接】apisixThe Cloud-Native API Gateway and AI Gateway项目地址: https://gitcode.com/gh_mirrors/api/apisix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考