本文档指导第三方应用系统通过 OAuth2.0 授权码模式接入零信任平台的 SSO 单点登录服务。
适用于所有需要接入 SSO 单点登录的 Web 应用(包括经过网关的应用和 SaaS 应用直连)。
OIDC协议与OAuth2.0的过程一致,只是调用的接口不同,参数也是一致的,接口参见:{SSO授权服务}/oidc/config
| 场景 | 描述 | 适用情况 |
|---|---|---|
| 场景一:经过网关 | 应用部署在网关后面,用户流量经过网关反向代理 | 内网应用、私有化部署 |
| 场景二:SaaS 直连 | 应用独立部署,用户浏览器直接访问应用域名 | SaaS 应用、公网独立部署 |
两种场景的 API 调用方式完全一致,区别仅在于网络链路是否经过网关。
阅读时请关注与你场景匹配的架构图和流程图即可。
| 角色 | 说明 | 示例 |
|---|---|---|
| 资源拥有者 | 终端用户(在 SSO 上拥有账号) | 员工张三 |
| 零信任网关(场景一) | 反向代理,用户流量经过网关 | gateway.example.com |
| SSO 授权服务 | OAuth2.0 授权服务器 | sso.example.com |
| 应用系统 | 需要接入 SSO 的第三方 Web 应用 | app.example.com |
┌──────────────────────────────────┐
│ 零信任网关 │
│ ┌────────────────────────────┐ │
┌──────────┐ HTTPS │ │ SSO 授权服务 │ │
│ 用户浏览器 │ ◄──────────────────► │ │ sso.example.com │ │
└──────────┘ │ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 应用域名反向代理 │ │
│ │ app.example.com ──► 10.x:8080 │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
│
│ 内网
▼
┌─────────────┐
│ 应用后端服务 │
│ 10.x.x.x:8080│
└─────────────┘用户的所有请求都经过零信任网关。网关将 app.example.com的流量代理到内网的应用后端。
┌──────────┐ HTTPS ┌─────────────────┐
│ 用户浏览器 │ ◄────────────► │ 应用系统 │
└──────────┘ │ app.example.com │
│ └───────┬─────────┘
│ │ HTTPS(后端服务器间通信)
│ HTTPS ▼
└──────────────────► ┌─────────────────┐
│ SSO 授权服务 │
│ sso.example.com │
└─────────────────┘SaaS 应用直接暴露在公 网,用户浏览器直接访问应用域名,不经过零信任网关。
应用后端直接通过 HTTPS 与 SSO 服务通信。
ztp.example.com)完成应用发布,并获取 OAuth2.0 的相关密钥凭证。完整流程如下:ztp.example.com。OAuth(不要选择 SAML、CAS 或 JWT)。code 的完整 URL(例如 https://app.example.com/oauth2/callback)。这个地址必须与代码中发起的 redirect_uri 完全一致。name、uid、email、mobile(对应对接中的 scope 数值)。注意:务必妥善保存 Client_key,离开此页面后可能无法再次查看明文。
| 配置项 | 管理界面字段名 | API 参数名 | 示例值 |
|---|---|---|---|
| 应用标识 | Client ID | client_id | Abc123DefGhI456 |
| 应用密钥 | Client_key | client_secret | xK9mN2pL5qR8sT1vW4yA7bC0dE3fH6j |
| 授权范围 | 授权返回字段 | scope | uid,name,email,mobile |
| 回调地址 | 应用回调地址 | redirect_uri | https://app.example.com/oauth2/callback |
⚠️ 重要: 管理界面上的字段名叫 Client_key,但在 API 调用时参数名必须写 client_secret。
这两个名称指向同一个值,切勿混淆。
两种场景的核心逻辑相同: 浏览器 302 → SSO 认证 → code 回调 → 后端换 token → 获取用户信息。
区别仅在于场景一的每一步都经过网关代理,场景二则由浏览器直接与应用/SSO 通信。
| 步骤 | 发起方 | 说明 |
|---|---|---|
| ① | 用户 | 用户通过浏览器访问应用域名 app.example.com |
| ②③④ | 应用 | 应用检测到用户未登录,302 重定向浏览器到 SSO 授权页 |
| ⑤~⑧ | 用户/SSO | 用户在 SSO 页面上完成身份认证(输入账号密码等) |
| ⑨~⑫ | SSO | 认证通过后,SSO 生成授权码 code,302 重定向回应用的回调地址 |
| ⑬⑭ | 网关/应用 | 携带 code 的回调到达应用后端(场景一经网关代理,场景二直达) |
| ⑮ | 应用后端 | 后端用 code + client_id + client_secret 直接调用 SSO 接口换取 access_token |
| ⑯ | 应用后端 | 后端用 access_token 直接调用 SSO 接口获取用户信息 |
| ⑰⑱ | 应用 | 应用根据用户信息完成本地登录(创建会话、设置 Cookie 等) |
核心概念: 步骤 ⑮⑯ 是服务器间通信——应用后端直接请求 SSO 服务的 HTTPS 接口, client_secret不会暴露给浏览器,保证了密钥安全。
GET(浏览器地址栏跳转)GET {SSO地址}/oauth2.0
?response_type=code
&client_id={client_id}
&redirect_uri={回调地址,需 URL 编码}
&scope={授权范围}
&state={自定义状态值}| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
response_type | ✅ | string | 固定值 code |
client_id | ✅ | string | 在管理后台获取的应用标识 |
redirect_uri | ✅ | string | 回调地址,必须做 URL 编码,且必须与管理后台配置 完全一致 |
scope | ✅ | string | 授权范围,多个字段用 逗号 分隔:uid,name,email,mobile |
state | 建议 | string | 随机字符串,用于 CSRF 防护。SSO 会在回调时原样返回此值 |
https://sso.example.com/oauth2.0
?response_type=code
&client_id=Abc123DefGhI456
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth2%2Fcallback
&scope=uid,name,email,mobile
&state=a1b2c3d4e5f6https://app.example.com/oauth2/callback?code=AUTHORIZATION_CODE_HERE&state=a1b2c3d4e5f6GET⚠️ 关键注意:此接口仅支持 GET 方式。 使用 POST(无论 form 还是 JSON)都会导致认证失败或返回 HTML 页面。
GET {SSO地址}/oauth2.0/token
?client_id={client_id}
&client_secret={client_secret}
&grant_type=authorization_code
&scope={scope}
&redirect_uri={redirect_uri,需 URL 编码}
&code={上一步获取的授权码}| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
client_id | ✅ | string | 应用标识 |
client_secret | ✅ | string | 应用密钥(管理界面上叫 Client_key,参数名必须写 client_secret) |
grant_type | ✅ | string | 固定值 authorization_code |
scope | ✅ | string | 与 Step 1 中一致 |
redirect_uri | ✅ | string | 与 Step 1 中一致,需 URL 编码 |
code | ✅ | string | Step 1 回调中获取的授权码,一次性使用 |
{
"access_token": "MZVLNTG4MWITMJGXNY0ZMZRMLTG1ODETYZFKMDFMMWM5NGEZ",
"expires_in": 7200,
"refresh_token": "NDQ5ZMI0ZJMTMWU2YI01ZGQ2LTHKNGETNJCYMMM3Y2I5NDY2",
"scope": "name,uid,email,mobile",
"token_type": "Bearer"
}| 字段 | 类型 | 说明 |
|---|---|---|
access_token | string | 访问令牌,用于下一步获取用户信息 |
expires_in | int | 有效期(秒),通常 7200(2 小时) |
refresh_token | string | 刷新令牌,用于续期(可选实现) |
scope | string | 实际授权的范围 |
token_type | string | 固定值 Bearer |
{
"error": "invalid_grant",
"error_description": "The provided authorization grant is invalid, expired, revoked..."
}| 错误码 | 含义 | 排查方法 |
|---|---|---|
invalid_client | Client 认证失败 | ① 检查 client_id/client_secret 是否正确 ② 确认使用了 GET 方式(非 POST) ③ 确认参数名是 client_secret(非 client_key) |
invalid_grant | 授权码无效或已过期 | ① code 是一次性的,不能重复使用 ② code 有效期很短,需尽快使用 ③ redirect_uri 必须与获取 code 时完全一致 |
| 返回 HTML 页面 | 请求格式不对 | ① 确认使用 GET 方式 ② 确认参数名 client_secret(非 client_key) |
GETGET {SSO地址}/oauth2.0/res?access_token={access_token}| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
access_token | ✅ | string | 上一步获取的访问令牌,通过 URL 查询参数 传递 |
⚠️ 注意: access_token必须通过 URL 查询参数传递,不支持Authorization: Bearer <token>请求头方式。
虽然原始文档中提到了 Bearer 头方式,但实际测试中 仅 URL 查询参数 能正确返回用户信息。
{
"code": 10000,
"msg": "成功",
"data": {
"uid": "zhangsan",
"name": "张三",
"email": "zhangsan@example.com",
"mobile": "+8613800138000"
},
"traceId": "0b4d7582-26eb-4edf-a192-1a247ee0a78e"
}| 字段 | 类型 | 说明 |
|---|---|---|
code | int | 状态码,10000 表示成功 |
msg | string | 状态信息 |
data.uid | string | 用户唯一标识(登录账号名) |
data.name | string | 用户真实姓名 |
data.email | string | 用户邮箱地址 |
data.mobile | string | 用户手机号 |
traceId | string | 请求追踪 ID,用于问题排查 |
返回的字段取决于 Step 1 中 scope参数指定的范围。未授权的字段不会出现在响应中。
uid 在本地数据库中查找对应用户📌 代码说明 以下代码示例完整展示了 OAuth2.0 对接的核心逻辑,可直接用于开发参考。 Go 示例:完整可运行程序,直接 go run main.go启动Java 示例:展示 Spring Boot Controller 核心代码,需在已有 Spring Boot 项目中使用,补充对应依赖 Python 示例:完整可运行程序, pip install flask requests后直接启动所有示例中的 sso.example.com、client_id、client_secret等配置需替换为实际值。
内网环境使用自签证书时,示例已包含跳过 TLS 校验的配置。
application.yaml:⛔ 安全风险警告 启用逃生机制(降级到本地密码登录)意味着绕过了 SSO 的统一认证和多因素验证(MFA)。
这可能带来以下安全风险:本地账号密码不受零信任策略保护(无 MFA、无设备检查、无地理围栏) 如果本地密码泄露,攻击者可直接登录,不会被 SSO 侧的异常检测拦截 审计日志中将缺少 SSO 登录事件,可能影响安全审计的完整性 使用建议: 1.逃生机制仅限 SSO 完全不可用时的应急使用,恢复后应立即关闭 2.本地管理员账号应设置高强度密码并定期轮换 3.建议记录所有本地登录事件并设置告警,确保逃生期间的操作可审计
用户访问 → 检查 SSO 状态 → SSO 可用?
├── 是 → 正常 SSO 登录
└── 否 → 降级到本地账号密码登录integration_configs.enabled = 0)最佳实践: 无论是否启用了 SSO,始终保留至少一个 本地管理员账号(有密码),
确保在 SSO 完全不可用时,仍有人可以登录系统进行故障处理。
client_id 和 client_secret 已在管理后台获取redirect_uri 与管理后台"应用回调地址"完全一致(包含协议、域名、路径)scope 已正确设置(uid,name,email,mobile,逗号分隔)client_secret(非 client_key)access_tokenclient_secret 仅在后端使用,不暴露给前端/浏览器state 参数已实现 CSRF 防护(存入 Cookie/Session,回调时校验)code 仅使用一次,使用后立即废弃POST 方式,或参数名写成了 client_key。Token 接口只接受 GET 请求,参数名必须是 client_secret。invalid_client?client_id 和 client_secret 值正确GET 方式(POST 也会导致此错误)client_secretinvalid_grant?code 已失效。code 是一次性的,获取后需尽快使用(有效期较短),且不能重复使用。此外 redirect_uri 必须与获取 code 时完全一致。http.Client 默认自动跟随 302 重定向。SSO 服务可能返回 302 到登录页面,Go 自动跟随后获取到了 HTML。解决方法:scope 参数中多个字段用什么分隔?uid,name,email,mobile。不是空格。sso_uid 字段,用于存储 SSO 返回的 uid。匹配策略:sso_uid 精确匹配username 匹配(如 MySQL utf8mb4_general_ci 不区分大小写)uid 写入 sso_uid 字段,实现一次绑定