hmac-auth
hmac-auth 插件支持 HMAC (Hash-based Message Authentication Code) 认证,作为确保请求完整性的机制,防止其在传输过程中被修改。要使用该插件,你需要在 消费者 上配置 HMAC 密钥,并在路由或服务上启用该插件。
当消费者成功通过身份验证时,APISIX 会在将请求代理到上游服务之前,向请求添加额外的头部,例如 X-Consumer-Username、X-Credential-Identifier 以及配置的其他消费者自定义头部。上游服务将能够区分消费者并根据需要实施额外的逻辑。如果任何这些值不可用,则不会添加相应的头部。
实现
启用后,该插件会验证请求 Authorization 头部中的 HMAC 签名,并检查传入请求是否来自受信任的来源。具体而言,当 APISIX 收到 HMAC 签名的请求时,会从 Authorization 头部提取 Key ID。然后,APISIX 检索相应的消费者配置,包括密钥 (secret key)。如果 Key ID 有效且存在,APISIX 使用请求的 Date 头部和密钥生成 HMAC 签名。如果生成的签名与 Authorization 头部中提供的签名匹配,则请求通过身份验证并转发到上游服务。
该插件的实现基于 draft-cavage-http-signatures。
示例
以下示例演示了如何在不同场景下使用 hmac-auth 插件。
在路由上实现 HMAC 认证
以下示例演示了如何在路由上实现 HMAC 认证。你还将把消费者自定义 ID 附加到 Consumer-Custom-Id 头部中的已认证请求,这可用于根据需要实现额外的逻辑。
创建一个带有自定义 ID 标签的消费者 john:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "john",
"labels": {
"custom_id": "495aec6a"
}
}'
为消费者创建 hmac-auth 凭据:
curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-hmac-auth",
"plugins": {
"hmac-auth": {
"key_id": "john-key",
"secret_key": "john-secret-key"
}
}
}'
使用默认配置创建一个启用 hmac-auth 插件的路由:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "hmac-auth-route",
"uri": "/get",
"methods": ["GET"],
"plugins": {
"hmac-auth": {}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
生成签名。你可以使用下面的 Python 代码片段或你选择的其他技术栈:
```python title="hmac-sig-header-gen.py"
import hmac
import hashlib
import base64
from datetime import datetime, timezone
key_id = "john-key" # 密钥 ID
secret_key = b"john-secret-key" # 密钥
request_method = "GET" # HTTP 方法
request_path = "/get" # 路由 URI
algorithm= "hmac-sha256" # 可以使用 allowed_algorithms 中允许的其他算法
# 获取当前的GMT时间
# 注意:签名在时钟偏移(默认 300 秒)后会失效
# 你可以在签名失效后重新生成,或者增加时钟偏移以在建议的安全边界内延长有效期
gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')
# 构造签名字符串(有序)
# 日期以及任何后续的自定义头都应转换为小写,并由单个空格字符分隔,即`<key>:<space><value>`
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6
signing_string = (
f"{key_id}\n"
f"{request_method} {request_path}\n"
f"date: {gmt_time}\n"
)
# 创建签名
signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()
signature_base64 = base64.b64encode(signature).decode('utf-8')
# 构造请求头
headers = {
"Date": gmt_time,
"Authorization": (
f'Signature keyId="{key_id}",algorithm="{algorithm}",'
f'headers="@request-target date",'
f'signature="{signature_base64}"'
)
}
# 打印请求头
print(headers)
运行脚本:
python3 hmac-sig-header-gen.py
你应该看到打印出的请求头:
{'Date': 'Fri, 06 Sep 2024 06:41:29 GMT', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="'}
使用生成的请求头,向路由发送请求:
curl -X GET "http://127.0.0.1:9080/get" \
-H "Date: Fri, 06 Sep 2024 06:41:29 GMT" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="'
你应该看到类似于以下的 HTTP/1.1 200 OK 响应:
{
"args": {},
"headers": {
"Accept": "*/*",
"Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"",
"Date": "Fri, 06 Sep 2024 06:41:29 GMT",
"Host": "127.0.0.1",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-66d96513-2e52d4f35c9b6a2772d667ea",
"X-Consumer-Username": "john",
"X-Credential-Identifier": "cred-john-hmac-auth",
"X-Consumer-Custom-Id": "495aec6a",
"X-Forwarded-Host": "127.0.0.1"
},
"origin": "192.168.65.1, 34.0.34.160",
"url": "http://127.0.0.1/get"
}
如果你想将更多消费者自定义头部附加到已认证请求中,请参阅 attach-consumer-label 插件。
从上游隐藏授权信息
如 [上一个示例](#在路由上实现 HMAC 认证) 所示,传递给上游的 Authorization 头部包含了签名和所有其他详细信息。这可能会引入潜在的安全风险。
以下示例演示了如何防止将这些信息发送到上游服务。
更新插件配置,将 hide_credentials 设置为 true:
curl "http://127.0.0.1:9180/apisix/admin/routes/hmac-auth-route" -X PATCH \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"plugins": {
"hmac-auth": {
"hide_credentials": true
}
}
}'
向路由发送请求:
curl -X GET "http://127.0.0.1:9080/get" \
-H "Date: Fri, 06 Sep 2024 06:41:29 GMT" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="'
你应该看到 HTTP/1.1 200 OK 响应,并注意到 Authorization 头部已被完全移除:
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "127.0.0.1",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-66d96513-2e52d4f35c9b6a2772d667ea",
"X-Consumer-Username": "john",
"X-Credential-Identifier": "cred-john-hmac-auth",
"X-Forwarded-Host": "127.0.0.1"
},
"origin": "192.168.65.1, 34.0.34.160",
"url": "http://127.0.0.1/get"
}
启用 Body 校验
以下示例演示了如何启用 Body 校验以确保请求体的完整性。
创建消费者 john:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "john"
}'
为消费者创建 hmac-auth 凭据:
curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-hmac-auth",
"plugins": {
"hmac-auth": {
"key_id": "john-key",
"secret_key": "john-secret-key"
}
}
}'
创建一个启用 hmac-auth 插件的路由:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "hmac-auth-route",
"uri": "/post",
"methods": ["POST"],
"plugins": {
"hmac-auth": {
"validate_request_body": true
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
生成签名。你可以使用下面的 Python 代码片段或你选择的其他技术栈:
import hmac
import hashlib
import base64
from datetime import datetime, timezone
key_id = "john-key" # 密钥 ID
secret_key = b"john-secret-key" # 密钥
request_method = "POST" # HTTP 方法
request_path = "/post" # 路由 URI
algorithm= "hmac-sha256" # 可以使用 allowed_algorithms 中允许的其他算法
body = '{"name": "world"}' # 示例请求体
# 获取当前的 GMT 时间
# 注意:签名在时钟偏移(默认 300 秒)后会失效。
# 您可以在签名失效后重新生成,或者增加时钟偏移以在建议的安全边界内延长有效期
gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')
# 构造签名字符串(有序)
# 日期以及任何后续的自定义头都应转换为小写,并由单个空格字符分隔,即`<key>:<space><value>`
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6
signing_string = (
f"{key_id}\n"
f"{request_method} {request_path}\n"
f"date: {gmt_time}\n"
)
# 创建签名
signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()
signature_base64 = base64.b64encode(signature).decode('utf-8')
# 创建请求体的 SHA-256 摘要并进行 base64 编码
body_digest = hashlib.sha256(body.encode('utf-8')).digest()
body_digest_base64 = base64.b64encode(body_digest).decode('utf-8')
# 构造请求头
headers = {
"Date": gmt_time,
"Digest": f"SHA-256={body_digest_base64}",
"Authorization": (
f'Signature keyId="{key_id}",algorithm="hmac-sha256",'
f'headers="@request-target date",'
f'signature="{signature_base64}"'
)
}
# 打印请求头
print(headers)
运行脚本:
python3 hmac-sig-digest-header-gen.py
你应该看到打印出的请求头:
{'Date': 'Fri, 06 Sep 2024 09:16:16 GMT', 'Digest': 'SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="'}
使用生成的请求头,向路由发送请求:
curl "http://127.0.0.1:9080/post" -X POST \
-H "Date: Fri, 06 Sep 2024 09:16:16 GMT" \
-H "Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="' \
-d '{"name": "world"}'
你应该看到类似于以下的 HTTP/1.1 200 OK 响应:
{
"args": {},
"data": "",
"files": {},
"form": {
"{\"name\": \"world\"}": ""
},
"headers": {
"Accept": "*/*",
"Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"",
"Content-Length": "17",
"Content-Type": "application/x-www-form-urlencoded",
"Date": "Fri, 06 Sep 2024 09:16:16 GMT",
"Digest": "SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=",
"Host": "127.0.0.1",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-66d978c3-49f929ad5237da5340bbbeb4",
"X-Consumer-Username": "john",
"X-Credential-Identifier": "cred-john-hmac-auth",
"X-Forwarded-Host": "127.0.0.1"
},
"json": null,
"origin": "192.168.65.1, 34.0.34.160",
"url": "http://127.0.0.1/post"
}
如果你发送不带摘要或带无效摘要的请求:
curl "http://127.0.0.1:9080/post" -X POST \
-H "Date: Fri, 06 Sep 2024 09:16:16 GMT" \
-H "Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="' \
-d '{"name": "world"}'
你应该看到带有以下消息的 HTTP/1.1 401 Unauthorized 响应:
{"message":"client request can't be validated"}
强制签名请求头
以下示例演示了如何强制某些请求头必须包含在请求的 HMAC 签名中。
创建消费者 john:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "john"
}'
为消费者创建 hmac-auth 凭据:
curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-hmac-auth",
"plugins": {
"hmac-auth": {
"key_id": "john-key",
"secret_key": "john-secret-key"
}
}
}'
创建一个启用 hmac-auth 插件的路由,该路由要求 HMAC 签名中必须包含三个请求头:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "hmac-auth-route",
"uri": "/get",
"methods": ["GET"],
"plugins": {
"hmac-auth": {
"signed_headers": ["date","x-custom-header-a", "x-custom-header-b"]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
生成签名。你可以使用下面的 Python 代码片段或你选择的其他技术栈:
import hmac
import hashlib
import base64
from datetime import datetime, timezone
key_id = "john-key" # 密钥 ID
secret_key = b"john-secret-key" # 密钥
request_method = "GET" # HTTP 方法
request_path = "/get" # 路由 URI
algorithm= "hmac-sha256" # 可以使用 allowed_algorithms 中允许的其他算法
custom_header_a = "hello123" # 必需的自定义头部
custom_header_b = "world456" # 必需的自定义头部
# 获取当前的 GMT 时间
# 注意:签名在时钟偏移(默认 300 秒)后会失效
# 您可以在签名失效后重新生成,或者增加时钟偏移以在建议的安全边界内延长有效期
gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')
# 构造签名字符串(有序)
# 日期以及任何后续的自定义头都应转换为小写,并由单个空格字符分隔,即`<key>:<space><value>`
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6
signing_string = (
f"{key_id}\n"
f"{request_method} {request_path}\n"
f"date: {gmt_time}\n"
f"x-custom-header-a: {custom_header_a}\n"
f"x-custom-header-b: {custom_header_b}\n"
)
# 创建签名
signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()
signature_base64 = base64.b64encode(signature).decode('utf-8')
# 构造请求头
headers = {
"Date": gmt_time,
"Authorization": (
f'Signature keyId="{key_id}",algorithm="hmac-sha256",'
f'headers="@request-target date x-custom-header-a x-custom-header-b",'
f'signature="{signature_base64}"'
),
"x-custom-header-a": custom_header_a,
"x-custom-header-b": custom_header_b
}
# 打印请求头
print(headers)
运行脚本:
python3 hmac-sig-req-header-gen.py
你应该看到打印出的请求头:
{'Date': 'Fri, 06 Sep 2024 09:58:49 GMT', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE="', 'x-custom-header-a': 'hello123', 'x-custom-header-b': 'world456'}
使用生成的请求头,向路由发送请求:
curl -X GET "http://127.0.0.1:9080/get" \
-H "Date: Fri, 06 Sep 2024 09:58:49 GMT" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE="' \
-H "x-custom-header-a: hello123" \
-H "x-custom-header-b: world456"
你应该看到类似于以下的 HTTP/1.1 200 OK 响应:
{
"args": {},
"headers": {
"Accept": "*/*",
"Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\"",
"Date": "Fri, 06 Sep 2024 09:58:49 GMT",
"Host": "127.0.0.1",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-66d98196-64a58db25ece71c077999ecd",
"X-Consumer-Username": "john",
"X-Credential-Identifier": "cred-john-hmac-auth",
"X-Custom-Header-A": "hello123",
"X-Custom-Header-B": "world456",
"X-Forwarded-Host": "127.0.0.1"
},
"origin": "192.168.65.1, 103.97.2.206",
"url": "http://127.0.0.1/get"
}
匿名消费者的速率限制
以下示例演示了如何针对常规消费者和匿名消费者配置不同的速率限制策略,其中匿名消费者无需认证且配额较少。
创建常规消费者 john 并配置 limit-count 插件,允许在 30 秒窗口内有 3 次配额:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "john",
"plugins": {
"limit-count": {
"count": 3,
"time_window": 30,
"rejected_code": 429
}
}
}'
为消费者 john 创建 hmac-auth 凭据:
curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-hmac-auth",
"plugins": {
"hmac-auth": {
"key_id": "john-key",
"secret_key": "john-secret-key"
}
}
}'
创建匿名用户 anonymous 并配置 limit-count 插件,允许在 30 秒窗口内有 1 次配额:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "anonymous",
"plugins": {
"limit-count": {
"count": 1,
"time_window": 30,
"rejected_code": 429
}
}
}'
创建路由并配置 hmac-auth 插件以接受匿名消费者 anonymous 绕过认证:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "hmac-auth-route",
"uri": "/get",
"methods": ["GET"],
"plugins": {
"hmac-auth": {
"anonymous_consumer": "anonymous"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
生成签名。你可以使用下面的 Python 代码片段或你选择的其他技术栈:
import hmac
import hashlib
import base64
from datetime import datetime, timezone
key_id = "john-key" # 密钥 ID
secret_key = b"john-secret-key" # 密钥
request_method = "GET" # HTTP 方法
request_path = "/get" # 路由 URI
algorithm= "hmac-sha256" # 可以使用 allowed_algorithms 中允许的其他算法
# 获取当前的 GMT 时间
# 注意: 签名在时钟偏移(默认 300 秒)后会失效
# 您可以在签名失效后重新生成,或者增加时钟偏移以在建议的安全边界内延长有效期
gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')
# 构造签名字符串(有序)
# 日期以及任何后续的自定义头都应转换为小写,并由单个空格字符分隔,即`<key>:<space><value>`
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6
signing_string = (
f"{key_id}\n"
f"{request_method} {request_path}\n"
f"date: {gmt_time}\n"
)
# 创建签名
signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()
signature_base64 = base64.b64encode(signature).decode('utf-8')
# 构造请求头
headers = {
"Date": gmt_time,
"Authorization": (
f'Signature keyId="{key_id}",algorithm="{algorithm}",'
f'headers="@request-target date",'
f'signature="{signature_base64}"'
)
}
# 打印请求头
print(headers)
运行脚本:
python3 hmac-sig-header-gen.py
你应该看到打印出的请求头:
{'Date': 'Mon, 21 Oct 2024 17:31:18 GMT', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="ztFfl9w7LmCrIuPjRC/DWSF4gN6Bt8dBBz4y+u1pzt8="'}
要进行验证,请使用生成的请求头发送五个连续请求:
resp=$(seq 5 | xargs -I{} curl "http://127.0.0.1:9080/anything" -H "Date: Mon, 21 Oct 2024 17:31:18 GMT" -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="ztFfl9w7LmCrIuPjRC/DWSF4gN6Bt8dBBz4y+u1pzt8="' -o /dev/null -s -w "%{http_code}\n") && \
count_200=$(echo "$resp" | grep "200" | wc -l) && \
count_429=$(echo "$resp" | grep "429" | wc -l) && \
echo "200": $count_200, "429": $count_429
你应该看到以下响应,显示在 5 个请求中,3 个请求成功(状态码 200),而其他请求被拒绝(状态码 429) 。
200: 3, 429: 2
发送五个匿名请求:
resp=$(seq 5 | xargs -I{} curl "http://127.0.0.1:9080/anything" -o /dev/null -s -w "%{http_code}\n") && \
count_200=$(echo "$resp" | grep "200" | wc -l) && \
count_429=$(echo "$resp" | grep "429" | wc -l) && \
echo "200": $count_200, "429": $count_429
你应该看到以下响应,显示只有一个请求成功:
200: 1, 429: 4