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 数量。

分组方法路径认证
OAuth2 授权码流程 GET|POST /oauth/authorize.php client_id + SSO Session
OAuth2 授权码流程 POST /oauth/token.php client_id + client_secret
OAuth2 授权码流程 GET|POST /oauth/userinfo.php Bearer Token
REST 认证 API POST /api/auth/login.php 应用凭证 + 用户密码
REST 认证 API GET|POST /api/auth/userinfo.php Bearer Token
REST 认证 API POST /api/auth/logout.php Bearer Token
扫码登录 API POST /api/auth/qr/create.php Web: Session;API: client_id + client_secret
扫码登录 API POST /api/auth/qr/dingtalk-confirm.php 钉钉 JSAPI auth_code + qr_token
扫码登录 API POST /api/auth/qr/scan.php qr_token
扫码登录 API POST /api/auth/qr/confirm.php Bearer Token、SSO Session 或用户名密码
扫码登录 API GET|POST /api/auth/qr/status.php Web: Session + qr_token;API: qr_token + poll_key
扫码登录 API POST /api/auth/device/create.php 公开 client_id
扫码登录 API POST /api/auth/device/status.php device_code + poll_key
扫码登录 API POST /api/auth/device/dingtalk-confirm.php 钉钉 JSAPI auth_code + device_code
扫码登录 API POST /api/auth/dingtalk/pc-login.php 钉钉 authCode + qr_token
组织 API GET /api/org/departments.php Bearer Token 或 client_id + client_secret
微信小程序认证与通知 POST /api/auth/wechat/login.php 微信 login_code
微信小程序认证与通知 POST /api/auth/wechat/mini-login.php 白名单 client_id + 微信 login_code
微信小程序认证与通知 POST /api/auth/wechat/mini-refresh.php Authorization: Bearer + 固定 client_id
微信小程序认证与通知 POST /api/auth/wechat/mini-bind.php 白名单 client_id + 微信 login_code + 用户密码
微信小程序认证与通知 POST /api/auth/wechat/mini-phone-bind.php 白名单 client_id + 微信 login_code + phone_code
微信小程序认证与通知 POST /api/auth/wechat/bind.php Bearer Token 或 SSO Session
微信小程序认证与通知 POST /api/auth/wechat/qr/confirm.php 微信 login_code
微信小程序认证与通知 POST /api/auth/web-qr/create.php 匿名浏览器 Session
微信小程序认证与通知 GET /api/auth/web-qr/image.php 创建挑战的匿名浏览器 Session
微信小程序认证与通知 GET /api/auth/web-qr/status.php 创建挑战的匿名浏览器 Session
微信小程序认证与通知 POST /api/auth/wechat/mini-web-login.php Authorization: Bearer;固定 app005_client
微信小程序认证与通知 POST /api/auth/wechat/salary-security/status.php Authorization: Bearer;固定 app005_client
微信小程序认证与通知 POST /api/auth/wechat/salary-security/set.php Authorization: Bearer;固定 app005_client
微信小程序认证与通知 POST /api/auth/wechat/salary-security/verify.php Authorization: Bearer;固定 app005_client
微信小程序认证与通知 POST /api/auth/wechat/unbind.php SSO Session
微信小程序认证与通知 POST /api/notifications/create.php SSO Session
微信小程序认证与通知 GET /api/notifications/index.php SSO Session
微信小程序认证与通知 POST /api/notifications/retry.php SSO Session
格式约定: /api/** 成功响应包含 ok=true,失败响应包含 ok=false、error 和 message。 OAuth2 端点遵循标准 OAuth 响应格式,不增加 ok 字段。

接入准备

  1. 注册应用:管理员在应用管理中创建 OAuth2 应用,保存系统生成的 client_id 和 client_secret。
  2. 登记回调:Web SSO 必须配置精确的 HTTPS redirect_uri 白名单,协议、域名、端口和路径均参与匹配。
  3. 分配权限:需要限制访问时,将应用分配给允许登录的角色;未授权用户会收到 access_denied。
  4. 保护凭证: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

解除当前用户微信绑定。

认证方式SSO Session
请求格式JSON

成功响应

HTTP 200:绑定已解除。

{"ok":true,"unbound":true}

错误响应

error / 响应HTTP说明
unauthorized 401 未登录。
注意:
  • 不会删除 SSO 用户。
POST /api/notifications/create.php

创建站内及微信通知。

认证方式SSO Session
请求格式JSON

请求参数

参数位置类型必填说明
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

查询当前用户通知。

认证方式SSO Session
请求格式JSON

请求参数

参数位置类型必填说明
page Query integer 否 页码。

成功响应

HTTP 200:返回通知列表。

{"ok":true,"items":[]}

错误响应

error / 响应HTTP说明
unauthorized 401 未登录。
注意:
  • 按当前会话用户隔离数据。
POST /api/notifications/retry.php

重试微信通知投递。

认证方式SSO Session
请求格式JSON

请求参数

参数位置类型必填说明
delivery_id Body integer 是 投递记录 ID。

成功响应

HTTP 200:投递成功或已记录失败。

{"ok":true,"status":"sent"}

错误响应

error / 响应HTTP说明
not_found 400 投递记录不存在。
注意:
  • 重试会递增 attempts 并保存错误信息。

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
apiclient_id + client_secretqr_token + poll_key首次领取返回 access_token + user
device公开 client_iddevice_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身份可见范围
allsuper_admin Bearer Token全部启用部门与 active employee 成员。
department普通用户 Bearer Token按用户角色、所属部门和下级部门计算。
serviceclient_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。