跳到主要内容

JWT 企业 IdP 认证

适用认证方式:开放接口 authType = jwt 凭证来源:企业身份提供方(IdP)签发的 JWT access token,ApiGo 只验签、不签发 适用场景:已统一使用 Keycloak、Azure AD、Okta、Authing 等企业 IdP,希望复用现有身份体系调用 API 的组织

认证方式概览​

authType名称令牌来源请求头
none无认证--
app_key简单认证平台应用凭证X-AppKey
app_secret签名认证平台应用凭证X-AppKey、X-Timestamp、X-Nonce、X-Sign
oauth2平台 OAuth2平台用 AppKey/AppSecret 换取Authorization: Bearer <access_token>
jwt企业 IdP JWT企业 IdP 签发Authorization: Bearer <enterprise_token>

JWT 认证链路:

  1. 网关从 Authorization: Bearer 中取出 token;
  2. 用未验签的 iss 定位注册表中的 IdP,随后只用该 IdP 的 JWKS 公钥验签;
  3. 校验 iss/aud/exp/nbf/iat/jti/client_id/typ 等 claims;
  4. 用 (jwt_idp, jwt_subject) 反查绑定的应用凭证;
  5. 复用平台既有的应用启用、项目隔离、IP 白名单、接口授权、订阅与限流链路。

与平台 OAuth2 的区别:

项平台 OAuth2企业 IdP JWT
token 签发方ApiGo 网关 /oauth2/token企业 IdP
调用方凭证AppKey / AppSecretIdP 的 client credentials 或用户授权
ApiGo 是否持有密钥是(AppSecret)否(只保存 IdP 公钥)
应用识别方式token 内 appKey应用绑定的 (jwt_idp, subject)
错误契约HTTP 200 + 业务码HTTP 401/403/503 + WWW-Authenticate
MCP 说明

当前版本 MCP 入口仍只支持 app_key 与平台 OAuth2,企业 IdP JWT 暂不能用于 /mcp/{appCode},二期按 iss 分发后开放。

前置条件​

管理员:注册企业 IdP​

网关与管理端共用 apigo.jwt.* 配置(Nacos 公共配置或两端分别配置且保持一致)。最小示例:

apigo:
jwt:
enabled: true
resource: https://api.example.com
clock-skew-seconds: 60
idps:
corp-idp:
display-name: 企业 IdP
issuer: https://idp.example.com/realms/corp
discovery: true
audiences:
- https://api.example.com
token-profile: compat # Keycloak / Azure AD 通常选 compat
algorithms:
- RS256

主要配置项:

配置默认值说明
apigo.jwt.enabledfalse总开关;关闭时 authType=jwt 接口返回 503/40030,管理端不提供绑定入口
apigo.jwt.resource空资源标识,用于文档与二期 RFC 9728 元数据
apigo.jwt.clock-skew-seconds60全局时钟偏移,可按 IdP 覆盖(0–300)
apigo.jwt.max-token-bytes8192token 最大字节数,超限按格式错误拒绝
apigo.jwt.forward-identity-headersfalse是否向下游注入 X-Apigo-* 身份头
apigo.jwt.disable-static-credentials-for-bound-appsfalse开启后,已绑定 JWT 的应用禁止再用 appKey / appSecret 调用(REST 与 MCP 均生效),避免静态凭证绕过 IdP 治理;oauth2 与 jwt 不受影响
apigo.jwt.idps.{key}.issuer必填必须与 token 的 iss 精确匹配,且不能是平台保留 issuer
apigo.jwt.idps.{key}.discoverytrue通过 issuer/.well-known/openid-configuration 获取 jwks_uri
apigo.jwt.idps.{key}.jwks-uri空discovery=false 时必填
apigo.jwt.idps.{key}.allow-httpfalse是否允许 http:// 的内网 IdP
apigo.jwt.idps.{key}.allow-private-networkfalse是否允许 JWKS 指向私网地址(元数据/链路本地地址始终拒绝)
apigo.jwt.idps.{key}.audiences必填允许的 audience 列表,与 token 的 aud 必须有交集
apigo.jwt.idps.{key}.token-profilerfc9068rfc9068 严格模式;compat 兼容常见 IdP
apigo.jwt.idps.{key}.subject-claimsub用于绑定与日志的主体 claim
apigo.jwt.idps.{key}.display-claimpreferred_username展示名 claim,缺失时回落 subject
apigo.jwt.idps.{key}.scope-claimscopescope claim 名,兼容 scp
apigo.jwt.idps.{key}.algorithmsRS256, ES256允许的签名算法,还可选 PS256、EdDSA
apigo.jwt.idps.{key}.require-kidtrue是否强制 JWS header 携带 kid
apigo.jwt.idps.{key}.max-token-lifetime-seconds0限制 exp - iat 的最大时长,0 表示不限制
apigo.jwt.idps.{key}.required-claims空额外必填 claim 及期望值,如 token_use: access
apigo.jwt.jwks.*-JWKS 超时、缓存 TTL、轮换冷却、stale-if-error 等

enabled=true 时配置错误会直接阻止服务启动,包括:IdP key 不合法、issuer 重复或非法、audiences 为空、算法不在允许集合内、discovery=false 却没有 jwks-uri。

管理员:绑定应用​

JWT 认证必须先建立「IdP 主体 → 应用凭证」的绑定关系:

  1. 在 账户中心 → 应用凭证 创建或编辑凭证;
  2. 在「JWT 身份绑定」区块选择 IdP,并填写主体值(默认取 token 的 sub,需与 claim 字符串完全一致);
  3. IdP 与主体必须同时填写或同时为空,只填一项会被拒绝;
  4. 同一项目内一个 (jwt_idp, jwt_subject) 只能绑定一个应用。

也可以用管理端接口维护绑定:

GET /api/box/sys/app/jwt-idps
[
{ "key": "corp-idp", "displayName": "企业 IdP", "issuer": "https://idp.example.com/realms/corp" }
]
PUT /api/box/sys/app/{appId}/jwt-binding
Content-Type: application/json

{ "jwtIdp": "corp-idp", "jwtSubject": "service-account-order" }

两个字段都传空表示解绑。绑定写入后同步维护 Redis 反向索引,网关按 (projectId, idpKey, subject) 直接定位应用。

管理员:设置接口认证方式​

在接口编辑页把认证方式改为「企业 IdP JWT」。接口级凭证仍需先获得该接口的授权;项目级凭证按项目范围放行。

调用方:确认 token 条件​

  • token 的 iss 与注册的 issuer 完全一致;
  • token 的 aud 包含注册的任一 audience;
  • token 未过期,且时钟偏差在允许范围内;
  • token 的主体值与绑定值一致;
  • token-profile: rfc9068 时还需满足 typ=at+jwt、jti、client_id、iat 等要求。

获取 token​

token 由企业 IdP 签发,换取方式取决于企业的 IdP 与应用类型。以下示例仅说明通用形态,不绑定具体厂商。

Keycloak(client credentials)​

curl -sS -X POST \
'https://idp.example.com/realms/{realm}/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id={client_id}' \
--data-urlencode 'client_secret={client_secret}'

Keycloak 默认 access token 的 typ 为 Bearer,通常需要:

  • token-profile: compat;
  • 为 client 配置 audience mapper,使 aud 包含 ApiGo 注册的 audience;
  • 用 client credentials 的用户主体或服务账号主体作为绑定 subject。

Azure AD(client credentials)​

curl -sS -X POST \
'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id={client_id}' \
--data-urlencode 'client_secret={client_secret}' \
--data-urlencode 'scope={api-application-id-uri}/.default'

Azure AD access token 的 aud 通常是受保护 API 的 Application ID URI,typ 为 JWT,应使用 token-profile: compat。应用令牌可能没有 scope/scp,只有 roles,此时可配置 scope-claim: roles,或通过 required-claims 固定校验关键 claim。

用户授权(authorization code)​

如果希望以「用户身份」调用,可由前端或网关外的登录流程通过 authorization code + PKCE 获取 access token,再交给后端调用 ApiGo。注意:

  • 用户 token 的 sub 必须与绑定值一致;
  • 浏览器侧不得持有 ApiGo AppSecret;
  • 用户 token 的受众、scope 与有效期需满足 IdP 注册配置。

ApiGo 不提供向外部 IdP 换取 token 的代理接口,也不会接收企业的 client_secret。

调用示例​

cURL​

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="https://api.example.com"
IDP_TOKEN_URL="https://idp.example.com/realms/corp/protocol/openid-connect/token"
TOKEN="$(curl -sS -X POST "${IDP_TOKEN_URL}" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode "client_id=${IDP_CLIENT_ID}" \
--data-urlencode "client_secret=${IDP_CLIENT_SECRET}" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')"

curl -sS "${BASE_URL}/open/api/demo?page=1" \
-H "Authorization: Bearer ${TOKEN}"

失败时响应体形如:

{
"code": 40029,
"msg": "JWT无效或已过期",
"error": "invalid_token",
"error_description": "the access token is invalid or expired"
}

响应头同时包含:

WWW-Authenticate: Bearer realm="apigo", error="invalid_token", error_description="the access token is invalid or expired"

Java(JDK 11+ HttpClient)​

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

public class JwtClientDemo {

private static final String BASE_URL = "https://api.example.com";

public static void main(String[] args) throws Exception {
// 生产环境请在服务端实现 token 获取与缓存,此处假设 token 已由企业 IdP SDK 取得
String token = System.getenv("IDP_ACCESS_TOKEN");
HttpRequest request = HttpRequest.newBuilder(URI.create(BASE_URL + "/open/api/demo?page=1"))
.header("Authorization", "Bearer " + token)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println(response.statusCode());
System.out.println(response.body());
}
}

Node.js​

const BASE_URL = 'https://api.example.com'

async function callApi(token) {
const res = await fetch(`${BASE_URL}/open/api/demo?page=1`, {
headers: { Authorization: `Bearer ${token}` },
})
const text = await res.text()
if (res.status === 401) {
// token 失效:向企业 IdP 重新获取后再试一次
throw new Error(`token rejected: ${text}`)
}
if (res.status === 403) {
// 主体未绑定 / 应用禁用 / IP 不符 / 无接口授权
throw new Error(`forbidden: ${text}`)
}
if (res.status === 503) {
// IdP 暂不可用:按 Retry-After 退避重试
throw new Error(`idp unavailable: ${text}`)
}
return text
}

Python(requests)​

import requests

BASE_URL = "https://api.example.com"


def call_api(token: str) -> requests.Response:
return requests.get(
f"{BASE_URL}/open/api/demo",
params={"page": 1},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)


resp = call_api("enterprise_idp_access_token")
if resp.status_code == 401:
# 重新向企业 IdP 获取 token 后重试一次
pass
elif resp.status_code == 403:
# 检查应用绑定、应用状态、IP 白名单与接口授权
pass
elif resp.status_code == 503:
# 检查 Retry-After,退避后重试
pass

调用方不应在收到 401 时无限重试。建议先刷新一次 token,仍失败则检查 iss/aud/exp 与绑定关系。

Claims 要求​

通用要求​

项要求
iss必须与注册的 issuer 精确匹配(区分大小写)
aud字符串或字符串数组,与配置的 audiences 至少有一个交集
主体 claim默认 sub,非纯空白、原文不超过 256 字符、不能包含控制字符;保留首尾空格及大小写,按原文精确匹配
exp必填,数值型 Unix 秒;exp <= now - skew 视为过期
nbf可选,若存在则不能晚于 now + skew
iat严格模式必填;不能晚于 now + skew;配置了 max-token-lifetime-seconds 时 exp - iat 不得超限
jti严格模式必填;长度不超过 256 且不能包含控制字符
client_id严格模式必填,字符串
ID token 特有 claimnonce、at_hash、c_hash 出现即拒绝,防 ID token 混淆
额外 claims通过 required-claims 配置后必须存在且值一致

两种 profile​

规则rfc9068(默认,严格)compat(常见 IdP)
JWS typ必须为 at+jwt 或 application/at+jwtJWT、Bearer、at+jwt、application/at+jwt 均可
iat必填可选
jti必填可选
client_id必填可选
access token 判定若存在 token_use,必须精确为 access存在 token_use 时必须精确为 access;仅在该字段缺省时允许 scope/scp(或配置的 scope claim)非空

兼容模式必须逐 IdP 验证访问令牌判据,并用 required-claims 约束其专有标记;scope 非空本身不能证明令牌用途。资源 audience 不应接受该 IdP 登录客户端的 ID token audience。

JWS header 限制​

  • 算法必须属于该 IdP 配置的 algorithms,支持 RS256、ES256、PS256、EdDSA;
  • 拒绝 none 与所有 HS* 算法;
  • 默认要求携带 kid(require-kid: true);
  • 拒绝 jku、x5u、x5c、jwk、crit、zip,避免外部密钥注入;
  • 拒绝 b64: false(RFC 7797 未编码载荷)。

scope 说明​

MVP 只解析并记录 scope,不参与接口授权判定;接口权限仍由平台的应用授权与订阅决定。错误码 40032 为后续 scope 校验预留。

错误码与排查​

JWT 接口使用标准 HTTP 语义,响应体仍包含平台 code/msg,并按 RFC 6750 附加 error/error_description:

HTTPcodeerror含义排查方向
40140028-未携带 Authorization 或 Bearer token补请求头;确认前缀为 Bearer
40040029invalid_requestAuthorization 格式错误、多个 Authorization 头、token 超长只保留一个头;检查 token 是否超过 8192 字节
40140029invalid_token签名、iss、aud、exp、typ、kid 或 claims 不合法逐项核对 Claims 要求;确认 token 来自已注册 IdP
40340031-(jwt_idp, subject) 未绑定任何应用,或索引与应用不一致在凭证中配置 JWT 身份绑定;确认 subject 完全一致
40340019-应用已禁用启用应用凭证
40340002-无接口授权,或项目级凭证跨项目调用补充订阅/授权,或使用本项目凭证
40340018-来源 IP 不在应用白名单补充出口 IP 或调整白名单
40340032-scope 不足(预留)检查 IdP 签发的 scope
50340030-JWT 功能关闭、IdP/JWKS 不可用检查 apigo.jwt.enabled、IdP 可达性与 JWKS 配置;按 Retry-After 重试
50350001-Redis/缓存等内部依赖异常稍后重试并查看网关日志

补充说明:

  • 503 响应带 Retry-After: 5;
  • 接口不存在、接口已下线等路由层错误在认证之前判定,仍返回 HTTP 200 + 业务码(40001 / 40006),与存量契约一致;
  • WWW-Authenticate 只携带 realm 与 RFC 定义的 error/error_description,不暴露内部异常信息。

常见排查顺序:

  1. 用 IdP 的 introspection 或调试工具确认 token 未过期、iss/aud 正确;
  2. 解码 token 确认主体值,与凭证绑定值逐字比对(注意空格与大小写);
  3. 确认接口认证方式为 jwt、应用已启用、IP 在白名单内;
  4. 若返回 503,检查网关到 IdP 的网络与 JWKS 地址;
  5. 若仅在密钥轮换后失败,等待缓存刷新或确认 kid 已出现在 JWKS 中。

密钥轮换与时钟要求​

  • JWKS 缓存:默认 TTL 600 秒;到期后刷新,刷新失败时在 stale-ttl-seconds(默认 24 小时)内继续使用旧公钥;
  • 主动刷新:遇到未知 kid 时允许一次主动刷新,冷却时间默认 30 秒,避免异常 token 打爆 IdP;
  • 轮换方式:建议在 IdP 中先发布新密钥,新旧密钥并存一个 token 有效期以上,再下线旧密钥;每次轮换保持 kid 稳定且唯一;
  • 时钟:默认允许 60 秒偏移,可按 IdP 调整(0–300);调用方与 IdP 均应使用 NTP 同步;
  • token 生命周期:建议 access token 有效期不超过 15 分钟;需要更强约束时配置 max-token-lifetime-seconds。

安全建议​

  1. 全程 HTTPS:issuer 默认必须为 https://,仅内网 IdP 可显式开启 allow-http;
  2. 只信任配置的密钥来源:禁止 jku/x5u/x5c/jwk,JWKS 地址默认禁止重定向、私网地址需显式放行;
  3. 短时效 token:由 IdP 控制有效期,调用方按需刷新,不要把 token 写入长期存储;
  4. token 不落日志、不进 URL:只放在 Authorization 请求头,避免被代理与访问日志采集;
  5. 最小绑定:一个主体只绑定必要应用;员工离职或服务账号停用后及时解绑;
  6. 受众隔离:不同环境、不同 API 使用不同 audience,防止 token 跨环境复用;
  7. 下游隔离:JWT 认证成功后网关会剥离 Authorization、X-AppKey、X-Timestamp、X-Nonce、X-Sign、X-User-Id、X-User-Name、X-Create-By、X-Apigo-* 请求头;默认不向目标接口注入身份头,避免 token passthrough 与身份伪造;
  8. 错误处理:401 刷新一次 token 后重试;403 不要重试,按提示检查绑定与授权;503 按 Retry-After 退避。

速查卡​

① 管理员注册 IdP:apigo.jwt.enabled=true + idps.{key}.issuer/audiences
② 绑定应用:凭证编辑页「JWT 身份绑定」= (IdP, subject)
③ 接口认证方式:企业 IdP JWT
④ 调用方取 token:由企业 IdP 签发(client credentials / 授权码)
⑤ 调用:Authorization: Bearer <enterprise_token>
⑥ 校验:iss + aud + exp + 签名(JWKS)+ 主体绑定
⑦ 成功:走平台应用启用 / 项目隔离 / IP / 授权 / 限流
⑧ 失败:401 invalid_token → 刷新 token;403 → 检查绑定/授权/IP;503 → 退避重试

相关文档​