推理服务 JWT 认证
推理服务(InferenceService)支持开启 JWT 统一认证:只有携带平台为该服务签发体系下生成的合法 token 的请求才能访问,其余请求被 istio 网关直接拒绝。
原理概览
- 在创建/编辑推理服务时,给
sidecar字段勾选jwt(可同时勾istio;jwt依赖istio,平台会自动补上,见下文)。 - 平台用
jwcrypto为该服务生成一对 RSA 公私钥(kid= 服务英文名),存进服务的 扩展字段(expand)。 - 服务部署时,平台基于公钥下发 istio 的
RequestAuthentication+AuthorizationPolicy,校验请求头里的 token。 - 客户端用 私钥 自行签发 token,请求时带
Authorization: Bearer <token>即可通过。
sidecar 字段定义(myapp/views/view_inferenceserving.py:255-262),描述为"容器的 agent 代理,istio 用于服务网格,jwt 用于统一认证(需要客户端在请求头中添加开发者生成的 token)",可选值 istio / jwt。
一、平台侧:公私钥的生成与存储
勾选 jwt 后,保存服务时平台自动生成密钥(myapp/views/view_inferenceserving.py:811-827):
# sidecar 含 jwt 且 expand 里还没有 public_key 时,生成一对 RSA 公私钥
if item.sidecar and 'jwt' in item.sidecar:
expand = json.loads(item.expand)
if 'public_key' not in expand:
from jwcrypto import jwk
# kid 用服务英文名 item.name,所以每个服务的公私钥都不同
key = jwk.JWK.generate(kty='RSA', size=2048, alg='RS256', kid=item.name)
expand['public_key'] = json.loads(key.export_public())
expand['private_key'] = json.loads(key.export_private())
item.expand = json.dumps(expand, indent=4, ensure_ascii=False)
# 取消勾选 jwt 时,会从 expand 里删除 public_key / private_key
要点:
- 密钥算法固定为
RSA / 2048 / RS256;kid= 服务英文名(item.name),因此每个服务的公私钥都不一样。 - 公钥
public_key、私钥private_key都存在服务的 扩展字段 expand(数据库inferenceserving.expand)。 jwt必须配合istio:保存时若只勾了jwt没勾istio,平台在pre_update自动补上istio(myapp/views/view_inferenceserving.py:877-879)。
在哪里拿到公私钥
打开对应推理服务的 "详情",在 扩展字段(expand) 中即可看到 public_key 和 private_key(JSON 结构)。客户端签 token 用 private_key。
注意:原始文档示例里展示了一段明文私钥(
kid=decisiontree-20231001),仅作格式示例,切勿在生产中使用示例密钥。实际密钥请从你自己服务的扩展字段中读取。
也可以离线手动生成同样结构的密钥(与平台逻辑一致):
from jwcrypto import jwk
import json
# kid 用你的服务英文名
key = jwk.JWK.generate(kty='RSA', size=2048, alg='RS256', kid='<服务英文名>')
public_key = key.export_public() # 公钥(JSON 字符串)
private_key = key.export_private() # 私钥(JSON 字符串)
print(public_key)
print(private_key)
公钥 / 私钥的 JSON 结构示例(脱敏,仅看字段):
// public_key
{
"alg": "RS256",
"e": "AQAB",
"kid": "<服务英文名>",
"kty": "RSA",
"n": "<base64url 模数,已脱敏>"
}
// private_key(比公钥多 d/p/q/dp/dq/qi 等私有参数,全部脱敏)
{
"alg": "RS256",
"d": "<已脱敏>",
"dp": "<已脱敏>",
"dq": "<已脱敏>",
"e": "AQAB",
"kid": "<服务英文名>",
"kty": "RSA",
"n": "<已脱敏>",
"p": "<已脱敏>",
"q": "<已脱敏>",
"qi": "<已脱敏>"
}
二、平台侧:部署时下发 istio 校验规则
服务部署(deploy)时,若 sidecar 含 jwt,平台用公钥生成 jwks,并下发两个 istio CRD(myapp/views/view_inferenceserving.py:1724-1783):
security.istio.io/v1beta1RequestAuthentication:issuer= 服务英文名(service.name)——所以客户端 token 的iss必须等于服务英文名。jwks= 服务公钥组成的{"keys":[public_key]}。forwardOriginalToken: true(校验后把原始 token 透传给后端容器)。
security.istio.io/v1beta1AuthorizationPolicy:action: DENY,规则notRequestPrincipals: ["*"]——即 没有带合法认证主体(未通过 JWT 校验)的请求一律拒绝。
删除服务时会一并清理这两个 CRD(requestauthentications / authorizationpolicies,见 myapp/views/view_inferenceserving.py:865-866)。
三、客户端:签发 token
用服务的 私钥 签发 token。iss 必须等于服务英文名(与 istio RequestAuthentication.issuer 对应):
import datetime
from jwcrypto import jwk, jwt
private_key = '''<从服务扩展字段复制的 private_key JSON 字符串>'''
claims = {
"iss": "<服务英文名>", # 必须与服务名一致,否则 istio 校验不过
"sub": "pengluan", # 用户名(自定义)
"iat": int(datetime.datetime.now().timestamp()), # 签发时间
"exp": int(datetime.datetime.now().timestamp() + 60*60*24*365), # 过期时间(示例:1 年)
# "nbf": int(datetime.datetime.now().timestamp()), # 可选:生效时间
}
# 用 RS256 算法签名
token = jwt.JWT(header={'alg': 'RS256'}, claims=claims)
token.make_signed_token(jwk.JWK.from_json(private_key))
serialized_token = token.serialize()
print(serialized_token)
四、客户端:本地验证 token(可选)
用服务的公钥本地校验 token 是否合法:
from jwcrypto import jwk, jwt
public_key_json = '''<从服务扩展字段复制的 public_key JSON 字符串>'''
serialized_token = '<上一步生成的 token>'
public_key = jwk.JWK.from_json(public_key_json)
verified_token = jwt.JWT(key=public_key, jwt=serialized_token)
print(verified_token.claims)
五、客户端:携带 token 调用服务
请求头加上 Authorization: Bearer <token> 即可访问开启了 JWT 的推理服务:
curl --header "Authorization: Bearer $token" https://<服务域名>/<推理路径>
不带 token 或 token 非法的请求,会被 istio AuthorizationPolicy 直接拒绝(403)。
相关源码
| 功能 | 位置 |
|---|---|
sidecar 字段(istio/jwt 可选项) |
myapp/views/view_inferenceserving.py:255-262 |
| 生成/删除公私钥到 expand | myapp/views/view_inferenceserving.py:811-827 |
| jwt 自动补 istio | myapp/views/view_inferenceserving.py:877-879 |
| 部署下发 RequestAuthentication / AuthorizationPolicy | myapp/views/view_inferenceserving.py:1724-1783 |
| 删除服务时清理 istio CRD | myapp/views/view_inferenceserving.py:865-866 |
服务模型 sidecar/expand 字段(InferenceService 类,行 202 起) |
myapp/models/model_serving.py:239(sidecar)、:246(expand) |