JWT在AI服务鉴权中有两种常见签名方式:HS256和RS256。HS256使用同一个密钥进行签名和验证,而RS256使用RSA非对称密钥对,私钥负责签发,公钥负责验证。对于面向多租户或开放平台的AI推理服务来说,如果采用HS256,客户端和服务端共享同一个密钥,一旦服务端密钥库泄露,攻击者就可以自行签发任意用户身份的令牌。RS256把密钥管理拆分开,AI服务只保存公钥,私钥只存在于签发服务中,即使AI服务被拖库,攻击者也无法伪造合法令牌。

RS256非对称签名在AI服务中的优势
AI服务通常由推理网关、模型服务、任务队列等多个内部组件组成,外部客户端通过API获取模型推理结果。如果所有组件都持有HS256共享密钥,任何一个组件被攻破都会导致全局令牌伪造。RS256可以避免这种横向扩散,因为验证方只需要公钥,没有私钥就无法签发新令牌。公钥可以通过JWKS端点公开分发,任何验证方都能拉取,但只有持有私钥的授权服务才能产生有效签名。
另一个优点是密钥轮换更方便。当需要更换签名密钥时,签发端生成新的RSA密钥对,把新公钥追加到JWKS文档中,并设置新的kid。验证端根据令牌头里的kid自动匹配对应公钥,旧令牌还能用旧公钥验证,新令牌用新公钥验证,整个过程不需要重启推理服务。HS256要做到平滑轮换,需要在所有节点同步更换共享密钥,容易出现短暂鉴权失败。
RS256也有代价:令牌体积会更大,验证计算量高于HMAC。RSA签名验证通常需要几次模幂运算,对于高并发推理请求会带来一定CPU开销。实际部署时可以在网关层做JWT验证并缓存结果,或者使用EdDSA等更轻量算法替代。不过在生态兼容性上,RS256被绝大多数JWT库和API网关支持,仍是AI服务最稳妥的非对称选择。
生成RSA密钥对并构建JWKS公钥端点
先使用OpenSSL生成2048位或4096位RSA私钥。4096位安全性更高,但验证性能稍差,AI推理请求量大时建议用2048位。生成私钥后提取对应公钥,并把公钥的模数n和指数e转换为Base64URL编码,写入JWKS文档。下面命令展示从生成私钥到输出公钥PEM的过程。
# 生成2048位RSA私钥 openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 # 从私钥中提取公钥 openssl rsa -in private_key.pem -pubout -out public_key.pem # 查看公钥内容 cat public_key.pem
JWKS文档是一个JSON对象,包含keys数组。每个公钥需要提供kty、use、alg、kid、n和e字段。其中n是RSA公钥模数的Base64URL编码,e是公钥指数的Base64URL编码,通常为AQAB表示65537。kid是密钥标识符,必须与JWT头部的kid字段一致,否则验证端无法选择对应公钥。
可以使用Python脚本从PEM公钥自动生成JWKS文档,避免手动计算n和e。下面示例读取public_key.pem,解析出n和e并输出JWKS JSON。生成的文档可以托管在Nginx静态目录,或者由AI服务自身的元数据接口返回。
import json
from cryptography.hazmat.primitives import serialization
from base64 import urlsafe_b64encode
def b64url(data: bytes) -> str:
return urlsafe_b64encode(data).rstrip(b'=').decode('ascii')
with open('public_key.pem', 'rb') as f:
pub_key = serialization.load_pem_public_key(f.read())
pub_numbers = pub_key.public_numbers()
n = pub_numbers.n
e = pub_numbers.e
n_bytes = n.to_bytes((n.bit_length() + 7) // 8, 'big')
e_bytes = e.to_bytes((e.bit_length() + 7) // 8, 'big')
jwks = {
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "ai-service-signing-key-2024",
"n": b64url(n_bytes),
"e": b64url(e_bytes)
}
]
}
print(json.dumps(jwks, indent=2))把生成的JWKS文档保存为jwks.json并放在可访问的HTTPS地址下。推荐使用单独的公钥端点路径,例如https://auth.ipipp.com/.well-known/jwks.json。这个地址不需要鉴权,但必须保证内容不被篡改,因为验证端会根据这里面的公钥判断令牌合法性。如果攻击者能修改JWKS内容,就可能换成自己的公钥,所以JWKS端点应配置严格的写权限和HTTPS传输。
在AI服务中验证JWT并缓存公钥
验证端接收到JWT后,先解析头部拿到alg和kid,再从JWKS文档中找到kid一致的公钥进行签名验证。常用的Python库包括PyJWT和python-jose,可以配合requests拉取JWKS。下面的示例使用PyJWT和手动缓存公钥的方式,演示在FastAPI依赖中验证令牌。实际项目中可以把JWKS拉取和公钥缓存封装成独立模块。
import jwt
import requests
from jwt import PyJWKClient
JWKS_URL = "https://auth.ipipp.com/.well-known/jwks.json"
AUDIENCE = "ai-inference-service"
ISSUER = "https://auth.ipipp.com/"
jwks_client = PyJWKClient(JWKS_URL)
def verify_token(token: str):
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=ISSUER,
options={"verify_exp": True}
)
return payloadPyJWKClient内部会缓存拉取到的公钥,默认按kid建立索引,避免每次验证都请求JWKS端点。如果请求量很大,可以在网关层增加进程内缓存并设置合理的TTL,例如5到15分钟。缓存过期后重新拉取,既降低了公钥端点压力,又能在密钥轮换后及时获取新公钥。需要注意,如果验证时出现kid不匹配错误,不要立即清空整个缓存,可以先尝试拉取最新JWKS并重试一次。
JWT验证还应该校验exp、nbf、iss、aud等声明。AI推理接口可能有不同模型对应不同权限,可以在payload中增加自定义声明,比如model_access或quota。验证通过后,把这些声明放入请求上下文中,后续权限判断就能直接使用。不要在AI服务内部继续传递原始JWT字符串,以免令牌泄露到日志或错误信息中。
常见配置问题与密钥轮换策略
密钥轮换时最容易出现的问题是新旧kid不一致。签发端使用新私钥生成JWT时,如果头部kid还是旧值,验证端会错误地选择旧公钥验证,导致签名不匹配。因此每次生成新密钥对时,必须同步生成新的kid并写入JWKS文档。JWKS文档里可以同时保留多个公钥,旧公钥用于验证历史令牌,新公钥用于新签发令牌。一般建议在JWKS中保留两代公钥,等旧令牌全部过期后再移除。
另一个常见问题是JWKS文档字段格式错误。n和e必须是Base64URL编码且不带填充等号,如果直接使用Base64编码或保留等号,部分JWT库会解析失败。验证前可以写单元测试,确认生成的JWKS能被PyJWKClient正常加载。还要注意JWKS URL必须使用HTTPS,HTTP明文传输的公钥存在被中间人替换的风险。
对于AI服务的高并发场景,可以在网关层做短期验证结果缓存。相同JWT在有效期内重复出现时直接返回缓存结果,但缓存键必须包含完整JWT或签名部分,避免只缓存用户ID导致越权。更稳妥的做法是验证通过后仅在单次请求上下文中传递解析出的身份信息,不缓存令牌本身。RSA验证的CPU消耗通常可接受,过度缓存反而会增加令牌被重放的窗口,需根据实际负载权衡。
JWT鉴权RS256非对称加密JWKS公钥端点修改时间:2026-09-25 20:03:19