跳转到正文
图奕科技 开发文档 平台登录
接入指南 / 自定义网页登录下载本章 Markdown ↓

本实例 Issuer https://platform.too1.net

官网自定义登录(不跳页)

短信发送须先完成滑块验证,并在原发送请求中携带一次性 captcha_token;发布前同步升级相关客户端。

这是图奕自有产品的第一方认证扩展,不是 OAuth 密码 Grant,也不是可直接用于第三方网站的 SDK。仅当产品前端及发布供应链可信时启用;前端脚本能读取用户输入,CORS 不能防止同源 XSS。

选择接入方式

方式 适用场景 凭据边界 浏览器共享登录
标准 OIDC 跳转 需要统一登录、多个站点复用登录态 统一页面收密码;产品后端持 Secret 支持平台中央会话
本章自定义 Web 官网内二维码、短信或密码表单 浏览器直连平台;产品后端换码 不创建平台 Cookie,不承诺跨站静默登录
Native 直接认证 自有桌面客户端原生界面 原生 HTTP,无 Secret,PKCE verifier 留客户端 不创建浏览器共享会话

自定义网站可以自由设计 UI,但不能省略协议确认、手机号绑定、错误恢复或后端验签。产品不接收公众号事件:微信仍只回调平台 /callbacks/wechat/official-account。

产品级“强制绑定微信”策略同样适用于本接口。已绑定用户可以继续使用短信或密码表单;未绑定用户在 /complete 收到 409 wechat_binding_required 后,前端应取消旧事务并让产品后端创建新的微信事务。事务凭据不能跨方式复用,产品也不能把短信验证成功提前当成登录成功。

开通与跨域

先部署包含 012_custom_auth.sql 的版本。在 OIDC 客户端 → 编辑配置 中,为 Web Client 开启“自定义网页登录”,登记精确 Origin,例如 https://www.example.com,按需开启密码管理。需要最近十分钟二次验证、原因和 Client ID 确认。新权限默认关闭;修改配置会取消未完成的直接认证事务。

Origin 由协议、主机和端口组成,没有末尾斜杠或路径。不支持 *、后缀匹配或 null。HTTPS 必需;只有开发 Client 允许 http://localhost:端口 或回环 IP。官网 https://www.example.com 与 API https://api.example.com 是两个 Origin;本章的产品换码接口必须通过官网同源路由提供。

请登记浏览器 location.origin 的规范值:主机小写,HTTPS 默认 443(HTTP 默认 80)不写端口,非默认端口范围 1–65535 且无前导零。

只有 /api/v1/web-auth/transactions/{id}/… 和 /api/v1/web-auth/password-transactions/{id}/… 有受限 CORS。预检允许 GET、POST 和 Authorization, Content-Type,有效期 60 秒。响应带精确 Access-Control-Allow-Origin、Vary: Origin,不带 Allow-Credentials。浏览器必须 credentials: 'omit';不传平台 Cookie。创建事务只接受产品后端 Basic 认证,不开放浏览器 CORS。/oauth2/token、管理后台、短信业务及支付 API 不因此获得跨域权限。预检通过后,每个实际请求仍检查事务所属 Origin 和 Client 权限。

创建事务(产品后端)

POST /api/v1/web-auth/transactions,Content-Type: application/json,Authorization: Basic base64(client_id:client_secret);仅 client_secret_basic。请求体不接受未知字段,最大 64 KiB。不传 Origin、Cookie 或 URL 参数。

字段 类型 / 必填 约束
client_id string / 是 已启用自定义登录的 Web Client,最长 128,与 Basic ID 相同
browser_origin string / 是 该 Client 已登记的精确 Origin
method string / 是 wechat、sms、password
redirect_uri string / 是 已登记的登录回调;仅用于绑定,不会执行导航
scope string / 是 空格分隔,包含 openid;可选 profile、phone、offline_access,必须已授权
state、nonce string / 是 分别独立随机,22–128 字符 base64url
code_challenge string / 是 SHA256(verifier) 的无填充 base64url,固定 43 字符
code_challenge_method string / 是 固定 S256

不要复制“随机字符串”等文字作为参数。Go 后端生成方式:

random := func() (string, error) {
    b := make([]byte, 32)
    if _, err := rand.Read(b); err != nil { return "", err }
    return base64.RawURLEncoding.EncodeToString(b), nil
}
verifier, err := random() // 每个错误必须中止,不使用默认值
if err != nil { return err }
state, err := random()
if err != nil { return err }
nonce, err := random()
if err != nil { return err }
hash := sha256.Sum256([]byte(verifier))
challenge := base64.RawURLEncoding.EncodeToString(hash[:])

verifier、state、nonce 保存于绑定当前浏览器业务会话的服务端临时记录,五分钟过期。浏览器不获得 verifier 或 Web Secret。创建请求返回 HTTP 201,外层格式与 Native 相同:

{"data":{"transaction_id":"73688356-98eb-45fa-9230-b3ab939a7aca","transaction_token":"S1DKP9IMhrGNXQ6bcDpkzh_FSPjxQt4IskbgTsX2mGA","status":"waiting","expires_in":300,"expires_at":"2026-09-03T09:05:00Z","poll_interval":2,"legal":{"version":"2026-09","terms_url":"https://platform.too1.net/legal/terms","privacy_url":"https://platform.too1.net/legal/privacy"}}}

示例响应为虚构数据,不能作为凭据使用。只有微信事务附带 qr_url。五分钟固定到期,轮询不续期。后端将此结果返回原浏览器;事务 Token 仅保留浏览器内存,不入 URL、localStorage、日志或分析埋点。

浏览器直连接口

令 base=/api/v1/web-auth/transactions/{id}。所有请求带 Authorization: WebAuth <transaction_token> 和浏览器自动生成的 Origin。POST 为 JSON;不再提交可变 Client、Scope 或回调。成功格式 {"data":…};失败格式见错误表。

方法 / 路径 请求 JSON 成功响应 / 状态
GET base 无 200 data.status, expires_at, poll_interval, legal, phone_masked;状态 waiting/phone_required/ready/completed/cancelled/error
GET base/qr 无 200 image/png 或 image/jpeg,只限微信事务;最大 512 KiB
POST base/sms/send {"phone_e164":"+8613800138000"} 201 {"data":{"status":"accepted","message":"验证码已发送","retry_after":60}}
POST base/sms/verify {"phone_e164":"+8613800138000","code":"123456"} 200 {"data":{"status":"ready"}}
POST base/password {"phone_e164":"+8613800138000","password":"仅作字段说明"} 200 {"data":{"status":"ready"}}
POST base/complete {"accept_terms":true,"legal_version":"返回的当前版本"} 200 一次性授权码,见下节
POST base/cancel {} 200 {"data":{"status":"cancelled"}}

短信只接受大陆 E.164;六位数字验证码。新微信身份必须扫码后验证手机号;既有 UnionID 直接进入 ready。冲突返回 409,不自动合并。手机号/验证码/密码输入直接提交上述平台 API,禁止产品后端中转。

const res = await fetch(issuer + '/api/v1/web-auth/transactions/' + tx.id + '/sms/verify', {
  method: 'POST', credentials: 'omit', cache: 'no-store',
  headers: {'Content-Type': 'application/json', Authorization: 'WebAuth ' + tx.token},
  body: JSON.stringify({phone_e164: phoneInput.value, code: codeInput.value}),
  signal: AbortSignal.timeout(12000)
});
// 读取 HTTP 状态和 JSON error;不要对短信/完成接口自动重试。

二维码不能直接使用无认证的 <img src>:先带 WebAuth 头 fetch 图片,创建 URL.createObjectURL(blob) 再显示;关闭弹窗时 URL.revokeObjectURL。每两秒串行查询状态,响应完成后才安排下一轮;关闭、切换方式、到期或结束后停止。

短信渠道超时返回 202:

{"data":{"status":"unknown","message":"发送结果未知,请勿自动重发;至少等待 60 秒后手动操作","retry_after":60}}

unknown 不代表受理成功,挑战未激活,不自动重发。HTTP 请求取消也不等于服务端未执行。密码/验证码字段在提交后清空,不能输出到日志。

完成与产品业务会话

/complete 只有 ready 且协议当前时成功。返回:

{"data":{"status":"completed","code":"UozIeHZLdbyLGiqYmDmczA50wlZVEKj16ka7dbBgPLs","state":"创建时的state","redirect_uri":"https://www.example.com/auth/tuyi/callback","issuer":"https://platform.too1.net","expires_in":60,"token_endpoint":"https://platform.too1.net/oauth2/token"}}

授权码只领取一次;响应丢失后不能重新领取,请重建事务。浏览器通过官网 同源 POST + CSRF 发送 code/state/issuer/redirect_uri 给产品后端。后端从自己的会话取原记录,精确校验四项绑定(code 交由 Token 端点校验),原子消费临时记录,再用 Web Basic 和原 verifier 换 Token:

POST /oauth2/token
Authorization: Basic <仅产品后端持有>
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<一次性code>&redirect_uri=<URL编码回调>&code_verifier=<原verifier>

不要信任浏览器传回的 token_endpoint 或临时替换 Issuer;服务端固定配置平台域名。用正式 OIDC 验证器校验 RS256/JWKS、issuer、audience、exp、nonce,再请求 UserInfo 并检查 sub 一致。创建自己的业务会话,长期 Token 留服务端。刷新必须串行;退出先撤销 Refresh Token 并清理本地会话。参见 OIDC。

错误、限流及重试

{"error":{"code":"origin_not_allowed","message":"此网站未获准使用自定义登录","request_id":"诊断请求编号"}}
HTTP code 处理
400 / 415 invalid_request / web_request_required / json_required 修正 JSON、禁止 Cookie/查询凭据,不重试原请求
401 invalid_client / invalid_transaction_credential 检查后端 Basic 或本次事务头
403 origin_not_allowed / preflight_not_allowed / native_client_not_allowed 检查 Web 开关、精确 Origin、回调、Scope;native_client_not_allowed 为兼容共用错误码
409 transaction_unavailable / identity_conflict / wechat_binding_required 重新开始;身份冲突联系管理员;要求绑定微信时改建微信扫码事务
422 invalid_code / invalid_credentials / phone_unsupported / terms_changed 用户修正;协议变化需重新读取并主动同意
429 rate_limited / sms_limited 遵守 Retry-After,不循环重试
503 authentication_busy 密码校验容量暂满,Retry-After 为 1 秒;用户稍后手动重试或换方式,不代表密码错误
502 / 503 sms_failed / sms_activation_failed / storage_unavailable / channel_unavailable / login_paused 显示安全提示与 request_id;保留“已执行但结果未知”的可能

Web 创建:Basic 校验前每后端来源 IP 1,200 次/分钟,校验成功后每 Web Client 600 次/分钟(登录与密码管理共用);不套用 Native 的 30 次/5 分钟单终端限制,避免把同一产品后端出口的所有用户误当作一个用户。轮询每事务 180 次/5 分钟。

密码校验与统一页面、Native 共用三层预算:每 IP 总请求 1,200 次/分钟、每账号失败 10 次/15 分钟、每 IP 失败 300 次/15 分钟。成功不消耗失败额度,也不清空其他失败;账号预算跨 IP 生效。Redis 原子预留在途名额防止并发绕过,租约 30 秒,进程中断后自动回收。故障、取消和容量繁忙不当作错误密码;已经判定错误的尝试即使随后断线仍计入失败。单进程最多 4 个同时执行的密码校验,最多排队 500ms,超出返回 503 authentication_busy。Redis 校验/确认失败均拒绝认证。429 的 Retry-After 是最小等待建议,不保证下次成功;失败保护最长到当前 15 分钟窗口结束,禁止自动循环重试。

认证短信限额本轮未改:每手机号 60 秒一次、每来源 IP 每小时 10 次、国家前缀每分钟 100 次;这些是认证风控,不是业务模板短信额度。事务接口不使用业务 API 的 Idempotency-Key:凭据、状态机、一次性完成提供独立重放控制。

可运行示例与验收

仓库 examples/custom-web 是 Go 后端 + 原生 JavaScript,不需要 Node 构建。运行说明见该目录 README。涵盖扫码/短信/密码、找回/设置密码、业务会话、刷新、退出。此示例使用内存会话,生产替换为 Redis 等共享存储并消费撤销事件;不能把示例直接当作已验收的 WYC 改造。

下载 认证 OpenAPI,查看 架构与时序图。安全依据:OAuth 2.0 Security BCP、Fetch CORS 协议。