Apache APISIX hmac-auth 插件实战:HMAC 签名认证的配置、签名计算与源码解析
【免费下载链接】apisixThe Cloud-Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/ap/apisix
hmac-auth是 Apache APISIX 内置的认证类插件,通过 HMAC 签名机制为 Route 或 Service 提供请求身份校验,适用于服务间调用鉴权、开放 API 接入方认证等场景。本文基于 hmac-auth 官方文档 与该插件在仓库中的真实实现 apisix/plugins/hmac-auth.lua,系统讲解插件属性、启用方式、签名生成公式、请求体校验、自定义认证请求头,并结合源码与测试用例(t/plugin/hmac-auth.t)说明其底层校验流程。读完本文,你将能够在 APISIX 中独立配置 hmac-auth 消费者、计算出正确的 HMAC 签名,并掌握多语言签名生成与排错方法。
插件概述:基于 Consumer 的 HMAC 认证
hmac-auth插件为 Route 或 Service 添加 HMAC 认证能力。它依赖 Consumer 对象工作:API 的调用方(Consumer)必须把其密钥放入请求头中供网关校验。相比简单的 key-auth,HMAC 认证将请求方法、URI、查询参数、时间戳与指定请求头一起纳入签名计算,能够有效防止请求被篡改与重放。
在源码 apisix/plugins/hmac-auth.lua 中可以看到该插件的基本元信息:
local _M = { version = 0.1, priority = 2530, type = 'auth', name = plugin_name, schema = schema, consumer_schema = consumer_schema }priority = 2530意味着它在认证阶段具有较高的执行优先级;type = 'auth'表明它属于认证类插件,通过rewrite阶段(apisix/plugins/hmac-auth.lua)完成校验并在失败时直接返回401与{"message":"client request can't be validated"}。
插件属性(Attributes)详解
以下属性均配置在Consumer 对象的hmac-auth插件配置中:
| 名称 | 类型 | 必填 | 默认值 | 合法值 | 描述 |
|---|---|---|---|---|---|
| access_key | string | 是 | - | - | Consumer 的唯一标识。若不同 Consumer 配置了相同 key,会出现请求匹配异常 |
| secret_key | string | 是 | - | - | 与access_key配对使用的密钥。该字段支持通过 APISIX Secret 资源保存在 Secret Manager 中 |
| algorithm | string | 否 | "hmac-sha256" | ["hmac-sha1", "hmac-sha256", "hmac-sha512"] | 签名使用的加密算法 |
| clock_skew | integer | 否 | 0 | - | 签名允许的时钟偏移(秒)。设为0时跳过日期校验 |
| signed_headers | array[string] | 否 | - | - | 参与签名计算的请求头列表。指定后客户端请求只能携带这些指定请求头;未指定时全部请求头参与计算 |
| keep_headers | boolean | 否 | false | [true, false] | 为true时,认证成功后保留请求头X-HMAC-SIGNATURE、X-HMAC-ALGORITHM与X-HMAC-SIGNED-HEADERS,否则移除 |
| encode_uri_params | boolean | 否 | true | [true, false] | 为true时对 URI 参数做 URL 编码,例如params1=hello%2Cworld会被编码,而params2=hello,world不会被编码 |
| validate_request_body | boolean | 否 | false | [true, false] | 为true时校验请求体 |
| max_req_body | integer | 否 | 512 * 1024 | - | 允许的最大请求体大小(字节) |
对应地,apisix/plugins/hmac-auth.lua 中的consumer_schema给出了更精确的约束细节,可作为配置校验的参考:
access_key、secret_key:字符串,长度限制 1~256;signed_headers中的每个请求头名称:字符串,长度限制 1~50;max_req_body默认值为MAX_REQ_BODY,即1024 * 512(512KB),定义于 apisix/plugins/hmac-auth.lua;required = {"access_key", "secret_key"},两个字段缺一不可。
加密存储:
consumer_schema中定义了encrypt_fields = {"secret_key"},意味着该字段在 etcd 中以加密形式存储。关于 APISIX 加密存储字段的机制,可参见 plugin-develop.md 中的 encrypted storage fields 章节:通过 Admin API 增删改资源时自动加密、读取资源及插件运行时自动解密。此外,secret_key也支持通过 APISIX Secret 资源引用外部密钥管理系统的值。
启用插件
第一步:为 Consumer 启用
首先在 Consumer 对象上启用插件。文档示例使用 Admin API 创建名为jack的 Consumer:
curl http://127.0.0.1:9180/apisix/admin/consumers -H "X-API-KEY: $admin_key" -X PUT -d ' { "username": "jack", "plugins": { "hmac-auth": { "access_key": "user-key", "secret_key": "my-secret-key", "clock_skew": 0, "signed_headers": ["User-Agent", "Accept-Language", "x-custom-a"] } } }'其中admin_key从conf/config.yaml中提取(该文件位于仓库 conf/config.yaml,默认配置见 conf/config.yaml.example):
admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')如果本机没有yq,也可以直接打开conf/config.yaml查看deployment.admin.admin_key下的 key 值手动填入环境变量。
测试用例 t/plugin/hmac-auth.t 验证了缺失secret_key或缺失access_key时 Admin API 都会返回 400,例如:
{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \"secret_key\" is required"}同时该测试文件还验证了access_key与secret_key的 256 长度上限(t/plugin/hmac-auth.t)。
也可以通过 APISIX Dashboard 的 Web 界面完成上述操作。仓库 docs/assets/images/plugin 目录保存了 Dashboard 操作界面的截图,第一步填写 Consumer 基本信息:
第二步为 Consumer 启用 hmac-auth 插件并配置密钥:
第二步:为 Route 或 Service 绑定插件
接下来将插件配置到 Route(或 Service)上,使该路由的所有请求都要求 HMAC 认证:
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "uri": "/index.html", "plugins": { "hmac-auth": {} }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }'注意 Route 上hmac-auth的配置体为空{}:插件的实际认证参数(access_key、secret_key 等)均定义在 Consumer 上,Route 只负责声明“此路由需要 HMAC 认证”。
签名生成原理
签名公式
签名计算公式为:
signature = HMAC-SHAx-HEX(secret_key, signing_string)生成签名需要两个参数:secret_key(由 Consumer 配置)与signing_string(待签字符串)。其中:
signing_string = HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n + signed_headers_string各组成部分的含义:
- HTTP Method:大写的 HTTP 请求方法,如 GET、PUT、POST;
- HTTP URI:请求 URI,必须以
/开头,/表示空路径; - Date:HTTP 头中的日期,GMT 格式;
- canonical_query_string:URL 中查询字符串(问号
?之后的key1=value1&key2=value2)规范化编码后的结果; - signed_headers_string:参与签名计算的指定请求头拼接结果。
缺失项处理:上述任何一项缺失时,都以空字符串参与计算(见 apisix/plugins/hmac-auth.lua 中
core.table.concat(signing_string_items, "\n") .. "\n"的实现,末尾还会追加一个换行符)。
canonical_query_string 生成算法
- 从 URL 中提取查询项;
- 以
&为分隔符将查询项拆分为键值对; - 若
encode_uri_params为true:- 只有 key 时,转换公式为
uri_encode(key) + "="; - 同时存在 key 和 value 时,转换公式为
uri_encode(key) + "=" + uri_encode(value)(value 可以为空字符串); - 按 key 的字典序排序,用
&连接生成canonical_query_string;
- 只有 key 时,转换公式为
- 若
encode_uri_params为false:- 只有 key 时,转换公式为
key + "="; - 同时存在 key 和 value 时,转换公式为
key + "=" + value(value 可以为空字符串); - 按 key 的字典序排序,用
&连接生成canonical_query_string。
- 只有 key 时,转换公式为
在 apisix/plugins/hmac-auth.lua 的generate_signature函数中可以看到该算法的实际实现:query 参数被收集后按键排序,再根据encode_uri_params决定是否调用ngx.escape_uri对 key/value 编码。对于 URL 中只出现 key 没有=value的情况,源码将参数值true替换为空字符串以保持兼容(apisix/plugins/hmac-auth.lua)。
signed_headers_string 生成算法
- 从请求头中取出需要参与计算的指定请求头;
- 按
name:value格式拼接,得到signed_headers_string:
HeaderKey1 + ":" + HeaderValue1 + "\n" HeaderKey2 + ":" + HeaderValue2 + "\n" ... HeaderKeyN + ":" + HeaderValueN + "\n"注意:name与value之间使用冒号直接连接,没有空格。源码中的实现为h .. ":" .. canonical_header(apisix/plugins/hmac-auth.lua),其中canonical_header通过core.request.header(ctx, h)读取,缺失时取空字符串。
签名生成全流程逐步推导
以下请求为例:
curl -i http://127.0.0.1:9080/index.html?name=james&age=36 \ -H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \ -H "x-custom-a: test" \ -H "User-Agent: curl/7.29.0"第 1 步:HTTP Method
请求默认方法为 GET,signing_string为:
"GET"第 2 步:拼接 HTTP URI
请求 URI 为/index.html,得到:
"GET /index.html"第 3 步:拼接 canonical_query_string
URL 中查询项为name=james&age=36,假设encode_uri_params为false。按 canonical_query_string 算法对 key 做字典排序得到age=36&name=james:
"GET /index.html age=36&name=james"第 4 步:拼接 access_key
access_key为user-key:
"GET /index.html age=36&name=james user-key"第 5 步:拼接 Date
Date 为 GMT 格式,如Tue, 19 Jan 2021 11:33:20 GMT:
"GET /index.html age=36&name=james user-key Tue, 19 Jan 2021 11:33:20 GMT"第 6 步:拼接 signed_headers_string
signed_headers指定参与签名的请求头,示例中为User-Agent: curl/7.29.0与x-custom-a: test:
"GET /index.html age=36&name=james user-key Tue, 19 Jan 2021 11:33:20 GMT User-Agent:curl/7.29.0 x-custom-a:test "最终将上述字符串作为 message、my-secret-key作为 secret,使用 hmac-sha256 计算并做 base64 编码即得到签名。官方文档给出的 Python 生成代码如下:
import base64 import hashlib import hmac secret = bytes('my-secret-key', 'utf-8') message = bytes("""GET /index.html age=36&name=james user-key Tue, 19 Jan 2021 11:33:20 GMT User-Agent:curl/7.29.0 x-custom-a:test """, 'utf-8') hash = hmac.new(secret, message, hashlib.sha256) # to lowercase base64 print(base64.b64encode(hash.digest()))计算得到的签名值:
| 类型 | 值 |
|---|---|
| SIGNATURE | 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg= |
注意:示例中的待签字符串末尾包含一个换行符,这与源码
concat(..., "\n") .. "\n"的实现一致(apisix/plugins/hmac-auth.lua)。signing_string由六个部分依次以\n连接,最后再补一个\n结尾。
如果需要其他编程语言(Java、Go、Ruby、Node.js、JavaScript ES6、PHP、Lua、Shell)的签名生成示例,可参考仓库中的 Generating HMAC signatures,该文档为每种语言提供了 hex 与 base64 两种输出方式的完整代码。
源码中的签名校验流程
从源码结构看,apisix/plugins/hmac-auth.lua 中的validate函数按以下顺序完成校验:
- 检查
access_key、signature、algorithm是否存在; - 通过
get_consumer(access_key)查找 Consumer(apisix/plugins/hmac-auth.lua),若 key 无效返回"Invalid access key"; - 校验请求携带的
algorithm与 Consumer 配置的algorithm是否一致,否则返回"algorithm <x> not supported"; - 若
clock_skew > 0,用ngx.parse_http_time解析 Date 头并计算与网关当前时间的差值,超过clock_skew则返回"Clock skew exceeded"; - 若配置了
signed_headers,校验请求声明的签名请求头是否都在允许列表中,否则返回"Invalid signed header <x>"; - 用 Consumer 的
secret_key与请求参数重新计算签名,与请求携带的 base64 解码后的签名比较,不一致返回"Invalid signature"。
测试用例 t/plugin/hmac-auth.t 逐一验证了上述失败分支:缺失签名、缺失算法、无效 access key、不支持算法、时钟偏移超限、非法 GMT 时间等场景均返回 401 与{"message":"client request can't be validated"},具体错误原因会写入 error log。
校验请求体(validate_request_body)
将validate_request_body设置为true后,插件会计算请求体的 HMAC-SHA 值并与X-HMAC-DIGEST请求头比对:
X-HMAC-DIGEST: base64(hmac-sha(<body>))如果请求没有请求体,可将X-HMAC-DIGEST设置为空字符串的 HMAC-SHA 值。
性能提醒:计算请求体摘要时,插件会把请求体加载到内存中。请求体过大时可能造成较高的内存消耗。可以通过配置max_req_body(默认 512KB)限制允许的最大请求体大小,超过设定大小的请求体将被拒绝。
源码中对应的实现位于 apisix/plugins/hmac-auth.lua:当validate_request_body为真时,通过core.request.get_body(max_req_body, ctx)读取请求体(超过限制会返回"Exceed body limit size"),将请求体(空时为"")用hmac_funcs[params.algorithm]计算摘要并 base64 编码,与X-HMAC-DIGEST头比对,不一致则返回"Invalid digest"。
携带签名发起请求
方式一:签名放在 X-HMAC-* 独立请求头中
curl -i "http://127.0.0.1:9080/index.html?name=james&age=36" \ -H "X-HMAC-SIGNATURE: 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg=" \ -H "X-HMAC-ALGORITHM: hmac-sha256" \ -H "X-HMAC-ACCESS-KEY: user-key" \ -H "Date: Tue, 19 Jan 2021 11:33:20 GMT" \ -H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \ -H "x-custom-a: test" \ -H "User-Agent: curl/7.29.0"认证通过后返回正常响应:
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Date: Tue, 19 Jan 2021 11:33:20 GMT Server: APISIX/2.2 ......方式二:签名放入 Authorization 头
签名也可以放在Authorization请求头中,格式为hmac-auth-v1#加#分隔的多个字段:
curl http://127.0.0.1:9080/index.html \ -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + DATE + # + SIGNED_HEADERS' -iHTTP/1.1 200 OK Content-Type: text/html Content-Length: 13175 ... Accept-Ranges: bytes从源码 apisix/plugins/hmac-auth.lua 可以看到:当请求头中没有X-HMAC-ACCESS-KEY时,插件会读取Authorization头,按#拆分,当拆分结果恰好为 6 段且首段为hmac-auth-v1时,依次取出 access_key、signature、algorithm、date、signed_headers。测试用例 t/plugin/hmac-auth.t 验证了通过Authorization头传递认证信息并通过校验的场景,其实际构造的认证字符串为:
local auth_string = "hmac-auth-v1#" .. access_key .. "#" .. ngx_encode_base64(signature) .. "#" .. "hmac-sha256#" .. gmt .. "#x-custom-header-a;x-custom-header-b"方式三:签名放在单独请求头中
curl http://127.0.0.1:9080/index.html \ -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' \ -H 'X-HMAC-ALGORITHM: ALGORITHM' \ -H 'Date: DATE' \ -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' \ -H 'X-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' -iHTTP/1.1 200 OK Content-Type: text/html Content-Length: 13175 ... Accept-Ranges: bytes注意:
- 多个签名请求头之间必须以
;分隔,例如x-custom-header-a;x-custom-header-b;SIGNATURE需要先做 base64 编码再放入请求头(源码在 apisix/plugins/hmac-auth.lua 用ngx.decode_base64(params.signature)解码后与计算值比对,因此客户端侧应使用base64_encode)。
使用自定义请求头名
默认使用的认证相关请求头名称为:
| 用途 | 默认请求头 |
|---|---|
| 签名 | X-HMAC-SIGNATURE |
| 算法 | X-HMAC-ALGORITHM |
| 日期 | Date |
| access key | X-HMAC-ACCESS-KEY |
| 签名请求头列表 | X-HMAC-SIGNED-HEADERS |
| 请求体摘要 | X-HMAC-DIGEST |
你可以在配置文件conf/config.yaml的plugin_attr中为这些头自定义名称(plugin_attr结构可参考 conf/config.yaml.example 附近的注释):
plugin_attr: hmac-auth: signature_key: X-APISIX-HMAC-SIGNATURE algorithm_key: X-APISIX-HMAC-ALGORITHM date_key: X-APISIX-DATE access_key: X-APISIX-HMAC-ACCESS-KEY signed_headers_key: X-APISIX-HMAC-SIGNED-HEADERS body_digest_key: X-APISIX-HMAC-BODY-DIGEST配置后即可使用新请求头名发起请求:
curl http://127.0.0.1:9080/index.html \ -H 'X-APISIX-HMAC-SIGNATURE: base64_encode(SIGNATURE)' \ -H 'X-APISIX-HMAC-ALGORITHM: ALGORITHM' \ -H 'X-APISIX-DATE: DATE' \ -H 'X-APISIX-HMAC-ACCESS-KEY: ACCESS_KEY' \ -H 'X-APISIX-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' \ -H 'X-APISIX-HMAC-BODY-DIGEST: BODY_DIGEST' -iHTTP/1.1 200 OK Content-Type: text/html Content-Length: 13175 ... Accept-Ranges: bytes源码 apisix/plugins/hmac-auth.lua 中的get_params函数会优先读取plugin.plugin_attr(plugin_name)配置的自定义头名,未配置时才回退到默认值。
删除插件
删除插件时,将 Route 配置中的plugins置空即可。APISIX 会自动热加载配置,无需重启:
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "uri": "/index.html", "plugins": {}, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }'同样地,若不再需要某个 Consumer 的 hmac-auth 配置,也可以从对应 Consumer 的plugins中移除hmac-auth配置块。
常见排错与注意事项
结合源码 apisix/plugins/hmac-auth.lua 与测试用例 t/plugin/hmac-auth.t,以下排查点值得注意:
- 401 且 error log 提示
access key or signature missing:请求未携带X-HMAC-ACCESS-KEY、X-HMAC-SIGNATURE或有效的Authorization头(t/plugin/hmac-auth.t); algorithm missing或algorithm <x> not supported:请求头中的X-HMAC-ALGORITHM缺失,或与 Consumer 配置的 algorithm 不一致(t/plugin/hmac-auth.t);Clock skew exceeded:Date头与网关时间差超过clock_skew,或clock_skew为 0 时携带了无法解析的日期;注意clock_skew设为 0 表示跳过日期校验,若设为正值则必须提供合法 GMT 格式的Date头(t/plugin/hmac-auth.t);Invalid signature:签名字符串拼接顺序、大小写、URI 编码方式或请求头值不一致。请重点核对:待签字符串是否以换行符结尾、name:value中冒号后是否有空格、canonical_query_string是否按字典序排序;Invalid signed header <x>:X-HMAC-SIGNED-HEADERS中声明了 Consumer 配置signed_headers之外的请求头(t/plugin/hmac-auth.t);Invalid digest/Exceed body limit size:启用validate_request_body后,X-HMAC-DIGEST缺失或与请求体摘要不一致,或请求体超过max_req_body限制;keep_headers行为:默认(false)情况下,认证成功后插件会移除X-HMAC-SIGNATURE、X-HMAC-ALGORITHM、X-HMAC-SIGNED-HEADERS三个请求头,避免上游服务收到无关认证头(apisix/plugins/hmac-auth.lua);需要上游读取这些头时再开启keep_headers。
小结
hmac-auth是 APISIX 认证插件体系中实现请求签名防篡改的成熟方案:通过 Consumer 上的属性配置 完成密钥与算法管理,在 Route/Service 上声明启用,由网关在 rewrite 阶段完成签名、时间窗与请求体的三重校验。理解其signing_string拼接规则(方法、URI、规范化查询串、access_key、GMT 时间、签名请求头,以\n连接并结尾补\n)是正确生成签名的关键;官方文档 hmac-auth.md 中的逐步推导示例与 多语言签名生成示例,配合 插件源码 与 测试用例,可以为接入方提供从配置到联调的完整参考。
【免费下载链接】apisixThe Cloud-Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/ap/apisix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考