接入诊断与故障协作
诊断分为项目视图、平台视图和可选的应用上报。系统记录观察到的事实,不把 HTTP 状态码、没有后续请求或应用自报直接当作责任归属。诊断采集或告警不可用不会阻止正常登录。
项目与平台能看到什么
项目控制台的“接入诊断”按 Organization → Project → Application 限定访问。具备项目查看权限的成员及组织角色可查询本项目最近 30 天的流程。筛选、统计、详情与 CSV/JSONL 导出使用相同的服务端授权和字段投影;知道 requestId 或 flowId 不会获得查看权限。平台管理员的诊断视图保留最近 90 天的安全运行信息。
项目侧可见应用、时间、阶段、请求编号、粗粒度结果、可执行的错误提示以及与本流程匹配的平台故障处理状态。用户成功同意授权后才会出现项目内用户代号;同一用户跨项目得到不同代号。代号仍可用于同项目内关联活动,不属于不可追踪的完全匿名数据,也没有邮箱反查入口。项目侧不显示姓名、邮箱、手机号、平台用户 UUID、完整 IP、User-Agent、账号是否存在、使用的登录方式、安全因子或其他应用活动。登录与额外验证的内部细节在项目视图统一为身份认证阶段。
平台管理员可见受控内部原因、依赖、发布版本和具体诊断阶段,但密码、验证码、授权码、access/refresh/ID token、Cookie、PKCE verifier、完整 OAuth 查询及上游用户资料均不进入诊断记录。平台故障的内部备注不会出现在项目视图、公共错误或 Lark 卡片里。读取、导出和故障处理修改都会留下管理审计记录。
| 分类 | 表示什么 | 不代表什么 |
|---|---|---|
| 开发者请求/配置 | Auth 校验到 scope、回调地址、客户端或协议请求不满足契约 | 公开 client_id 被使用不证明请求一定来自开发者服务器 |
| 平台 | Auth 内部数据库、执行或令牌签发等已观察到的技术异常 | 不推断第三方应用内部情况 |
| 依赖 | Auth 与身份提供方或其他依赖的交互失败 | 连接超时不直接证明依赖厂商自身故障 |
| 用户/业务规则 | 取消授权、身份验证未完成或已知账号规则拒绝 | 不属于服务故障,也不把密码输错逐条发群 |
| 未确定 | 目前没有足够证据说明原因 | 不默认归责给开发者 |
“授权码已签发”只表示 Auth 已产生回调;不能证明用户浏览器成功收到回调。“令牌已签发”只表示 Auth 完成令牌请求;不能证明应用已创建本地会话。没有后续请求显示为“未观察到”,不是“应用服务器出错”。应用主动提交的记录始终标记为“应用自报”,不计入 Auth 技术失败告警。
流程的 completion 与单个步骤的 outcome 分开:授权回调发出但没有观察到令牌兑换显示“尚未观察到令牌兑换”;令牌兑换成功但没有应用会话结果显示“应用会话结果未知”。“结果待确认”统计包括这些未完成观测的流程。普通个人中心登录和刷新在 Auth 完成其负责的处理后可以完成,不要求不存在的应用会话上报。应用报告会话创建成功始终标明“应用自行上报”,不能作为 Auth 独立证明的端到端成功。
请求编号与流程编号
requestId 是单个服务端请求的编号,flowId 关联一次已验证的 OAuth 授权流程。浏览器的多标签和不同授权请求不会仅因用户相同而合并。项目归属来自服务端验证的应用登记与绑定,不信任调用方自报的项目 ID 或关联请求头。授权码兑换与刷新由服务端 grant 关系关联,不记录凭据原文。每次 refresh 使用新的独立 flow;平台内部可通过 grantReference 关联同一授权记录,项目视图不提供该字段。它是已有授权记录标识的摘要,不是 token,也不是可兑换的授权码。
登录页遇到 Lark 等外部身份提供方错误时,使用对应的稳定错误码和本地化操作说明;HTTP 302 跳转成功与业务认证失败分别记录。账号关联冲突不再被等同于“获取资料失败”。
可选:补齐应用回调后的观测
普通 OAuth 接入不需要改变。需要查看应用内部阶段时,在原有 authorize 请求增加 kai_diagnostics=1。Auth 只向已验证登记的回调地址附加 kai_flow_id 和 kai_diagnostic_token,原有 state、授权码和错误语义保持不变。
诊断 token 是仅能向单次流程追加事件的短期凭据,15 分钟有效、最多 12 个唯一事件,不能获取账号、会话或令牌。仅保存在应用回调处理期间的内存中,不放入日志、分析系统、localStorage 或持久 Cookie。网页应用应立即从地址栏移除两个诊断参数;服务端应避免访问日志收集回调查询。不能把 kai_flow_id、诊断 token 或诊断成功当成 OAuth 登录的真实性证明;仍必须正常验证 state、PKCE、nonce、issuer、audience 和令牌签名。
POST /api/auth/diagnostics/report 使用 Authorization: Bearer <kai_diagnostic_token>。正文只接受 eventId、code、stage、outcome 及可选 durationMs、requestId,不支持任意日志文本、邮箱或 URL。同一 eventId 和同一正文重试幂等;相同 ID 配不同内容会拒绝。客户端不能自行指定项目或责任分类。
| code | stage | outcome |
|---|---|---|
CALLBACK_RECEIVED |
callback_received |
success |
CALLBACK_REJECTED / STATE_MISMATCH |
callback_received |
failure |
TOKEN_RECEIVED / TOKEN_EXCHANGE_FAILED |
token_exchange |
success / failure |
TOKEN_VALIDATED / TOKEN_VALIDATION_FAILED |
id_token_validation |
success / failure |
SESSION_CREATED / SESSION_CREATION_FAILED |
session_establishment |
success / failure |
CLIENT_CANCELLED |
session_establishment |
cancelled |
CLIENT_UNKNOWN_FAILURE |
session_establishment |
failure |
下例使用仓库 examples/auth-diagnostics/report.mjs 的完整辅助函数。finishExistingOAuthLogin 是应用现有的登录完成入口,必须验证 state、PKCE、nonce、issuer、audience、签名并成功创建应用会话。诊断参数在交给该入口之前从地址栏移除,标准 OAuth 参数保持不变;服务端回调同样不要把查询串写入访问日志。
import { takeDiagnosticCallback, createDiagnosticReporter } from "./report.mjs"
const callback = takeDiagnosticCallback(window.location.href)
window.history.replaceState(null, "", callback.cleanUrl)
let reporter = null
try {
reporter = createDiagnosticReporter({
authOrigin: "https://auth.kai.com", // 来自应用配置,不能读取回调参数
token: callback.token,
})
} catch {
// 可选诊断配置错误不阻断正常 OAuth;不记录回调或凭据。
} finally {
callback.token = null
}
const session = await finishExistingOAuthLogin(callback.cleanUrl).catch(
error => {
reporter?.dispose()
throw error // 保留原本的 OAuth/业务会话失败。
},
)
if (reporter) {
const reporting = reporter
reporter = null
void reporting.report("SESSION_CREATED").finally(() => reporting.dispose())
}
辅助函数在内存中保留诊断凭据,15 分钟后自动清除;登录失败立即释放,登录成功的可选上报在后台完成后释放。上报不延迟 session 的使用,不影响已有 OAuth 的校验或错误处理。
使用应用配置的 Auth 地址,不能从回调参数决定投递地址。该上报端点允许不携带 Cookie 的跨域请求;Bearer 凭据只可追加本次流程的有限事件,不提供诊断读取权限。429、网络失败或服务停机时不循环重试,不延长登录耗时。返回成功仅表示 Auth 接受了一条“应用自报”,不证明应用正确完成认证。
服务端和原生应用使用相同 HTTP 契约。requestId 和 eventId 必须是 UUID;durationMs 为 0–900000 的整数。不要上传 stack、姓名、邮箱、URL 或自由文本。仓库中的 examples/auth-diagnostics/report.mjs 提供无依赖完整辅助函数,支持最多 12 次上报、2 秒超时、凭据过期和主动清理;对应测试使用 node --test examples/auth-diagnostics/report.test.mjs。
诊断错误可以按下表处理,具体业务 OAuth 错误仍按错误码文档处理:
| 错误码 | 处理 |
|---|---|
DIAGNOSTIC_REPORT_TOKEN_INVALID |
诊断凭据无效或到期,停止本次上报,不重新登录用户 |
DIAGNOSTIC_REPORT_EVIDENCE_INVALID |
code、stage、outcome 不匹配,修正应用映射 |
DIAGNOSTIC_REPORT_IDEMPOTENCY_CONFLICT |
同一 eventId 被用于不同内容,修正幂等实现 |
DIAGNOSTIC_REPORT_LIMIT_REACHED |
已达到单流程 12 个唯一事件的限额,停止上报 |
DIAGNOSTIC_REPORT_RATE_LIMITED |
按 Retry-After 等待;不要自动重走 OAuth |
DIAGNOSTIC_REPORT_TOO_LARGE |
正文超过 4096 字节,移除契约外数据 |
Web、iOS 与 CLI 接入示例
三个可运行示例位于仓库 examples/auth-diagnostics/。它们只补充应用上报,现有 OAuth/OIDC 库仍负责所有认证验证、业务会话创建与凭据保存;不要把示例中的诊断 token 当成 OAuth token。
- Web:使用
report.mjs的takeDiagnosticCallback和createDiagnosticReporter。回调加载后立即history.replaceState移除诊断参数,保留标准 code/state 供 OIDC 库处理。报告 token 仅保存在内存,15 分钟自动清除,完成后调用dispose()。 - iOS:
ios.swift提供可编译的completeIOSOAuthCallback、Actor reporter 和URLSession实现。把ASWebAuthenticationSession返回的 URL 交给它;回调诊断参数会在交给现有 OIDC SDK 前移除。通过authenticate闭包调用应用现有的授权码兑换、令牌验证和会话保存,只有闭包成功返回才报告SESSION_CREATED。临时 URLSession 不共享 Cookie、URL 缓存或 HTTP 凭据,拒绝重定向,每次请求最多 2 秒,内存 token 15 分钟自动清除。报告在独立 Task 中执行,不延迟登录结果。 - Node.js CLI:
cli.mjs的completeCliOAuthCallback用于现有 loopback HTTP 回调处理器,不要把完整回调 URL 或诊断 token 放在命令行参数、终端历史或配置文件。它在交给现有 OIDC 实现前移除诊断参数;上报通过fetch、不使用 Cookie、禁止重定向,完成或失败后释放内存 token。
CLI 调用形态如下,finishExistingOAuthLogin 是当前 CLI 已有的、完整校验并保存应用会话的实现:
import { completeCliOAuthCallback } from "./cli.mjs"
const session = await completeCliOAuthCallback(callbackUrl, {
authOrigin: configuredAuthOrigin,
authenticate: finishExistingOAuthLogin,
})
iOS 对应调用形态如下,existingOIDCFlow.finish 是当前 App 已有的 OIDC 完成入口;SDK 和业务会话失败仍按原行为向调用方返回,诊断失败不会覆盖它:
let session = try await completeIOSOAuthCallback(
callbackURL,
authOrigin: configuredAuthOrigin
) { cleanURL in
try await existingOIDCFlow.finish(callbackURL: cleanURL)
}
这些入口不会根据任意异常消息猜测具体失败原因,未知应用异常只报告 CLIENT_UNKNOWN_FAILURE。若要区分 token 兑换和 ID token 校验失败,请在相应的真实处理步骤调用 typed DiagnosticEvidence 或 JS 的闭合集合 code,不上传异常文本。
示例的本地验证命令(不访问真实 Auth 或 Lark)为:
node --test examples/auth-diagnostics/report.test.mjs examples/auth-diagnostics/cli.test.mjs
swiftc -swift-version 6 -parse-as-library examples/auth-diagnostics/ios.swift examples/auth-diagnostics/ios-test.swift -o /tmp/kai-diagnostics-ios-example-test
/tmp/kai-diagnostics-ios-example-test
Swift 示例使用 ContinuousClock 和 async URLSession,要求 iOS 16 / macOS 13 或更新系统。它不修改 App 的原有登录协议,也不注册新的回调方案。
聚合告警与恢复
后台 worker 读取已落库的结构化记录。可信技术告警只使用 Auth 自己观察到的事件;项目级统计需要受信应用归属,未归属的平台异常仅用于全局运行诊断。
| 规则 | 触发条件 |
|---|---|
| 相同平台/依赖异常 | 5 分钟内至少 5 个不同 flow |
| 技术失败率异常 | 5 分钟内至少 50 个已完成观测 flow、至少 5 个技术失败 flow,失败率达到 10% |
| 未知异常 | 新指纹首次出现即记录,30 分钟内合并 |
| 业务体验异常 | 10 分钟内特定账号规则拒绝达到 20 个 flow 且占该阶段观测 flow 至少 30%;低优先级,不包括普通密码/验证码输入错误 |
| 恢复 | 连续两个互不重叠的 5 分钟窗口没有同类失败,且两个窗口都有真实成功流量 |
技术失败率仅计算最新平台事件体现的终态:已验证应用的 token/refresh 成功,未关联 OAuth 应用的直接登录已完成会话建立,或上述流程的明确失败。OAuth 流程的 authorize/authentication 中间成功、发送验证码和等待验证器批准都不扩大分母;用户取消和预期业务拒绝不计技术失败分子。恢复窗口采用同样的完成口径,没有流量不会自动宣布恢复;错过的恢复窗口不会补算为成功。
上述统计仅采用 Auth 自身的事件,包含已验证应用的流程及未关联应用的直接登录。服务端已确认的 Lark 身份冲突等也适用体验异常阈值;客户端上报和未经验证的应用归属不进入告警聚合。普通密码、验证码错误和明确限流保留在诊断中,不触发未知异常首报。
指纹由错误码、阶段、依赖、环境组成,项目作为受影响范围而不是每项目重复通知。同一事件的首次通知、每 30 分钟的持续摘要和恢复通知分别去重。故障支持“调查中、已确认、观察恢复、已解决”、指派平台管理员和内部备注;手动标记已解决只是人工处理状态,不伪造“自动验证已恢复”的通知。新失败仍可创建新故障。
管理员告警链接使用 ?incident=<故障编号> 定位具体事件,保留期内的旧故障不受列表首页数量限制。故障列表按最近发生时间分页,支持按处理状态筛选和继续加载;查询与定位仍要求当前平台管理员权限,平台故障查询同样限制在最近 90 天。
Lark 卡片只包含固定模板、安全错误码、汇总流程数、时间、发布版本、示例 flowId 及需登录的平台控制台链接。不包含用户身份、登录因子、项目内部详情、Webhook 或自由文本内部备注。测试投递仅发送固定测试卡,页面先显示“已排队”;可查询投递状态,不能把入队当作已送达。
配置、观察期与发布
采集、项目展示、应用上报分别由 DIAGNOSTICS_CAPTURE_ENABLED、DIAGNOSTICS_PROJECT_ENABLED、DIAGNOSTICS_REPORT_ENABLED 控制,默认关闭。项目内用户代号使用独立的 DIAGNOSTICS_PSEUDONYM_SECRET 与 DIAGNOSTICS_PSEUDONYM_KEY_VERSION;不要复用 OAuth、会话、验证码或其他日志系统的密钥。账号删除时清除关联诊断的用户引用、用户代号与 grantReference,包括尚未形成授权同意或用户代号的流程。延迟采集不会重新建立已删除账号的关联。
采集队列有固定容量与写入时限。队列溢出、写入超时或进程重启可能造成观测缺口;平台可查看采集状态、待写入数量与丢失计数,数据不足时不把它当作完整可用率。报告凭据与安全开关不会改变 OAuth 登录权限。
DIAGNOSTICS_ALERTS_ENABLED 控制后台聚合,DIAGNOSTICS_ALERTS_SEND_ENABLED 控制自动对外发送;二者默认关闭。Webhook 通过服务端 DIAGNOSTICS_LARK_WEBHOOK 注入,可选签名密钥通过 DIAGNOSTICS_LARK_SIGNING_SECRET 注入,控制台不接收、不显示密钥。
只允许国际版 Lark 官方 https://open.larksuite.com/open-apis/bot/v2/hook/... 地址,不接受任意 URL、凭据、查询参数或重定向。签名遵循 Lark 自定义机器人官方说明。投递设置不复用面向用户的邮箱/短信渠道。
首次启用聚合后有至少 24 小时观察期,起始时间持久化,部署和重启不会重新计时。观察期只记录模拟通知;期满仍需要显式打开发送开关。观察期积累的旧通知不会批量补发。平台管理员可以单独请求固定安全测试卡验证通道,不改变正式发送开关。
队列在事务中认领,有 30 秒租约;每次网络请求限制 5 秒、禁止跟随重定向并限制响应体大小。失败采用 30 秒、2 分钟、5 分钟、15 分钟退避,最多 5 次,之后进入失败队列;全局每分钟最多 5 次、每小时最多 30 次投递尝试。超时后的远端结果可能不确定,Lark Webhook 没有业务幂等承诺,因此极少量重复卡片仍可能出现,卡片中的故障编号/详情链接可识别同一事件。
告警自身的数据库或投递失败显示在 worker/队列状态中,不再递归产生群告警。服务或数据库整体不可用时,进程内 worker 无法独立证明状态,需要部署方已有的外部健康检查补充;“无日志”不能当作“服务正常”。
推荐启用顺序:应用兼容数据库迁移 → 启用采集 → 验证项目隔离与错误提示 → 启用聚合观察 24 小时 → 测试安全卡 → 核对阈值和噪声 → 显式开启自动发送。关闭发送开关立即停止自动投递,保留诊断和处理状态供排查;回滚应用不删除新增诊断表。