实施 JWT 认证
JWT 认证是一种使用已签名 JSON Web Token 对 API 客户端进行身份认证的安全、无状态方法。Token 可以包含用户身份和角色等声明,使 API 无需查询身份认证数据库即可验证请求。这种方法适合需要轻量级身份认证的分布式应用程序。由于 Token 在过期前始终有效,请为敏感 API 设置较短的过期时间或实现 Token 撤销。
在本指南中,你将实现一个场景,其中有两个消费者使用 JWT 认证 向 APISIX 进行身份认证,每个消费者具有不同的限流限速配额。一旦实施,消费者应该能够访问上游服务并将消费者 ID 转发到上游服务,从而为额外的业务逻辑提供选项。
前置条件
- 安装 Python 3,以便在本地签发示例 JWT。
- 如果使用 Admin API 示例且启用了身份验证,请将管理员角色的密钥导出为
ADMIN_API_KEY。请在deployment.admin.admin_key下配置该密钥。Docker 快速入门为了便于本地评估而禁用了身份验证;生产环境中请保持启用。
创建消费者
消费者是指使用 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 创建 jwt-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-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "john-key",
"secret": "john-hs256-secret-that-is-very-long"
}
}
}'
为 janedoe 创建 jwt-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-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "jane-key",
"secret": "jane-hs256-secret-that-is-very-long"
}
}
}'
创建路由
创建一个路由并启用 jwt-auth:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "jwt-auth-route",
"uri": "/anything",
"plugins": {
"jwt-auth": {"claims_to_verify": ["exp"]}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
签发 JWT
在本地签发两个 HS256 Token,避免将签名密钥提交给第三方网站。下面生成的每个 Token 都包含消费者密钥、签发时间和五分钟后的过期时间。将脚本保存为 issue-jwts.py:
import base64
import hashlib
import hmac
import json
import shlex
import time
def encode(value):
data = json.dumps(value, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
now = int(time.time())
for variable, key, secret in [
("john_jwt_token", "john-key", "john-hs256-secret-that-is-very-long"),
("jane_jwt_token", "jane-key", "jane-hs256-secret-that-is-very-long"),
]:
header = encode({"alg": "HS256", "typ": "JWT"})
payload = encode({"key": key, "iat": now, "exp": now + 300})
message = f"{header}.{payload}"
signature = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
encoded_signature = base64.urlsafe_b64encode(signature).rstrip(b"=").decode()
print(f"export {variable}={shlex.quote(message + '.' + encoded_signature)}")
将新生成的 Token 加载到当前 Shell:
eval "$(python3 issue-jwts.py)"
该路由通过 claims_to_verify 明确要求提供 exp。当该列表未设置或为空时,APISIX 仅在 Token 包含 exp 或 nbf 时校验它们。Token 请求头中的 alg 必须与凭证配置的算法一致;如果算法不匹配,APISIX 会在验证签名前拒绝请求。如果验证过程超过五分钟,请重新生成 Token。
验证
使用 john 的密钥向路由发送请求:
curl -i "http://127.0.0.1:9080/anything" -H "Authorization: ${john_jwt_token}"
你应该看到类似以下的 HTTP/1.1 200 OK 响应:
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqb2huLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.TgoM2efMEaSX7KrIWujGLGK5dpPGS2nbaiinmVVjKKw",
"Host": "127.0.0.1",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-6873b51b-5502cfcd72380323266c22e8",
"X-Consumer-Custom-Id": "john-doe-junior",
"X-Consumer-Username": "johndoe",
"X-Credential-Identifier": "cred-john-jwt-auth",
"X-Forwarded-Host": "127.0.0.1"
},
...
}
等待先前验证请求所处的 30 秒配额窗口结束,然后使用 john 的密钥向路由发送三个请求:
sleep 31
resp=$(seq 3 | xargs -I{} curl "http://127.0.0.1:9080/anything" -H "Authorization: ${john_jwt_token}" -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 "http://127.0.0.1:9080/anything" -H "Authorization: ${jane_jwt_token}" -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 -i "http://127.0.0.1:9080/anything" -H 'Authorization: somewrongkey'
你应该看到 HTTP/1.1 401 Unauthorized 响应,并带有以下消息:
{"message":"JWT token invalid"}
下一步
你现在已经学会了如何实施基本认证。APISIX 支持其他内置身份认证方法,例如 密钥认证、基本认证 和 HMAC 认证。