推理服务 JWT 认证

给推理服务/模型服务加 token 鉴权,了解公私钥怎么生成、客户端怎么签 token 调用、istio 怎么校验时读这篇

06-二次开发 / 服务与认证
JWTjwt推理服务认证jwcryptojwkRS256RSApublic_keyprivate_keyAuthorizationBearertokenistioRequestAuthenticationAuthorizationPolicysidecar

推理服务 JWT 认证

推理服务(InferenceService)支持开启 JWT 统一认证:只有携带平台为该服务签发体系下生成的合法 token 的请求才能访问,其余请求被 istio 网关直接拒绝。

原理概览

  • 在创建/编辑推理服务时,给 sidecar 字段勾选 jwt(可同时勾 istiojwt 依赖 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 / RS256kid = 服务英文名(item.name),因此每个服务的公私钥都不一样。
  • 公钥 public_key、私钥 private_key 都存在服务的 扩展字段 expand(数据库 inferenceserving.expand)。
  • jwt 必须配合 istio:保存时若只勾了 jwt 没勾 istio,平台在 pre_update 自动补上 istiomyapp/views/view_inferenceserving.py:877-879)。

在哪里拿到公私钥

打开对应推理服务的 "详情",在 扩展字段(expand) 中即可看到 public_keyprivate_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)时,若 sidecarjwt,平台用公钥生成 jwks,并下发两个 istio CRD(myapp/views/view_inferenceserving.py:1724-1783):

  • security.istio.io/v1beta1 RequestAuthentication
    • issuer = 服务英文名(service.name)——所以客户端 token 的 iss 必须等于服务英文名
    • jwks = 服务公钥组成的 {"keys":[public_key]}
    • forwardOriginalToken: true(校验后把原始 token 透传给后端容器)。
  • security.istio.io/v1beta1 AuthorizationPolicy
    • 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)
最后更新 2026-06-30完整文档以官方仓库为准:GitHub Wiki