Appearance
单点登录接入
本指南面向接入知办AI 应用栏的 H5 应用开发者。用户从知办AI 打开应用后,接入应用的后端使用一次性授权码换取令牌,校验 ID Token 并读取用户与租户身份,再建立应用自己的登录会话。
整体流程
接入应用开发者需要调用的知办AI 接口只有以下几类。一次性登录链接由知办AI 客户端获取,接入应用无需调用应用启动接口。
| 接口 | 用途 | 请求/响应说明 |
|---|---|---|
POST /api/external/v1/oauth/token(authorization_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/token(refresh_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_type | 是 | string | 固定为 authorization_code。 |
code | 是 | string | 启动地址中的一次性授权码,短时有效且只能使用一次。 |
client_id | 是 | string | 创建应用后取得的客户端编号。 |
client_secret | 是 | string | 创建或重置密钥时取得,仅允许由接入应用后端读取。 |
redirect_uri | 是 | string(URI) | 必须与管理后台登记值完全一致,包括协议、域名、端口、路径和末尾斜杠。 |
成功响应:
json
{
"access_token": "eyJ...",
"id_token": "eyJ...",
"refresh_token": "rt_xxx",
"token_type": "Bearer",
"expires_in": 7200,
"scope": "kb.read contacts.read"
}成功响应字段:
| 字段 | 类型 | 一定返回 | 说明 |
|---|---|---|---|
access_token | string | 是 | RS256 签名的访问令牌,用于调用已授权的知办AI API。 |
id_token | string | 是 | RS256 签名的身份令牌,校验通过后读取用户、租户和成员信息。 |
refresh_token | string | 是 | 不透明刷新令牌;刷新时轮换,旧值立即失效。 |
token_type | string | 是 | 固定为 Bearer。 |
expires_in | integer | 是 | Access Token 和 ID Token 的有效期,单位为秒。 |
scope | string | 是 | 空格分隔的授权范围;只做免登时可以为空字符串。 |
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 | 典型原因 | 处理方式 |
|---|---|---|---|
400 | 4000 | 请求体无效、Content-Type 不支持、缺少字段或 grant_type 不支持 | 修正请求,不要原样重试。 |
401 | 4010 | 客户端凭证无效、应用已停用,或授权码无效/过期/已消费/回调地址不匹配 | 凭证问题检查配置;授权码问题重新启动应用。 |
429 | 4290 | 同一 client_id 请求过于频繁 | 退避后重试,不要并发撞库。 |
500 | 5000 | 服务暂时异常 | 保留脱敏后的请求时间和错误信息,联系知办AI 技术支持。 |
4. 校验 ID Token 并读取登录身份
4.1 读取发现配置
接入应用后端从知办AI 发现地址取得签发方、Token 接口和验签公钥地址。该接口无需鉴权,也没有请求参数:
http
GET https://<知办AI环境域名>/.well-known/openid-configuration成功响应(200 OK,Cache-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"
]
}响应字段:
| 字段 | 类型 | 说明 |
|---|---|---|
issuer | string(URI) | ID Token 的预期签发方,必须与 iss 完全一致。 |
authorization_endpoint | string(URI 模板) | 知办AI 客户端使用的应用启动地址;接入应用无需调用。 |
token_endpoint | string(URI) | 接入应用后端交换或刷新令牌的接口。 |
jwks_uri | string(URI) | 获取 ID Token 验签公钥集合的接口。 |
response_types_supported | string[] | 支持的响应类型,当前包含 code。 |
grant_types_supported | string[] | 环境支持的授权类型;本指南只使用 authorization_code 和 refresh_token。 |
subject_types_supported | string[] | ID Token 主体标识类型,当前为 public。 |
id_token_signing_alg_values_supported | string[] | ID Token 签名算法,当前只接受 RS256。 |
token_endpoint_auth_methods_supported | string[] | Token 接口支持的客户端认证方式。本文示例使用请求体传递凭证。 |
scopes_supported | string[] | 环境可用的 scope;应用实际权限以管理员授权和 Token 的 scope 为准。 |
claims_supported | string[] | ID Token 可能提供的 Claim 名称;可选资料不保证每个用户都有值。 |
4.2 获取 JWKS 公钥
使用发现响应中的 jwks_uri 请求公钥集合。该接口无需鉴权,也没有请求参数:
http
GET https://<知办AI环境域名>/.well-known/jwks.json成功响应(200 OK,Cache-Control: public, max-age=3600):
json
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "k-active-xxx",
"n": "<RSA modulus,Base64URL 编码>",
"e": "AQAB"
}
]
}| 字段 | 类型 | 说明 |
|---|---|---|
keys | object[] | 当前可用于验签的 active/retiring 公钥列表,顺序不代表优先级。 |
keys[].kty | string | 密钥类型,当前为 RSA。 |
keys[].use | string | 密钥用途,当前为签名验证 sig。 |
keys[].alg | string | 适用算法,当前为 RS256。 |
keys[].kid | string | 密钥编号,与 ID Token Header 中的 kid 匹配。 |
keys[].n | string | RSA 模数,Base64URL 编码。 |
keys[].e | string | RSA 公钥指数,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 库完成以下校验:
- 按 JWT Header 的
kid从 JWKS 选择公钥,只接受RS256。 - 校验签名,并确认
iss与 Discovery 的issuer完全一致。 - 确认
aud等于本应用的client_id。 - 校验
exp和iat。
只有全部校验通过后才能读取 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 | 类型 | 一定返回 | 说明 |
|---|---|---|---|
iss | string(URI) | 是 | 签发方,必须等于发现响应的 issuer。 |
aud | string | 是 | 令牌受众,必须等于本应用 client_id。 |
sub | string | 是 | 知办AI 用户稳定标识;与 iss 组合为外部身份键。 |
iat | integer | 是 | 签发时间,Unix 秒。 |
exp | integer | 是 | 过期时间,Unix 秒。 |
nonce | string | 否 | 本次启动的关联值,不作为用户或租户身份字段。 |
name | string | 否 | 用户显示名称,可变资料,不作为账号唯一键。 |
email | string | 否 | 用户邮箱,可变资料。 |
email_verified | boolean | 否 | 邮箱是否已在知办AI 完成验证;缺失时不得按 true 处理。 |
phone_number | string | 否 | 用户手机号,可变资料。 |
tenant.uuid | string | 条件返回 | 当前租户稳定标识;存在 tenant 时返回。 |
tenant.name | string | 条件返回 | 当前租户名称,可变资料。 |
membership.employee_no | string | 否 | 当前租户内工号。 |
membership.title | string | 否 | 当前租户内职位。 |
membership.department | string | 否 | 当前租户内部门路径。 |
zhiban_context | object | 否 | 本次启动的知办AI 会话上下文;从工作台等无会话入口打开时可缺失。 |
zhiban_context.conversation_id | string | 条件返回 | 知办AI 会话稳定标识。 |
zhiban_context.conversation_kind | string | 条件返回 | 会话类型,如数字员工会话、单聊或群聊。 |
zhiban_context.peer.kind | string | 条件返回 | 会话对象类型:数字员工、用户或群聊。 |
zhiban_context.peer.uuid | string | 条件返回 | 会话对象稳定标识。 |
zhiban_context.peer.name | string | 条件返回 | 会话对象显示名称。 |
使用 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/oidcissuer 必须来自服务端固定配置,不能使用启动 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 会校验签名、允许算法、issuer、audience 和过期时间;示例另外检查了必填的 sub、iat 和租户身份。
5. 建立接入应用业务会话
身份校验成功后:
- 用
iss + sub查找接入应用的账号绑定。 - 按企业规则决定必须预绑定,或首次登录自动建档。
- 处理租户映射、账号冲突和禁用状态。
- 创建接入应用自己的服务端会话。
- 通过
Secure、HttpOnly和合适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_type | 是 | string | 固定为 refresh_token。 |
refresh_token | 是 | string | 上一次换码或刷新得到的不透明刷新令牌。 |
client_id | 是 | string | 创建应用后取得的客户端编号。 |
client_secret | 是 | string | 接入应用后端安全保存的客户端密钥。 |
成功响应(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 校验签名、算法、
iss、aud、exp和iat;未知kid强制刷新 JWKS 一次。 - 登录回调只完成登录,不自动执行审批、提交、支付等敏感业务操作。
- 联调阶段显示脱敏后的租户、用户和数字员工信息,确认身份映射正确。
- 以
iss + sub建立账号绑定,并校验租户归属。 - 只申请业务实际需要的最小 scope。
- 应用禁用、密钥重置和令牌过期均能回到安全的重新登录流程。
常见问题
| 现象 | 排查方向 |
|---|---|
invalid_client | 检查客户端编号、客户端密钥,以及是否仍使用重置前的旧密钥。 |
invalid_grant | 检查授权码是否过期或已使用,以及 redirect_uri 是否完全一致。 |
| ID Token 验签失败 | 重新读取 Discovery/JWKS,检查 issuer、audience、kid 和系统时间。 |
| 能登录但无法调用 API | 检查管理员授予的 scope;应用令牌不支持写接口。 |
租户管理员操作步骤见 应用栏配置。