Skip to content

单点登录接入

本指南面向接入知办AI 应用栏的 H5 应用开发者。用户从知办AI 打开应用后,接入应用的后端使用一次性授权码换取令牌,校验 ID Token 并读取用户与租户身份,再建立应用自己的登录会话。

整体流程

接入应用开发者需要调用的知办AI 接口只有以下几类。一次性登录链接由知办AI 客户端获取,接入应用无需调用应用启动接口。

接口用途请求/响应说明
POST /api/external/v1/oauth/tokenauthorization_code一次性授权码换令牌第 3 节
GET /.well-known/openid-configuration读取签发方、Token 接口和 JWKS 地址第 4.1 节
GET /.well-known/jwks.json获取 ID Token 验签公钥第 4.2 节
POST /api/external/v1/oauth/tokenrefresh_token可选:轮换并刷新令牌第 7 节
/api/external/v1/* 资源接口可选:使用 Access Token 调已授权能力OpenAPI 规范

1. 获取应用配置

租户管理员在“应用管理 → 应用栏配置”创建应用后,把以下信息交给接入应用的后端维护人员

配置用途要求
客户端编号(client_id标识应用仅用于后端配置,不作为秘密。
客户端密钥(client_secret换取令牌时认证应用仅创建或重置时显示一次;存入密钥管理或环境变量。
启动地址知办AI 打开的 H5 地址生产环境使用 HTTPS。
回调地址(redirect_uri授权码交换时校验必须与后台登记值完全一致,不支持通配符。
授权范围限制应用可调用的知办AI API按最小权限申请;只做免登可以不申请业务接口权限。

不要把 client_secret 写入 H5 前端、移动客户端、URL、日志或代码仓库。创建成功弹框只交付应用专属的客户端编号和客户端密钥;环境级协议地址统一由本指南提供。

2. 接收一次性授权码

用户点击应用后,知办AI 客户端打开启动地址:

text
https://app.example.com/zhiban-landing?code=ac_xxx&state=state_xxx&iss=https%3A%2F%2Fzhiban.example.com
参数含义处理要求
code短时、一次性的授权码立即提交给后端;不能作为用户编号或长期会话。
state本次启动的关联值原样提交给后端用于请求关联;不能作为身份凭证。
iss知办AI 令牌签发方提示后端必须与固定配置或 Discovery 的 issuer 比对,不能直接信任 URL。

H5 页面只负责转交参数。客户端密钥、Token 交换和身份校验必须全部在后端完成。

登录回调页只负责提交授权码、完成登录并进入业务页面。审批、提交、支付等敏感业务操作应在登录成功后由用户主动确认,不要在登录回调中自动执行。联调页面可展示脱敏后的租户名称、用户姓名和数字员工名称,便于确认身份映射是否正确;正式业务页面按产品需要展示。

下面的 /api/auth/zhiban-app 是接入应用自行实现的后端路由示例,不是知办AI 接口;它的响应格式和本地业务会话由接入应用自行定义。

javascript
const query = new URLSearchParams(location.search);

await fetch('/api/auth/zhiban-app', {
  method: 'POST',
  credentials: 'same-origin',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    code: query.get('code'),
    state: query.get('state'),
    iss: query.get('iss'),
  }),
});

登录完成后使用 history.replaceState 或服务端重定向移除 URL 中的授权码,避免进入历史记录、日志和 Referer。

3. 后端交换令牌

接入应用后端调用知办AI Token 接口:

http
POST https://<知办AI环境域名>/api/external/v1/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=ac_xxx&client_id=app_xxx&client_secret=sk_xxx&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback

请求体使用 OAuth 2.0 标准的 application/x-www-form-urlencoded 编码。

请求参数:

参数必填类型说明
grant_typestring固定为 authorization_code
codestring启动地址中的一次性授权码,短时有效且只能使用一次。
client_idstring创建应用后取得的客户端编号。
client_secretstring创建或重置密钥时取得,仅允许由接入应用后端读取。
redirect_uristring(URI)必须与管理后台登记值完全一致,包括协议、域名、端口、路径和末尾斜杠。

成功响应:

json
{
  "access_token": "eyJ...",
  "id_token": "eyJ...",
  "refresh_token": "rt_xxx",
  "token_type": "Bearer",
  "expires_in": 7200,
  "scope": "kb.read contacts.read"
}

成功响应字段:

字段类型一定返回说明
access_tokenstringRS256 签名的访问令牌,用于调用已授权的知办AI API。
id_tokenstringRS256 签名的身份令牌,校验通过后读取用户、租户和成员信息。
refresh_tokenstring不透明刷新令牌;刷新时轮换,旧值立即失效。
token_typestring固定为 Bearer
expires_inintegerAccess Token 和 ID Token 的有效期,单位为秒。
scopestring空格分隔的授权范围;只做免登时可以为空字符串。

Token 响应不会把用户和租户作为顶层字段重复返回:

  • id_token:登录身份令牌,包含用户、租户和成员信息;校验成功后用于建立接入应用会话。
  • access_token:调用知办AI 已授权 API 的访问令牌;不能直接当作接入应用的 Session Cookie。
  • refresh_token:不透明、可轮换的刷新令牌;只在需要持续调用知办AI API 时保存。

错误响应当前使用知办AI 通用错误结构,不返回任何令牌字段:

json
{
  "success": false,
  "code": 4010,
  "message": "unauthorized: invalid_grant"
}
HTTP 状态code典型原因处理方式
4004000请求体无效、Content-Type 不支持、缺少字段或 grant_type 不支持修正请求,不要原样重试。
4014010客户端凭证无效、应用已停用,或授权码无效/过期/已消费/回调地址不匹配凭证问题检查配置;授权码问题重新启动应用。
4294290同一 client_id 请求过于频繁退避后重试,不要并发撞库。
5005000服务暂时异常保留脱敏后的请求时间和错误信息,联系知办AI 技术支持。

4. 校验 ID Token 并读取登录身份

4.1 读取发现配置

接入应用后端从知办AI 发现地址取得签发方、Token 接口和验签公钥地址。该接口无需鉴权,也没有请求参数:

http
GET https://<知办AI环境域名>/.well-known/openid-configuration

成功响应(200 OKCache-Control: public, max-age=3600):

json
{
  "issuer": "https://zhiban.example.com",
  "authorization_endpoint": "https://zhiban.example.com/api/agent-bus/v1/apps/{uuid}/launch",
  "token_endpoint": "https://zhiban.example.com/api/external/v1/oauth/token",
  "jwks_uri": "https://zhiban.example.com/.well-known/jwks.json",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
  "scopes_supported": ["openid", "email", "profile", "kb.read", "contacts.read"],
  "claims_supported": [
    "sub", "email", "email_verified", "name", "phone_number",
    "tenant", "membership", "zhiban_context", "nonce"
  ]
}

响应字段:

字段类型说明
issuerstring(URI)ID Token 的预期签发方,必须与 iss 完全一致。
authorization_endpointstring(URI 模板)知办AI 客户端使用的应用启动地址;接入应用无需调用。
token_endpointstring(URI)接入应用后端交换或刷新令牌的接口。
jwks_uristring(URI)获取 ID Token 验签公钥集合的接口。
response_types_supportedstring[]支持的响应类型,当前包含 code
grant_types_supportedstring[]环境支持的授权类型;本指南只使用 authorization_coderefresh_token
subject_types_supportedstring[]ID Token 主体标识类型,当前为 public
id_token_signing_alg_values_supportedstring[]ID Token 签名算法,当前只接受 RS256
token_endpoint_auth_methods_supportedstring[]Token 接口支持的客户端认证方式。本文示例使用请求体传递凭证。
scopes_supportedstring[]环境可用的 scope;应用实际权限以管理员授权和 Token 的 scope 为准。
claims_supportedstring[]ID Token 可能提供的 Claim 名称;可选资料不保证每个用户都有值。

4.2 获取 JWKS 公钥

使用发现响应中的 jwks_uri 请求公钥集合。该接口无需鉴权,也没有请求参数:

http
GET https://<知办AI环境域名>/.well-known/jwks.json

成功响应(200 OKCache-Control: public, max-age=3600):

json
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "k-active-xxx",
      "n": "<RSA modulus,Base64URL 编码>",
      "e": "AQAB"
    }
  ]
}
字段类型说明
keysobject[]当前可用于验签的 active/retiring 公钥列表,顺序不代表优先级。
keys[].ktystring密钥类型,当前为 RSA
keys[].usestring密钥用途,当前为签名验证 sig
keys[].algstring适用算法,当前为 RS256
keys[].kidstring密钥编号,与 ID Token Header 中的 kid 匹配。
keys[].nstringRSA 模数,Base64URL 编码。
keys[].estringRSA 公钥指数,Base64URL 编码,通常为 AQAB

公钥暂时不可用时返回 503 Service Unavailable,响应为 {"error":"keys_unavailable"}。此时可短时使用仍在缓存有效期内的 JWKS 或退避重试,但不能跳过校验。

Discovery 与 JWKS 必须按响应 Cache-Control 或本地 TTL 缓存,避免每次登录都请求知办AI 服务。若 ID Token Header 的 kid 在缓存 JWKS 中不存在,必须绕过缓存强制刷新 JWKS 一次后重新匹配;仍不存在则拒绝登录,不能降级为跳过校验。

4.3 校验并读取 ID Token

后端必须使用成熟 JWT/JWK 库完成以下校验:

  1. 按 JWT Header 的 kid 从 JWKS 选择公钥,只接受 RS256
  2. 校验签名,并确认 iss 与 Discovery 的 issuer 完全一致。
  3. 确认 aud 等于本应用的 client_id
  4. 校验 expiat

只有全部校验通过后才能读取 Claim。典型内容如下:

json
{
  "iss": "https://zhiban.example.com",
  "aud": "app_xxx",
  "sub": "acc_xxx",
  "iat": 1784073600,
  "exp": 1784080800,
  "name": "张三",
  "email": "zhangsan@example.com",
  "email_verified": true,
  "phone_number": "+8613800000000",
  "tenant": {
    "uuid": "tnt_xxx",
    "name": "示例企业"
  },
  "membership": {
    "employee_no": "10086",
    "title": "产品经理",
    "department": "产品部"
  },
  "zhiban_context": {
    "conversation_id": "conv_xxx",
    "conversation_kind": "agent_chat",
    "peer": {
      "kind": "agent",
      "uuid": "agent_xxx",
      "name": "HR 助手"
    }
  }
}

ID Token Claim:

Claim类型一定返回说明
issstring(URI)签发方,必须等于发现响应的 issuer
audstring令牌受众,必须等于本应用 client_id
substring知办AI 用户稳定标识;与 iss 组合为外部身份键。
iatinteger签发时间,Unix 秒。
expinteger过期时间,Unix 秒。
noncestring本次启动的关联值,不作为用户或租户身份字段。
namestring用户显示名称,可变资料,不作为账号唯一键。
emailstring用户邮箱,可变资料。
email_verifiedboolean邮箱是否已在知办AI 完成验证;缺失时不得按 true 处理。
phone_numberstring用户手机号,可变资料。
tenant.uuidstring条件返回当前租户稳定标识;存在 tenant 时返回。
tenant.namestring条件返回当前租户名称,可变资料。
membership.employee_nostring当前租户内工号。
membership.titlestring当前租户内职位。
membership.departmentstring当前租户内部门路径。
zhiban_contextobject本次启动的知办AI 会话上下文;从工作台等无会话入口打开时可缺失。
zhiban_context.conversation_idstring条件返回知办AI 会话稳定标识。
zhiban_context.conversation_kindstring条件返回会话类型,如数字员工会话、单聊或群聊。
zhiban_context.peer.kindstring条件返回会话对象类型:数字员工、用户或群聊。
zhiban_context.peer.uuidstring条件返回会话对象稳定标识。
zhiban_context.peer.namestring条件返回会话对象显示名称。

使用 iss + sub 作为外部身份稳定键,不要只按手机号、邮箱或显示名称合并账号。租户归属必须取自验签后的 tenant,不能接受前端另传租户编号覆盖。

不要只做 JWT Base64 解码,不要根据请求中的 iss 动态访问任意地址,也不要把单个验签公钥永久写死在代码中。JWKS 应按缓存策略保存,并在遇到未知 kid 时刷新一次以支持密钥轮换。

4.4 后端代码示例

应用单点登录不使用知办AI Agent SDK。Agent SDK 面向 Agent Runtime 和服务端 API 调用,与 H5 应用登录的信任边界不同。本流程应直接使用成熟的 OIDC/JWT 库,并在进程内复用 Discovery、JWKS 客户端和 Verifier。

Python(PyJWT)

安装依赖:

bash
pip install "PyJWT[crypto]>=2.10" "httpx>=0.27"

下面的初始化代码只执行一次,不要在每次登录请求中重新创建 PyJWKClient

python
from urllib.parse import urlparse

import httpx
import jwt

ZHIBAN_BASE_URL = "https://zhiban.example.com"  # 服务端固定配置
CLIENT_ID = "app_xxx"


def origin(url: str) -> tuple[str, str]:
    parsed = urlparse(url)
    return parsed.scheme, parsed.netloc


response = httpx.get(
    f"{ZHIBAN_BASE_URL}/.well-known/openid-configuration",
    timeout=5.0,
)
response.raise_for_status()
discovery = response.json()
issuer = discovery["issuer"]
jwks_uri = discovery["jwks_uri"]

# Discovery 地址来自固定知办AI环境,且签发方、公钥地址必须保持同源。
if origin(issuer) != origin(ZHIBAN_BASE_URL) or origin(jwks_uri) != origin(ZHIBAN_BASE_URL):
    raise RuntimeError("Discovery 返回了不受信任的地址")

jwks_client = jwt.PyJWKClient(
    jwks_uri,
    cache_jwk_set=True,
    lifespan=3600,
    timeout=5,
)


def verify_id_token(raw_id_token: str) -> dict:
    signing_key = jwks_client.get_signing_key_from_jwt(raw_id_token)
    claims = jwt.decode(
        raw_id_token,
        signing_key.key,
        algorithms=["RS256"],
        audience=CLIENT_ID,
        issuer=issuer,
        options={
            "require": ["iss", "aud", "sub", "iat", "exp"],
            "strict_aud": True,
        },
    )

    tenant = claims.get("tenant")
    if not isinstance(tenant, dict) or not tenant.get("uuid"):
        raise ValueError("ID Token 缺少租户身份")
    return claims


# token_response 是第 3 节 Token 接口返回的 JSON 对象。
claims = verify_id_token(token_response["id_token"])
user_id = claims["sub"]
tenant_id = claims["tenant"]["uuid"]
user_name = claims.get("name", "")

PyJWKClient 会缓存 JWKS;缓存中找不到 kid 时会刷新一次并重新匹配。调用方仍需捕获网络、JWKS 和 JWT 校验异常,失败时拒绝建立登录会话。

Go(go-oidc)

安装依赖:

bash
go get github.com/coreos/go-oidc/v3/oidc

issuer 必须来自服务端固定配置,不能使用启动 URL 中未经校验的 iss。Provider 和 Verifier 在进程启动时创建并跨请求复用:

go
package appsso

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/go-oidc/v3/oidc"
)

type AppClaims struct {
	Subject       string `json:"-"`
	Name          string `json:"name"`
	Email         string `json:"email"`
	EmailVerified bool   `json:"email_verified"`
	Tenant        struct {
		UUID string `json:"uuid"`
		Name string `json:"name"`
	} `json:"tenant"`
	Membership struct {
		EmployeeNo string `json:"employee_no"`
		Title      string `json:"title"`
		Department string `json:"department"`
	} `json:"membership"`
	ZhibanContext struct {
		Peer struct {
			Kind string `json:"kind"`
			UUID string `json:"uuid"`
			Name string `json:"name"`
		} `json:"peer"`
	} `json:"zhiban_context"`
}

func NewVerifier(ctx context.Context, issuer, clientID string) (*oidc.IDTokenVerifier, error) {
	provider, err := oidc.NewProvider(ctx, issuer)
	if err != nil {
		return nil, fmt.Errorf("load OIDC discovery: %w", err)
	}
	return provider.Verifier(&oidc.Config{
		ClientID:             clientID,
		SupportedSigningAlgs: []string{oidc.RS256},
	}), nil
}

func VerifyIDToken(
	ctx context.Context,
	verifier *oidc.IDTokenVerifier,
	rawIDToken string,
) (AppClaims, error) {
	token, err := verifier.Verify(ctx, rawIDToken)
	if err != nil {
		return AppClaims{}, fmt.Errorf("verify ID token: %w", err)
	}
	if token.Subject == "" || token.IssuedAt.IsZero() || token.IssuedAt.After(time.Now().Add(2*time.Minute)) {
		return AppClaims{}, fmt.Errorf("ID token 缺少有效的 sub 或 iat")
	}

	var claims AppClaims
	if err := token.Claims(&claims); err != nil {
		return AppClaims{}, fmt.Errorf("decode ID token claims: %w", err)
	}
	if claims.Tenant.UUID == "" {
		return AppClaims{}, fmt.Errorf("ID token 缺少租户身份")
	}
	claims.Subject = token.Subject
	return claims, nil
}

go-oidc 会通过 Discovery 获取 JWKS,缓存远程公钥,并在遇到未知 kid 时重新获取。Verify 会校验签名、允许算法、issueraudience 和过期时间;示例另外检查了必填的 subiat 和租户身份。

5. 建立接入应用业务会话

身份校验成功后:

  1. iss + sub 查找接入应用的账号绑定。
  2. 按企业规则决定必须预绑定,或首次登录自动建档。
  3. 处理租户映射、账号冲突和禁用状态。
  4. 创建接入应用自己的服务端会话。
  5. 通过 SecureHttpOnly 和合适 SameSite 的 Cookie 返回会话。

浏览器只持有接入应用自己的会话,不应接触 client_secret、ID Token、Access Token 或 Refresh Token。

6. 可选:调用知办AI API

应用 Access Token 当前只开放两类只读能力:

授权范围可调用接口不支持
kb.read(读取知识库)知识库列表、详情、文档读取和检索创建、上传、用户私有知识库
contacts.read(读取通讯录)部门、成员、职位 GET 接口新增、修改、删除

云盘接口尚未开放,管理端不会提供对应权限。应用令牌的租户身份由知办AI 校验后取得,不能通过请求参数切换租户。缺少所需权限返回 403,令牌无效或过期返回 401

只需要免登、不需要反向调用知办AI API 的应用,无需保存 Access Token 或 Refresh Token。完整接口字段见 OpenAPI 规范

7. 刷新令牌

需要长期调用知办AI API 时,后端可在 Access Token 到期前刷新:

http
POST https://<知办AI环境域名>/api/external/v1/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=rt_xxx&client_id=app_xxx&client_secret=sk_xxx

请求参数:

参数必填类型说明
grant_typestring固定为 refresh_token
refresh_tokenstring上一次换码或刷新得到的不透明刷新令牌。
client_idstring创建应用后取得的客户端编号。
client_secretstring接入应用后端安全保存的客户端密钥。

成功响应(200 OK):

json
{
  "access_token": "eyJ...new",
  "id_token": "eyJ...new",
  "refresh_token": "rt_xxx_new",
  "token_type": "Bearer",
  "expires_in": 7200,
  "scope": "kb.read contacts.read"
}

响应字段与 第 3 节一致,三个 Token 都是新值。Refresh Token 采用轮换机制:必须原子保存新值并立即淘汰旧值;新 ID Token 仍要完整验签。错误结构也与第 3 节一致;Refresh Token 无效、过期、已使用或不属于当前客户端时返回 401、业务码 4010,错误消息包含 invalid_grant

上线检查

  • 启动地址和回调地址均使用 HTTPS,回调地址与后台登记值完全一致。
  • client_secret 和所有 Token 只存在于后端安全配置或服务端存储。
  • 授权码和完整启动 URL 不进入日志、埋点、错误上报和浏览器历史。
  • ID Token 校验签名、算法、issaudexpiat;未知 kid 强制刷新 JWKS 一次。
  • 登录回调只完成登录,不自动执行审批、提交、支付等敏感业务操作。
  • 联调阶段显示脱敏后的租户、用户和数字员工信息,确认身份映射正确。
  • iss + sub 建立账号绑定,并校验租户归属。
  • 只申请业务实际需要的最小 scope。
  • 应用禁用、密钥重置和令牌过期均能回到安全的重新登录流程。

常见问题

现象排查方向
invalid_client检查客户端编号、客户端密钥,以及是否仍使用重置前的旧密钥。
invalid_grant检查授权码是否过期或已使用,以及 redirect_uri 是否完全一致。
ID Token 验签失败重新读取 Discovery/JWKS,检查 issueraudiencekid 和系统时间。
能登录但无法调用 API检查管理员授予的 scope;应用令牌不支持写接口。

租户管理员操作步骤见 应用栏配置

© 知办AI Team — 开发者公开文档. 平台运维 / 内部架构文档见内部 wiki.