Developer Guide
SSO API 接口文档
本页从统一 PHP 路由元数据生成全部 34 个对外 SSO 接口的请求契约,覆盖 OAuth2、REST 认证、扫码登录和组织数据。
多步骤的 OAuth2 与钉钉原生二维码流程在接口参考之后单独说明。所有地址均基于 https://sso.xahrf.cn。
Base URL: https://sso.xahrf.cn
接口数量: 34
Token 有效期: 3600 秒
授权码有效期: 600 秒
扫码挑战有效期: 120 秒
接口总览
下表列出当前系统全部对外 API。后台管理表单、钉钉回调页面和二维码确认页面不计入通用 API 数量。
格式约定:
/api/** 成功响应包含 ok=true,失败响应包含 ok=false、error 和 message。
OAuth2 端点遵循标准 OAuth 响应格式,不增加 ok 字段。
接入准备
- 注册应用:管理员在应用管理中创建 OAuth2 应用,保存系统生成的
client_id 和 client_secret。
- 登记回调:Web SSO 必须配置精确的 HTTPS
redirect_uri 白名单,协议、域名、端口和路径均参与匹配。
- 分配权限:需要限制访问时,将应用分配给允许登录的角色;未授权用户会收到
access_denied。
- 保护凭证:
client_secret、poll_key 和 Access Token 不应出现在公开前端代码或日志中。
OAuth2 授权码流程
面向浏览器 Web SSO 的标准授权码流程。OAuth 响应遵循标准格式,不包含 REST API 的 ok 字段。
GET
POST
/oauth/authorize.php
校验应用与回调地址,要求用户登录并确认授权,最后将一次性 code 回传业务系统。
| 认证方式 | client_id + SSO Session |
| 请求格式 | Query;授权确认使用 Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
response_type |
Query |
string |
是 |
固定为 code。 |
client_id |
Query |
string |
是 |
管理后台注册应用后获得的 Client ID。 |
redirect_uri |
Query |
URL |
是 |
必须与应用登记的回调白名单完全一致。 |
state |
Query |
string |
推荐 |
业务系统生成并在回调时校验的 CSRF 随机值。 |
scope |
Query |
string |
否 |
空格分隔的权限范围,例如 openid profile。 |
action |
Form |
string |
POST 时 |
用户确认授权时固定为 authorize。 |
请求示例
GET https://sso.xahrf.cn/oauth/authorize.php?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&state=RANDOM_STATE&scope=openid%20profile
成功响应
HTTP 302:浏览器跳转至登记的 redirect_uri。
https://app.example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE
错误响应
| error / 响应 | HTTP | 说明 |
Invalid request / Invalid client_id / Invalid redirect_uri |
400 |
必要参数、应用或回调白名单校验失败,响应为纯文本。 |
Access denied |
403 |
当前用户没有访问目标应用的角色权限。 |
注意:
- 未登录时先跳转 SSO 登录页;授权码只能交换一次。
POST
/oauth/token.php
业务系统后端使用一次性授权码交换 Access Token。
| 认证方式 | client_id + client_secret |
| 请求格式 | application/x-www-form-urlencoded |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
grant_type |
Form |
string |
是 |
固定为 authorization_code。 |
code |
Form |
string |
是 |
authorize 回调返回的一次性授权码。 |
client_id |
Form |
string |
是 |
应用 Client ID。 |
client_secret |
Form |
string |
按应用配置 |
应用设置了 Secret 时必须传入。 |
redirect_uri |
Form |
URL |
是 |
必须与发起授权时的 redirect_uri 一致。 |
请求示例
POST https://sso.xahrf.cn/oauth/token.php
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTHORIZATION_CODE&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
成功响应
HTTP 200:标准 OAuth2 Token 响应。
{
"access_token": "64位十六进制令牌",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile"
}
错误响应
| error / 响应 | HTTP | 说明 |
unsupported_grant_type |
400 |
grant_type 不是 authorization_code。 |
invalid_grant |
400 |
授权码无效、过期、已使用,或客户端/回调不匹配。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- client_secret 只能保存在业务系统后端,不应下发到浏览器或移动客户端。
GET
POST
/oauth/userinfo.php
使用有效 Access Token 获取标准 OAuth 用户资料。
| 认证方式 | Bearer Token |
| 请求格式 | Authorization Header;兼容 Query/Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer token |
推荐 |
格式为 Bearer ACCESS_TOKEN。 |
access_token |
Query/Form |
string |
Header 未传时 |
兼容参数;生产接入优先使用 Authorization Header。 |
请求示例
GET https://sso.xahrf.cn/oauth/userinfo.php
Authorization: Bearer ACCESS_TOKEN
成功响应
HTTP 200:OAuth 用户资料响应。
{
"sub": "6",
"username": "zhangsan",
"name": "张三",
"email": "zhangsan@example.com",
"mobile": "13800138000",
"employee_no": "E006",
"dept_id": 7,
"roles": ["employee"]
}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_token |
401 |
未提供 Token,或 Token 无效、过期、已吊销。 |
注意:
- 该端点返回 OAuth 风格响应,不包含 ok 字段。
REST 认证 API
面向 App、桌面客户端和服务端集成的 JSON API。成功响应包含 ok=true,失败响应包含 error 与 message。
POST
/api/auth/login.php
使用用户名、手机号或邮箱与密码登录,并为指定应用签发 Access Token。
| 认证方式 | 应用凭证 + 用户密码 |
| 请求格式 | application/json 或 Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
username / mobile / email |
Body |
string |
三选一 |
登录标识只能提供一个。 |
password |
Body |
string |
是 |
用户密码。 |
client_id |
Body |
string |
是 |
目标应用 Client ID。 |
client_secret |
Body |
string |
按应用配置 |
目标应用设置了 Secret 时必须传入。 |
请求示例
POST https://sso.xahrf.cn/api/auth/login.php
Content-Type: application/json
{"username":"zhangsan","password":"USER_PASSWORD","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET"}
成功响应
HTTP 200:返回 Token 和当前用户完整资料。
{
"ok": true,
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"id": 6,
"username": "zhangsan",
"display_name": "张三",
"employee_no": "E006",
"email": "zhangsan@example.com",
"mobile": "13800138000",
"account_type": "formal",
"status": "active",
"dept_id": 7,
"dept_name": "技术部",
"position_id": 1,
"position_name": "开发工程师",
"roles": ["employee"]
}
}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
登录标识数量不正确或缺少密码。 |
invalid_client |
400/401 |
缺少 client_id,或应用凭证不正确。 |
invalid_credentials |
401 |
用户凭证错误或账号不可用。 |
access_denied |
403 |
用户没有访问目标应用的角色权限。 |
注意:
- 不要在无法安全保存 client_secret 的公开客户端中使用应用 Secret。
GET
POST
/api/auth/userinfo.php
验证 Access Token 并返回 REST 格式的当前用户资料与角色。
| 认证方式 | Bearer Token |
| 请求格式 | Authorization Header;兼容 Query/Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer token |
推荐 |
格式为 Bearer ACCESS_TOKEN。 |
access_token |
Query/Form |
string |
Header 未传时 |
兼容参数,由共享 bearerToken() 读取。 |
请求示例
GET https://sso.xahrf.cn/api/auth/userinfo.php
Authorization: Bearer ACCESS_TOKEN
成功响应
HTTP 200:返回 REST 用户资料。
{
"ok": true,
"user": {
"id": 6,
"username": "zhangsan",
"display_name": "张三",
"employee_no": "E006",
"email": "zhangsan@example.com",
"mobile": "13800138000",
"account_type": "formal",
"status": "active",
"dept_id": 7,
"dept_name": "技术部",
"position_id": 1,
"position_name": "开发工程师",
"roles": ["employee"]
},
"sub": "6"
}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_token |
401 |
缺少 Token,或 Token 无效、过期、已吊销。 |
method_not_allowed |
405 |
请求方法不是 GET 或 POST。 |
注意:
- Authorization Header 是推荐传递方式,可避免 Token 出现在 URL 和访问日志中。
POST
/api/auth/logout.php
吊销一个 Access Token;重复吊销是安全的。
| 认证方式 | Bearer Token |
| 请求格式 | Authorization Header;兼容 JSON/Form/Query |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer token |
推荐 |
需要吊销的 Token。 |
access_token |
Body/Query |
string |
Header 未传时 |
兼容参数。 |
请求示例
POST https://sso.xahrf.cn/api/auth/logout.php
Authorization: Bearer ACCESS_TOKEN
成功响应
HTTP 200:revoked 表示本次是否实际吊销了一条有效 Token。
{"ok":true,"revoked":true}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_token |
401 |
请求中没有 access_token。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- revoked=false 表示 Token 已失效、已吊销或不存在,不代表请求失败。
扫码登录 API
通过短期挑战完成 Web Session 登录或 API Token 登录;钉钉 Web 模式优先使用 SSO 托管纯二维码授权页。
POST
/api/auth/qr/create.php
创建 Web 或 API 模式的扫码登录挑战并返回二维码目标。
| 认证方式 | Web: Session;API: client_id + client_secret |
| 请求格式 | application/json 或 Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
redirect |
Body |
string |
Web 可选 |
Web 登录成功后的站内跳转地址。 |
client_id |
Body |
string |
API 模式是 |
传入即创建 API 模式挑战。 |
client_secret |
Body |
string |
按应用配置 |
API 模式下应用设置了 Secret 时必须传入。 |
请求示例
POST https://sso.xahrf.cn/api/auth/qr/create.php
Content-Type: application/json
{"redirect":"/"}
成功响应
HTTP 200:Web 模式示例;页面直接将 qr_url 绘制为二维码。API 模式额外返回 poll_key。
{
"ok": true,
"mode": "web",
"qr_token": "QR_TOKEN",
"qr_url": "https://oapi.dingtalk.com/connect/oauth2/sns_authorize?...",
"expires_in": 120,
"poll_interval": 2,
"dingtalk_appid": "DINGTALK_APP_KEY",
"dingtalk_goto": "https://oapi.dingtalk.com/connect/oauth2/sns_authorize?..."
}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_client |
401 |
API 模式应用凭证错误或应用未上线。 |
invalid_request |
400 |
浏览器会话或创建参数无效。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- 不传 client_id 时创建绑定当前 UNIFIED_SSO_SID 的 Web 挑战。
- API 模式返回的 poll_key 是轮询凭证,不能放入二维码或泄露给扫码者。
- 未启用钉钉时 qr_url 回退为 /qr/scan.php?token=...。
POST
/api/auth/qr/dingtalk-confirm.php
钉钉 Web 托管授权页提交 JSAPI authCode 并确认 Web 挑战。
| 认证方式 | 钉钉 JSAPI auth_code + qr_token |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
auth_code |
JSON Body |
string |
是 |
dd.runtime.permission.requestAuthCode 返回的一次性授权码。 |
qr_token |
JSON Body |
string |
是 |
Web 登录二维码中的短期挑战 Token。 |
请求示例
POST https://sso.xahrf.cn/api/auth/qr/dingtalk-confirm.php
Content-Type: application/json
{"auth_code":"DINGTALK_JSAPI_CODE","qr_token":"QR_TOKEN"}
成功响应
HTTP 200:已绑定钉钉 UserId 的用户确认 Web 挑战。
{"ok":true,"status":"confirmed","display_name":"张三"}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
JSON 无效或缺少 auth_code/qr_token。 |
not_found |
404 |
Web 挑战不存在、过期或模式不匹配。 |
not_bound |
403 |
钉钉 UserId 尚未绑定 SSO 用户。 |
dingtalk_error |
502 |
钉钉企业免登 authCode 交换失败。 |
注意:
- 仅接受 mode=web 挑战;不接受 poll_key,不返回 OAuth Token。JSAPI 失败时托管页回退到 dingtalk_goto。
POST
/api/auth/qr/scan.php
将有效挑战从 pending 标记为 scanned。
| 认证方式 | qr_token |
| 请求格式 | application/json 或 Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
qr_token |
Body |
string |
是 |
创建挑战时返回的二维码 Token。 |
请求示例
POST https://sso.xahrf.cn/api/auth/qr/scan.php
Content-Type: application/json
{"qr_token":"QR_TOKEN"}
成功响应
HTTP 200:返回当前扫码状态。
{"ok":true,"status":"scanned"}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
缺少 qr_token。 |
not_found |
400 |
二维码不存在。 |
expired |
410 |
二维码已经过期。 |
注意:
- 已经 confirmed 的挑战会保持 confirmed,不会退回 scanned。
POST
/api/auth/qr/confirm.php
以当前用户身份确认扫码登录挑战。
| 认证方式 | Bearer Token、SSO Session 或用户名密码 |
| 请求格式 | application/json 或 Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
qr_token |
Body |
string |
是 |
待确认的二维码 Token。 |
Authorization |
Header |
Bearer token |
三种身份方式之一 |
优先使用的确认用户身份。 |
UNIFIED_SSO_SID |
Cookie |
Session |
三种身份方式之一 |
已登录 SSO 的浏览器会话。 |
username |
Body |
string |
无 Token/Session 时 |
用户名密码确认方式的账号。 |
password |
Body |
string |
无 Token/Session 时 |
用户名密码确认方式的密码。 |
请求示例
POST https://sso.xahrf.cn/api/auth/qr/confirm.php
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"qr_token":"QR_TOKEN"}
成功响应
HTTP 200:挑战确认成功。
{"ok":true,"status":"confirmed","display_name":"张三"}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400/401 |
缺少 qr_token,或未提供可验证的用户身份。 |
invalid_token / invalid_credentials |
401 |
用户 Token 或用户名密码无效。 |
access_denied |
403 |
租户不匹配、账号不可用或无权访问目标应用。 |
not_found / expired |
404 |
挑战不存在或已过期。 |
invalid_state |
400 |
挑战状态已经被其他请求变更,不能重复确认。 |
注意:
- API 模式还会检查确认用户是否有权访问 client_id 对应应用。
GET
POST
/api/auth/qr/status.php
轮询挑战状态,并在确认后建立 Web Session 或签发 API Access Token。
| 认证方式 | Web: Session + qr_token;API: qr_token + poll_key |
| 请求格式 | Query、JSON 或 Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
qr_token |
Query/Body |
string |
是 |
待查询的二维码 Token。 |
poll_key |
Query/Body |
string |
API 模式是 |
API 模式创建时返回的私密轮询凭证。 |
UNIFIED_SSO_SID |
Cookie |
Session |
Web 模式是 |
必须与创建挑战时的浏览器 Session 相同。 |
请求示例
GET https://sso.xahrf.cn/api/auth/qr/status.php?qr_token=QR_TOKEN&poll_key=POLL_KEY
成功响应
HTTP 200:未完成时返回状态;API 首次 confirmed 轮询返回 Token。
{
"ok": true,
"status": "confirmed",
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"user": {"id":6,"username":"zhangsan","display_name":"张三","roles":["employee"]}
}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
缺少 qr_token、模式不匹配或挑战状态异常。 |
access_denied |
403 |
Web Session 或 API poll_key 不匹配。 |
not_found |
400 |
挑战不存在。 |
invalid_state |
400 |
挑战已确认但用户或 API 应用状态不完整。 |
注意:
- 状态可能为 pending、scanned、confirmed、failed、cancelled 或 expired。
- API Token 只在首次成功领取时返回;后续 confirmed 轮询只提示令牌已发放。
POST
/api/auth/device/create.php
为没有业务后端的 Qt 应用创建钉钉设备扫码挑战。
| 认证方式 | 公开 client_id |
| 请求格式 | application/json 或 Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
client_id |
Body |
string |
是 |
当前仅允许 app002_client;这是公开标识,不传 client_secret。 |
请求示例
POST https://sso.xahrf.cn/api/auth/device/create.php
Content-Type: application/json
{"client_id":"app002_client"}
成功响应
HTTP 200:返回 Qt 应保存的轮询凭证和 SSO 托管授权页地址。
{
"ok": true,
"device_code": "DEVICE_CODE",
"poll_key": "POLL_KEY",
"verification_url": "https://sso.xahrf.cn/device/authorize.php?device_code=DEVICE_CODE",
"qr_content": "https://sso.xahrf.cn/device/authorize.php?device_code=DEVICE_CODE",
"qr_image_url": "https://sso.xahrf.cn/device/qr-image.php?device_code=DEVICE_CODE",
"expires_in": 120,
"poll_interval": 2
}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_client |
401 |
应用不存在、未上线或 client_id 缺失。 |
rate_limited |
429 |
同一 IP 对该应用创建设备挑战过于频繁。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- 当前仅 app002_client 开启设备模式;Qt 可下载 qr_image_url 后用 QLabel/QPixmap 显示,poll_key 不进入二维码。
POST
/api/auth/device/status.php
轮询 Qt 设备扫码挑战,并一次性领取 Access Token。
| 认证方式 | device_code + poll_key |
| 请求格式 | application/json 或 Form |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
device_code |
Body |
string |
是 |
创建接口返回的设备挑战代码。 |
poll_key |
Body |
string |
是 |
创建接口返回的私密轮询凭证,只保存在 Qt,不放入 verification_url。 |
请求示例
POST https://sso.xahrf.cn/api/auth/device/status.php
Content-Type: application/json
{"device_code":"DEVICE_CODE","poll_key":"POLL_KEY"}
成功响应
HTTP 200:pending/scanned 表示继续轮询;confirmed 返回该挑战唯一的 Access Token。
{
"ok": true,
"status": "confirmed",
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"user": {"id":6,"username":"zhangsan","display_name":"张三","roles":["employee"]}
}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
缺少 device_code 或 poll_key,或挑战模式不匹配。 |
access_denied |
403 |
poll_key 不正确。 |
not_found |
400 |
设备挑战不存在。 |
invalid_state |
400 |
已确认挑战缺少有效用户或应用状态。 |
注意:
- 挑战有效期内可用同一 device_code + poll_key 幂等重取相同 Token;Qt 收到后应立即安全保存。
POST
/api/auth/device/dingtalk-confirm.php
钉钉 H5 页面提交 JSAPI authCode 并确认设备挑战。
| 认证方式 | 钉钉 JSAPI auth_code + device_code |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
auth_code |
JSON Body |
string |
是 |
dd.runtime.permission.requestAuthCode 返回的短期授权码。 |
device_code |
JSON Body |
string |
是 |
二维码 verification_url 中的短期设备挑战代码。 |
请求示例
POST https://sso.xahrf.cn/api/auth/device/dingtalk-confirm.php
Content-Type: application/json
{"auth_code":"DINGTALK_JSAPI_CODE","device_code":"DEVICE_CODE"}
成功响应
HTTP 200:已绑定钉钉 UserId 的用户通过应用访问检查并确认挑战。
{"ok":true,"status":"confirmed","display_name":"张三"}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
JSON 无效或缺少 auth_code/device_code。 |
not_found |
404 |
设备挑战不存在、过期或模式不匹配。 |
not_bound |
403 |
钉钉 UserId 尚未绑定 SSO 用户。 |
access_denied |
400 |
用户无权访问 app002_client。 |
dingtalk_error |
502 |
钉钉企业免登 authCode 交换失败。 |
注意:
- 仅由 SSO 手机授权页调用;不接受 poll_key,不返回 OAuth Token,不调用 contact/users/me。
POST
/api/auth/dingtalk/pc-login.php
钉钉 PC 客户端通过 JSAPI authCode 确认当前二维码挑战。
| 认证方式 | 钉钉 authCode + qr_token |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
auth_code |
JSON Body |
string |
是 |
dd.runtime.permission.requestAuthCode 返回的一次性授权码。 |
qr_token |
JSON Body |
string |
是 |
当前页面已创建的扫码挑战 Token。 |
请求示例
POST https://sso.xahrf.cn/api/auth/dingtalk/pc-login.php
Content-Type: application/json
{"auth_code":"DINGTALK_AUTH_CODE","qr_token":"QR_TOKEN"}
成功响应
HTTP 200:钉钉账号匹配 SSO 用户并确认挑战。
{"ok":true,"status":"confirmed","display_name":"张三"}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
请求体不是 JSON,或缺少 auth_code / qr_token。 |
not_found |
404 |
二维码不存在或已过期。 |
not_bound |
403 |
钉钉 unionId 尚未绑定 SSO 用户。 |
access_denied |
400 |
确认用户无权访问目标应用。 |
invalid_state |
400 |
挑战状态已被其他请求变更。 |
dingtalk_error |
502 |
钉钉授权码交换或用户资料接口失败。 |
注意:
- 这是钉钉 PC 客户端一键登录接口;浏览器原生内嵌二维码走 DDLogin 与回调流程。
组织 API
按调用者角色范围或服务应用凭证读取当前租户的启用部门与员工。
GET
/api/org/departments.php
返回可见部门及 active employee 成员,包含钉钉用户 ID。
| 认证方式 | Bearer Token 或 client_id + client_secret |
| 请求格式 | Authorization Header 或 Query |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer token |
两种方式之一 |
用户模式;范围由角色和所属部门决定。 |
client_id |
Query |
string |
无 Token 时 |
服务模式应用 Client ID。 |
client_secret |
Query |
string |
按应用配置 |
应用设置了 Secret 时必须传入。 |
请求示例
GET https://sso.xahrf.cn/api/org/departments.php
Authorization: Bearer ACCESS_TOKEN
成功响应
HTTP 200:scope 为 all、department 或 service;users 始终为数组。
{
"ok": true,
"scope": "department",
"departments": [
{
"id": 7,
"name": "技术部",
"code": "tech",
"parent_id": 1,
"users": [
{
"id": 6,
"username": "zhangsan",
"display_name": "张三",
"employee_no": "E006",
"mobile": "13800138000",
"dingtalk_user_id": "203029272224114661",
"dept_id": 7,
"roles": ["employee"]
}
]
}
]
}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_token |
401 |
Bearer Token 无效,或请求未提供 Token/client_id。 |
invalid_client |
401 |
服务模式应用凭证不正确。 |
method_not_allowed |
405 |
请求方法不是 GET。 |
注意:
- super_admin 得到 scope=all;普通用户得到 scope=department;应用凭证得到 scope=service。
- 当前实现中 service 模式同样返回 users 数组;没有成员时为空数组。
- 只返回状态为 active 且具有 employee 角色的成员。
微信小程序认证与通知
小程序一键登录、手机号绑定、PC 扫码确认和通知投递。
POST
/api/auth/wechat/login.php
微信小程序一键登录。
| 认证方式 | 微信 login_code |
| 请求格式 | JSON |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
tenant_id |
Body |
integer |
否 |
租户 ID。 |
login_code |
Body |
string |
是 |
wx.login 返回的 code。 |
成功响应
HTTP 200:登录成功并建立 SSO 会话。
{"ok":true,"user":{"id":1}}
错误响应
| error / 响应 | HTTP | 说明 |
not_bound |
403 |
微信身份未绑定。 |
注意:
- phone_auto 模式需额外提交 phone_code。
POST
/api/auth/wechat/mini-login.php
已绑定微信身份直接换取 SSO Access Token。
| 认证方式 | 白名单 client_id + 微信 login_code |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
client_id |
Body |
string |
是 |
必须是 app005_client;其他客户端一律拒绝。 |
login_code |
Body |
string |
是 |
wx.login 返回的一次性 code。 |
请求示例
POST https://sso.xahrf.cn/api/auth/wechat/mini-login.php
Content-Type: application/json
{"login_code":"WX_LOGIN_CODE","client_id":"app005_client"}
成功响应
HTTP 200:返回 Access Token、精简用户资料和精简授权应用列表。
{"ok":true,"access_token":"ACCESS_TOKEN","token_type":"Bearer","expires_in":3600,"user":{"id":6,"username":"zhangsan","display_name":"张三","employee_no":"E006","account_type":"formal","dept_id":7,"dept_name":"技术部","position_id":1,"position_name":"开发工程师","roles":["employee"]},"applications":[{"id":5,"code":"app005","name":"微信小程序","client_id":"app005_client","homepage_url":"https://example.test","icon_url":"https://example.test/icon.png","is_preferred":true}]}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
缺少参数,或提交了 tenant_id、user_id、client_secret 等禁止字段。 |
invalid_client |
401 |
客户端不在白名单、应用不存在、未上线或服务端未配置 Secret。 |
not_bound |
403 |
微信身份尚未绑定 SSO 用户。 |
access_denied |
403 |
用户不是 active 或无权访问目标应用。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
wechat_api_error |
502 |
微信接口或微信配置暂时不可用。 |
注意:
- client_secret 仅由 SSO 服务端读取,小程序不得提交且响应不会返回。
- 租户只从 client_id 对应的应用记录获取;该接口不建立 PHP Session。
- applications 只返回 id、code、name、client_id、homepage_url、icon_url、is_preferred。
POST
/api/auth/wechat/mini-refresh.php
使用现有小程序 Token 静默刷新用户资料和应用权限。
| 认证方式 | Authorization: Bearer + 固定 client_id |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer Token |
是 |
mini-login 或 mini-bind 返回的 SSO Access Token。 |
client_id |
Body |
string |
是 |
必须是 app005_client。 |
请求示例
POST https://sso.xahrf.cn/api/auth/wechat/mini-refresh.php
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"client_id":"app005_client"}
成功响应
HTTP 200:复用原 Token 返回最新用户资料和授权应用列表。
{"ok":true,"access_token":"ACCESS_TOKEN","token_type":"Bearer","expires_in":3600,"user":{"id":2,"username":"zhangjiawei","display_name":"张嘉玮","employee_no":"E002","dept_name":"技术部","roles":["employee"]},"applications":[]}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
client_id 类型错误或提交了禁止字段。 |
invalid_token |
401 |
Token 缺失、无效、过期或已吊销。 |
access_denied |
403 |
用户账号不可用或已无权访问小程序应用。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
wechat_api_error |
502 |
权限刷新服务暂时不可用。 |
注意:
- 不接收 login_code,不读取微信绑定关系。
- client_secret 仅由 SSO 服务端读取,任何请求或响应都不会暴露。
POST
/api/auth/wechat/mini-bind.php
未绑定微信身份通过 SSO 账号密码完成首次绑定并取得 Token。
| 认证方式 | 白名单 client_id + 微信 login_code + 用户密码 |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
client_id |
Body |
string |
是 |
必须是 app005_client;其他客户端一律拒绝。 |
login_code |
Body |
string |
是 |
wx.login 返回的一次性 code。 |
username |
Body |
string |
是 |
管理员预先创建的 SSO 登录名。 |
password |
Body |
string |
是 |
SSO 用户密码。 |
请求示例
POST https://sso.xahrf.cn/api/auth/wechat/mini-bind.php
Content-Type: application/json
{"login_code":"WX_LOGIN_CODE","client_id":"app005_client","username":"zhangsan","password":"USER_PASSWORD"}
成功响应
HTTP 200:绑定成功并返回 Access Token、精简用户资料和授权应用列表。
{"ok":true,"access_token":"ACCESS_TOKEN","token_type":"Bearer","expires_in":3600,"user":{"id":6,"username":"zhangsan","display_name":"张三","employee_no":"E006","account_type":"formal","dept_id":7,"dept_name":"技术部","position_id":1,"position_name":"开发工程师","roles":["employee"]},"applications":[{"id":5,"code":"app005","name":"微信小程序","client_id":"app005_client","homepage_url":"https://example.test","icon_url":"https://example.test/icon.png","is_preferred":true}]}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
缺少参数,或提交了身份/Secret 覆盖字段。 |
invalid_client |
401 |
客户端不在白名单、应用不存在、未上线或服务端未配置 Secret。 |
invalid_credentials |
401 |
账号不存在或密码错误。 |
access_denied |
403 |
用户不是 active 或无权访问目标应用。 |
already_bound |
409 |
微信或 SSO 用户已存在其他绑定,不会自动换绑。 |
rate_limited |
429 |
openid 或租户账号维度在固定 10 分钟窗口达到 5 次失败。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
wechat_api_error |
502 |
微信接口或微信配置暂时不可用。 |
注意:
- 第 5 次失败当次返回 429;成功绑定在同一事务中清除两个限流维度。
- 同一微信和同一用户重复绑定幂等成功;任一方向冲突均返回 409。
- 应用 Secret 仅在 SSO 内部用于现有认证流程,任何响应均不会包含 Secret。
POST
/api/auth/wechat/mini-phone-bind.php
通过微信授权手机号自动绑定唯一的企业账号并取得 SSO Access Token。
| 认证方式 | 白名单 client_id + 微信 login_code + phone_code |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
client_id |
Body |
string |
是 |
必须是 app005_client;其他客户端一律拒绝。 |
login_code |
Body |
string |
是 |
wx.login 返回的一次性 code,仅由 SSO 服务端换取 OpenID。 |
phone_code |
Body |
string |
是 |
getPhoneNumber 返回的一次性 code,仅由 SSO 服务端向微信解析。 |
请求示例
POST https://sso.xahrf.cn/api/auth/wechat/mini-phone-bind.php
Content-Type: application/json
{"client_id":"app005_client","login_code":"WX_LOGIN_CODE","phone_code":"WX_PHONE_CODE"}
成功响应
HTTP 200:在同一事务中创建严格微信绑定,并返回与 mini-login 相同的 Token、精简用户资料和应用列表。
{"ok":true,"access_token":"ACCESS_TOKEN","token_type":"Bearer","expires_in":3600,"user":{"id":6,"username":"zhangsan","display_name":"张三","employee_no":"E006","account_type":"formal","dept_id":7,"dept_name":"技术部","position_id":1,"position_name":"开发工程师","roles":["employee"]},"applications":[]}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
字段缺失、类型不正确,或提交了 tenant_id、user_id、client_secret 等禁止字段。 |
invalid_client |
401 |
客户端不是 app005_client,或应用未上线、未配置服务端 Secret。 |
access_denied |
403 |
唯一候选用户未启用或没有工作台应用权限。 |
phone_not_matched |
404 |
微信手机号不是可用的中国大陆 11 位号码,或未匹配企业账号。 |
ambiguous_phone |
409 |
同一手机号匹配多个企业账号。 |
already_bound |
409 |
微信身份或候选 SSO 用户已绑定到其他对象,不会自动换绑。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
wechat_api_error |
502 |
微信接口、微信配置或绑定服务暂时不可用;不会返回上游详情。 |
注意:
- 仅接受 +86、86 或无前缀的中国大陆号码;服务端规范为 11 位后才查询当前应用租户。
- 小程序不得提交用户 ID、租户、OpenID、UnionID 或应用 Secret;这些字段会被拒绝。
- login_code、phone_code、微信上游详情和应用 Secret 不会写入成功或错误响应。
POST
/api/auth/wechat/bind.php
将已认证的 SSO 用户绑定微信身份。
| 认证方式 | Bearer Token 或 SSO Session |
| 请求格式 | JSON |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer token |
两种方式之一 |
小程序使用账号密码登录取得的访问令牌,格式为 Bearer ACCESS_TOKEN。 |
UNIFIED_SSO_SID |
Cookie |
Session |
两种方式之一 |
浏览器已登录 SSO 时可使用 Session。 |
login_code |
Body |
string |
是 |
wx.login 返回的 code。 |
phone_code |
Body |
string |
否 |
微信手机号授权 code;当前登录用户绑定时可不传。 |
请求示例
POST https://sso.xahrf.cn/api/auth/wechat/bind.php
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"login_code":"WX_LOGIN_CODE"}
成功响应
HTTP 200:绑定成功。
{"ok":true,"user_id":1,"openid":"OPENID"}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_code |
400 |
未提供 login_code。 |
invalid_token |
401 |
Bearer Token 无效、已过期或已吊销。 |
unauthorized |
401 |
既未提供 Bearer Token,也没有有效 SSO Session。 |
access_denied |
403 |
Bearer Token 对应的用户账号不可用。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
already_bound |
409 |
微信身份已绑定其他用户。 |
wechat_api_error |
502 |
微信接口调用失败或微信配置不可用。 |
注意:
- user_id 和 tenant_id 只从已验证的 Bearer Token 或 SSO Session 获取,不接受请求参数指定。
- 请求携带 Authorization 时以 Bearer Token 为准;Token 无效不会降级使用 Session。
POST
/api/auth/wechat/qr/confirm.php
小程序确认 PC 登录 challenge。
| 认证方式 | 微信 login_code |
| 请求格式 | JSON |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
device_code |
Body |
string |
是 |
PC challenge 编码。 |
login_code |
Body |
string |
是 |
wx.login 返回的 code。 |
成功响应
HTTP 200:challenge 已确认。
{"ok":true,"status":"confirmed"}
错误响应
| error / 响应 | HTTP | 说明 |
challenge_expired |
400 |
challenge 已过期。 |
注意:
- 确认后 PC 端继续轮询既有 status 接口。
POST
/api/auth/web-qr/create.php
创建绑定当前浏览器 Session 的微信网页登录挑战;可生成微信小程序码或小程序内扫一扫普通二维码。
| 认证方式 | 匿名浏览器 Session |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
return_to |
Body |
string |
是 |
仅允许 /、/console 或带查询参数的 /oauth/authorize.php。 |
qr_type |
Body |
string |
否 |
wechat(缺省,生成微信小程序码)或 mini_scan(生成小程序内扫一扫的普通二维码)。 |
成功响应
HTTP 200:返回内部挑战 ID、短期 scene 及同浏览器可读取的 PNG 图片地址。
{"ok":true,"challenge_id":123,"qr_code":"HRF:AbCdEf1234567890xYz","qr_image_url":"https://sso.example.test/api/auth/web-qr/image.php?id=123","expires_in":120}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
请求字段或 return_to 非法。 |
access_denied |
403 |
默认租户不可用。 |
wechat_api_error |
502 |
微信小程序码生成暂时不可用。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- scene 仅包含短随机挑战值;AppSecret 和 access_token 只在 SSO 服务端使用。
- wechat 创建时保存微信返回的小程序码 PNG;mini_scan 在本地生成普通 PNG,不读取微信配置也不调用微信接口。图片请求不会再次调用微信接口。
GET
/api/auth/web-qr/image.php
返回创建时保存的微信小程序码 PNG。
| 认证方式 | 创建挑战的匿名浏览器 Session |
| 请求格式 | Query;响应为 image/png |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
id |
Query |
positive integer |
是 |
创建接口返回的内部挑战 ID。 |
成功响应
HTTP 200:返回原始 image/png 二进制。
PNG binary
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
id 缺失或格式无效。 |
not_found |
404 |
挑战不存在、过期、非所属 Session 或没有已保存图片。 |
wechat_api_error |
502 |
挑战存储暂时不可用。 |
method_not_allowed |
405 |
请求方法不是 GET。 |
注意:
- 设置 no-store 与 nosniff。图片接口只读取数据库中创建时保存的 PNG,不重新请求微信。
GET
/api/auth/web-qr/status.php
轮询微信小程序网页登录挑战;获批后建立 SSO Web Session 并消费挑战。
| 认证方式 | 创建挑战的匿名浏览器 Session |
| 请求格式 | Query |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
challenge_id |
Query |
positive integer |
是 |
创建接口返回的内部挑战 ID。 |
成功响应
HTTP 200:pending 表示继续等待;approved 表示本请求已建立 Web Session,并返回挑战中保存的白名单回跳地址。
{"ok":true,"status":"approved","redirect_url":"/oauth/authorize.php?response_type=code&client_id=app005_client"}
错误响应
| error / 响应 | HTTP | 说明 |
access_denied |
403 |
挑战不属于当前浏览器 Session,或获批用户不可用。 |
not_found |
404 |
挑战不存在或过期。 |
consumed |
409 |
挑战已被先前轮询消费。 |
method_not_allowed |
405 |
请求方法不是 GET。 |
注意:
- 状态转换为 pending → approved → consumed;消费使用条件更新,不能重复建立 Session。
POST
/api/auth/wechat/mini-web-login.php
已登录小程序用户确认微信网页登录挑战。
| 认证方式 | Authorization: Bearer;固定 app005_client |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer Token |
是 |
app005_client 签发的有效 Access Token。 |
client_id |
Body |
string |
是 |
固定为 app005_client。 |
qr_code |
Body |
string |
是 |
小程序码 scene 中的挑战值。 |
成功响应
HTTP 200:挑战已由 Token 对应的用户授权。
{"ok":true,"status":"approved"}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
参数缺失、类型错误或提交了用户/租户覆盖字段。 |
invalid_token |
401 |
Bearer Token 缺失、无效、过期或客户端不匹配。 |
access_denied |
403 |
Token 用户、租户或应用权限不可用。 |
not_found |
404 |
挑战不存在或过期。 |
qr_already_approved |
409 |
挑战已由其他用户授权。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- 用户和租户只能从 Bearer Token 获取,不接受请求传入 user_id 或 tenant_id。
- 同一用户重复确认幂等返回 approved 与 already_approved。
POST
/api/auth/wechat/salary-security/status.php
查询当前小程序用户是否已设置工资二次验证密码。
| 认证方式 | Authorization: Bearer;固定 app005_client |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer Token |
是 |
必须是 app005_client 签发且未过期的 SSO Access Token。 |
client_id |
Body |
string |
是 |
固定为 app005_client。 |
请求示例
POST https://sso.xahrf.cn/api/auth/wechat/salary-security/status.php
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"client_id":"app005_client"}
成功响应
HTTP 200:返回密码是否已设置。
{"ok":true,"is_set":false}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
Content-Type 或 client_id 不正确。 |
invalid_token |
401 |
Bearer Token 缺失、无效、过期或已吊销。 |
access_denied |
403 |
Token 不属于 app005_client。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- client_id 同时从 OAuth Token 和 JSON 请求体校验,不接受其他客户端。
POST
/api/auth/wechat/salary-security/set.php
首次设置 6 位工资二次验证密码并签发短期工资访问凭证。
| 认证方式 | Authorization: Bearer;固定 app005_client |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer Token |
是 |
必须是 app005_client 签发且未过期的 SSO Access Token。 |
client_id |
Body |
string |
是 |
固定为 app005_client。 |
password |
Body |
6-digit string |
是 |
仅接受 6 位 ASCII 数字;仅在传输中使用。 |
请求示例
POST https://sso.xahrf.cn/api/auth/wechat/salary-security/set.php
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"client_id":"app005_client","password":"123456"}
成功响应
HTTP 200:密码首次设置成功,返回 600 秒工资访问凭证。
{"ok":true,"salary_access_token":"SALARY_ACCESS_TOKEN","expires_in":600}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
Content-Type、client_id 或密码格式不正确。 |
invalid_token |
401 |
Bearer Token 缺失、无效、过期或已吊销。 |
access_denied |
403 |
Token 不属于 app005_client。 |
already_set |
409 |
该用户已经设置工资验证密码。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- 密码只使用 Argon2id 哈希保存;访问凭证为短期不透明值,客户端不得记录或回显。
POST
/api/auth/wechat/salary-security/verify.php
验证 6 位工资二次验证密码并换取短期工资访问凭证。
| 认证方式 | Authorization: Bearer;固定 app005_client |
| 请求格式 | application/json |
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
Authorization |
Header |
Bearer Token |
是 |
必须是 app005_client 签发且未过期的 SSO Access Token。 |
client_id |
Body |
string |
是 |
固定为 app005_client。 |
password |
Body |
6-digit string |
是 |
仅接受 6 位 ASCII 数字;仅在传输中使用。 |
请求示例
POST https://sso.xahrf.cn/api/auth/wechat/salary-security/verify.php
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"client_id":"app005_client","password":"123456"}
成功响应
HTTP 200:验证成功,返回替换旧凭证的 600 秒工资访问凭证。
{"ok":true,"salary_access_token":"SALARY_ACCESS_TOKEN","expires_in":600}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
Content-Type、client_id 或密码格式不正确。 |
invalid_token |
401 |
Bearer Token 缺失、无效、过期或已吊销。 |
invalid_password |
401 |
工资验证密码不正确。 |
access_denied |
403 |
Token 不属于 app005_client。 |
not_set |
409 |
尚未设置工资验证密码。 |
locked |
429 |
第六次错误后密码锁定 30 分钟。 |
method_not_allowed |
405 |
请求方法不是 POST。 |
注意:
- 成功验证会清空失败次数并撤销同一用户此前的工资访问凭证;密码或访问凭证不得记录到日志。
POST
/api/auth/wechat/unbind.php
解除当前用户微信绑定。
成功响应
HTTP 200:绑定已解除。
{"ok":true,"unbound":true}
错误响应
| error / 响应 | HTTP | 说明 |
unauthorized |
401 |
未登录。 |
POST
/api/notifications/create.php
创建站内及微信通知。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
user_ids |
Body |
array |
是 |
接收用户 ID。 |
title |
Body |
string |
是 |
标题。 |
成功响应
HTTP 201:通知已创建。
{"ok":true,"notification_id":1}
错误响应
| error / 响应 | HTTP | 说明 |
invalid_request |
400 |
通知参数无效。 |
GET
/api/notifications/index.php
查询当前用户通知。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
page |
Query |
integer |
否 |
页码。 |
成功响应
HTTP 200:返回通知列表。
{"ok":true,"items":[]}
错误响应
| error / 响应 | HTTP | 说明 |
unauthorized |
401 |
未登录。 |
POST
/api/notifications/retry.php
重试微信通知投递。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
delivery_id |
Body |
integer |
是 |
投递记录 ID。 |
成功响应
HTTP 200:投递成功或已记录失败。
{"ok":true,"status":"sent"}
错误响应
| error / 响应 | HTTP | 说明 |
not_found |
400 |
投递记录不存在。 |
OAuth2 授权码接入流程
业务系统生成并保存 state
→ 浏览器访问 /oauth/authorize.php
→ 用户登录 SSO 并确认授权
→ SSO 302 回到 redirect_uri?code=...&state=...
→ 业务系统校验 state
→ 后端 POST /oauth/token.php 交换 Access Token
→ GET /oauth/userinfo.php 获取用户资料
安全要求:授权码有效期为 600 秒且只能使用一次。业务系统必须验证 state,Token 交换必须在后端完成。
钉钉纯二维码流程
启用钉钉后,Web 模式的 /api/auth/qr/create.php 在 qr_url 中返回 SSO 托管地址 /qr/authorize.php?qr_token=...。登录页使用本地二维码库绘制该地址,不嵌入 DDLogin iframe;dingtalk_goto 仅作为 JSAPI 失败后的官方 OAuth 回退。
POST /api/auth/qr/create.php(保留同一浏览器 Session)
→ 浏览器将 qr_url 绘制为纯二维码
→ 钉钉打开 SSO /qr/authorize.php
→ requestAuthCode 获取 auth_code
→ POST /api/auth/qr/dingtalk-confirm.php
→ SSO 用 /user/getuserinfo 返回的 UserId 确认挑战
→ 建立 Web Session 并跳转 redirect
浏览器绘制示例
<script src="/assets/js/qrcode.min.js"></script>
<div id="dingtalk-login"></div>
<script>
const challenge = await fetch('/api/auth/qr/create.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ redirect: '/' })
}).then(response => response.json());
QRCode.toCanvas(challenge.qr_url, { width: 220 }, (error, canvas) => {
if (error) throw error;
document.getElementById('dingtalk-login').appendChild(canvas);
});
</script>
辅助端点:
JSAPI 失败时,手机页跳转到 dingtalk_goto,再由钉钉回调 /oauth/dingtalk-callback.php 完成 unionId 身份确认。
当钉钉未启用时,系统可回退到 /qr/scan.php?token=... 手机确认页面。
钉钉 PC 客户端一键登录
在钉钉 PC 客户端内,可调用 dd.runtime.permission.requestAuthCode 获取 auth_code,再请求 /api/auth/dingtalk/pc-login.php。该接口与浏览器纯二维码回调不是同一条授权路径。
Qt QWebEngineView 设备扫码流程
没有业务后端的 Qt 程序使用独立设备流。Qt 集成 webenginewidgets 后,将 verification_url 加载到 QWebEngineView;SSO 页面内部使用钉钉官方 DDLogin,Qt 不接收钉钉 HTTPS 回调。
Qt POST /api/auth/device/create.php(仅 client_id=app002_client)
→ QWebEngineView::setUrl(verification_url)
→ SSO 页面加载 DDLogin iframe 展示钉钉原生二维码
→ 钉钉回调 SSO /oauth/dingtalk-callback.php
→ Qt QNetworkAccessManager 按 poll_interval 轮询 /api/auth/device/status.php
→ 首次 confirmed 返回 access_token,Qt 安全保存并停止轮询
Qt 请求示例
POST /api/auth/device/create.php
{"client_id":"app002_client"}
QWebEngineView::setUrl(QUrl(QString::fromUtf8(response["verification_url"])))
POST /api/auth/device/status.php
{"device_code":"DEVICE_CODE","poll_key":"POLL_KEY"}
安全边界:client_secret、钉钉 AppSecret 和回调处理只在 SSO 服务端;poll_key 只放在 Qt 的内存/安全存储中,不放进 verification_url、HTML 或钉钉二维码。
Qt QLabel 纯二维码流程
推荐的桌面接入不需要 WebEngine。创建响应中的 qr_content 等于短期 verification_url,qr_image_url 返回由 SSO 本地生成的 PNG;Qt 下载图片后通过 QPixmap 加载并显示到 QLabel。
POST /api/auth/device/create.php {"client_id":"app002_client"}
→ QNetworkAccessManager 下载 qr_image_url
→ QPixmap::loadFromData(pngBytes)
→ QLabel::setPixmap(pixmap)
→ 用户使用钉钉扫描二维码
→ SSO 手机页调用 dd.runtime.permission.requestAuthCode
CorpId: ding661834ba2dd5b101a1320dcb25e91351
→ POST /api/auth/device/dingtalk-confirm.php
→ SSO 用 /user/getuserinfo 换取已绑定 UserId
→ Qt POST /api/auth/device/status.php 领取 Token
二维码边界:qr_content、qr_image_url 和 PNG 只包含短期 device_code;poll_key 仅保存在 Qt 内存并通过状态接口的 JSON Body 发送。/device/qr-image.php 不接受任意 URL,过期挑战返回 404。
钉钉应用的 JSAPI 安全域名必须包含 sso.xahrf.cn。手机确认成功后页面显示“登录成功”并尝试自动关闭;官方 OAuth 仍作为 JSAPI 失败时的兼容回退。
扫码状态与三种模式
| 模式 | 创建凭证 | 轮询凭证 | 确认结果 |
web | 浏览器 Session,不传 client_id | 同一 Session + qr_token | 建立 Web Session,返回 redirect |
api | client_id + client_secret | qr_token + poll_key | 首次领取返回 access_token + user |
device | 公开 client_id | device_code + poll_key | 首次领取返回 access_token + user |
| status | 说明 |
pending | 挑战已创建,等待扫码。 |
scanned | 已扫码,等待确认。 |
confirmed | 身份确认完成;根据模式建立 Session 或签发 Token。 |
failed | 钉钉授权或身份绑定失败,响应可包含具体 message。 |
cancelled | 挑战被新挑战替换或主动取消。 |
expired | 超过 120 秒有效期,需要重新创建。 |
客户端应按响应中的 poll_interval=2 秒轮询。API 与 device 模式每个挑战只签发一个 Access Token;device 模式在挑战有效期内支持使用同一凭证幂等重取,收到后仍应立即安全保存。
组织 API 权限范围
| scope | 身份 | 可见范围 |
all | super_admin Bearer Token | 全部启用部门与 active employee 成员。 |
department | 普通用户 Bearer Token | 按用户角色、所属部门和下级部门计算。 |
service | client_id + client_secret | 全部启用部门;当前实现同样返回成员数组。 |
当前契约:users 始终是数组,没有成员时返回 [],不会返回 null。
响应与错误格式
REST JSON API
{
"ok": false,
"error": "invalid_token",
"message": "无效的访问令牌或令牌已过期"
}
常见 HTTP 状态为 400 参数错误、401 凭证错误、403 权限不足、404 资源不存在、405 方法错误、410 二维码过期和 502 钉钉上游错误。
OAuth2 API
{"error":"invalid_grant"}
OAuth2 Token 和 Userinfo 端点不添加 ok 字段;调用方应同时检查 HTTP 状态与标准 error。
完整调用示例
密码登录并读取用户资料
curl -s -X POST 'https://sso.xahrf.cn/api/auth/login.php' \
-H 'Content-Type: application/json' \
-d '{"username":"zhangsan","password":"USER_PASSWORD","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET"}'
curl -s 'https://sso.xahrf.cn/api/auth/userinfo.php' \
-H 'Authorization: Bearer ACCESS_TOKEN'
API 模式扫码登录
# 创建挑战并保存 qr_token、poll_key
curl -s -X POST 'https://sso.xahrf.cn/api/auth/qr/create.php' \
-H 'Content-Type: application/json' \
-d '{"client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET"}'
# 按 poll_interval 轮询
curl -s 'https://sso.xahrf.cn/api/auth/qr/status.php?qr_token=QR_TOKEN&poll_key=POLL_KEY'
接入检查清单
- 已注册应用并安全保存 client_id / client_secret。
- OAuth redirect_uri 与后台白名单完全一致,并验证随机 state。
- 业务系统后端负责交换授权码,不向浏览器暴露 client_secret。
- 优先使用 Authorization Bearer Header 传递 Access Token。
- Web 扫码创建和轮询使用同一个浏览器 Session。
- API 扫码不泄露 poll_key,并在首次 confirmed 响应后保存 Token。
- 钉钉应用的 JSAPI 安全域名已配置为 sso.xahrf.cn。
- 调用方同时处理 HTTP 状态和响应中的 error / message。