跳到主要内容

实施 HMAC 认证

HMAC 认证是一种通过使用共享密钥和哈希算法生成加密签名来保护 API 请求的强大方法。客户端通过哈希请求负载或特定请求组件(如标头或参数)来计算签名,并将其与请求一起发送。然后,服务器使用相同的共享密钥重新计算签名并验证其有效性。此方法可确保请求的真实性和完整性,防止篡改或重放攻击。HMAC 认证特别适合保护敏感数据交换,例如在金融服务或需要强不可否认性的应用程序中。然而,安全地共享和管理密钥对其有效性至关重要。

在本指南中,你将实现一个场景,其中有两个消费者使用 HMAC 认证 向 APISIX 进行身份验证,每个消费者具有不同的限流限速配额。一旦实施,消费者应该能够访问上游服务并将消费者 ID 转发到上游服务,从而为额外的业务逻辑提供选项。

创建消费者

消费者是指使用 API 的应用程序或开发者。在使用 APISIX 内置身份验证方法时,你应该始终创建消费者。

创建一个具有可选自定义 ID 和 30 秒窗口内一个请求的限流限速配额的消费者 johndoe

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "johndoe",
"labels": {
"custom_id": "john-doe-junior"
},
"plugins": {
"limit-count": {
"count": 1,
"time_window": 30,
"rejected_code": 429
}
}
}'

如果你希望实施额外的业务逻辑,自定义 ID 将转发到上游服务。

创建另一个具有可选自定义 ID 和 30 秒窗口内两个请求的限流限速配额的消费者 janedoe

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "janedoe",
"labels": {
"custom_id": "jane-doe-senior"
},
"plugins": {
"limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429
}
}
}'

创建消费者凭证

凭证用于配置与消费者关联的身份验证凭证。

johndoe 创建 hmac-auth 凭证:

curl "http://127.0.0.1:9180/apisix/admin/consumers/johndoe/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"
}
}
}'

janedoe 创建 hmac-auth 凭证:

curl "http://127.0.0.1:9180/apisix/admin/consumers/janedoe/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jane-hmac-auth",
"plugins": {
"hmac-auth": {
"key_id": "jane-key",
"secret_key": "jane-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": "/anything",
"methods": ["GET"],
"plugins": {
"hmac-auth": {}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

创建签名

生成一个签名。你可以使用以下 Python 代码段或你选择的其他技术栈:

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" # 密钥(secret key)
request_method = "GET" # HTTP 请求方法
request_path = "/get" # 路由 URI
algorithm= "hmac-sha256" # 可使用 allowed_algorithms 中允许的其他算法

# 获取当前 GMT 时间
# 注意:在时钟偏移(默认 300 秒)之后,签名将失效
# 签名失效后可以重新生成签名,或者在建议的安全边界内适当增大
# 时钟偏移(clock skew)以延长签名的有效期
gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')

# 构造签名字符串(按顺序)
# date 以及后续自定义请求头必须使用小写,并使用
# 单个空格分隔,即格式为 `<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, 13 Dec 2024 10:52:03 GMT', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="xt4MHsraC7C6E6jGUD9wlwEbdLWlQS561yoixekboCs="'}

janedoe 重复相同的步骤。你应该看到打印的请求标头:

{'Date': 'Fri, 13 Dec 2024 10:52:04 GMT', 'Authorization': 'Signature keyId="jane-key",algorithm="hmac-sha256",headers="@request-target date",signature="5KKPz7tk1+YVpoVqtNBxS6xHzRE9Bu3CUqhmhXPxGUE="'}

验证

使用生成的标头,使用 johndoe 的凭证向路由发送请求:

curl -X GET "http://127.0.0.1:9080/anything" \
-H "Fri, 13 Dec 2024 10:52:03 GMT" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="xt4MHsraC7C6E6jGUD9wlwEbdLWlQS561yoixekboCs="'

你应该看到类似以下的 HTTP/1.1 200 OK 响应:

{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
...
"X-Consumer-Username": "johndoe",
"X-Credential-Identifier": "cred-john-basic-auth",
"X-Consumer-Custom-Id": "john-doe-junior",
"X-Forwarded-Host": "127.0.0.1"
},
...
}

使用 john 的密钥向路由生成三个请求:

resp=$(seq 3 | xargs -I{} curl -X GET "http://127.0.0.1:9080/anything" \
-H "Date: Fri, 13 Dec 2024 10:52:03 GMT" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="xt4MHsraC7C6E6jGUD9wlwEbdLWlQS561yoixekboCs="' \
-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

你应该看到以下响应,显示在 3 个请求中,1 个请求成功,而其他请求被拒绝:

200:    1, 429:    2

使用 jane 的凭证向路由生成三个请求:

resp=$(seq 3 | xargs -I{} curl -X GET "http://127.0.0.1:9080/anything" \
-H "Date: Fri, 13 Dec 2024 10:52:04 GMT" \
-H 'Authorization: Signature keyId="jane-key",algorithm="hmac-sha256",headers="@request-target date",signature="5KKPz7tk1+YVpoVqtNBxS6xHzRE9Bu3CUqhmhXPxGUE="' \
-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

你应该看到以下响应,显示在 3 个请求中,2 个请求成功,而另一个被拒绝:

200:    2, 429:    1

最后,发送一个带有无效密钥的请求:

curl -X GET "http://127.0.0.1:9080/anything" \
-H "Fri, 13 Dec 2024 10:52:03 GMT" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="randomrandomalwEbdLWlQS561yoixekboCs="'

你应该看到 HTTP/1.1 401 Unauthorized 响应,并带有类似于以下消息:

{"message":"client request can't be validated"}

下一步

你现在已经学会了如何实施 HMAC 认证。APISIX 支持其他内置身份验证方法,例如 密钥认证基本认证JWT 认证