本文说明开放应用·网站如何通过标准 OIDC Authorization Code + PKCE 接入小应账号快捷登录。
如果你更熟悉 OAuth,可以这样理解:OAuth 2.0 负责安全地把用户带到小应授权页、再把一次性 code 换成 token;OIDC 在这个流程上增加了 id_token,让第三方服务端可以验证用户身份并完成登录。本文涉及的 OIDC 登录流程,本质上就是“OAuth 2.0 Authorization Code + PKCE + 可验签的用户身份凭据”。
相关文档:
接入要求
- 第三方网站必须使用已登记的
redirect_uri接收登录结果。 client_secret必须保存在服务端密钥系统或环境变量中。- 回调接口必须校验
state。 - token 换取请求必须携带原始
code_verifier。 - 服务端必须验证
id_token签名与关键 claims。 - 本地账号绑定必须使用
iss + sub。
第三方只需要按标准 OpenID Connect(OIDC)流程把用户重定向到小应 authorization_endpoint。用户完成登录与授权后,小应会跳回第三方登记的 redirect_uri。
小应账号快捷登录的 access_token 只用于 /userinfo 端点。第三方不要把它当作其他接口访问凭据,也不要把它当作自己的登录 session。
登录服务端点(OIDC)
优先从 discovery 文档读取端点:
GET https://api.xiaoying.life/oidc/.well-known/openid-configuration从 discovery 中至少读取并使用以下字段:
| Discovery 字段 | 用途 |
|---|---|
issuer | 验证 id_token.iss |
authorization_endpoint | 发起授权 |
token_endpoint | 用 code 换 token |
jwks_uri | 获取验签公钥 |
userinfo_endpoint | 可选,读取用户展示资料 |
生产默认端点如下:
| 端点 | 方法 | 用途 |
|---|---|---|
https://api.xiaoying.life/oidc/auth | GET | 发起授权 |
https://api.xiaoying.life/oidc/token | POST | 用 code 换 token |
https://api.xiaoying.life/oidc/jwks | GET | 获取验签公钥 |
https://api.xiaoying.life/oidc/userinfo | GET/POST | 读取用户展示资料 |
协议参数:
| 能力 | 取值 |
|---|---|
response_type | code |
grant_type | authorization_code |
scope | openid profile |
code_challenge_method | S256 |
token_endpoint_auth_method | client_secret_basic |
id_token 签名算法 | EdDSA,通过 discovery / JWT header / JWKS kid 选择公钥 |
小应账号快捷登录面向单次快速授权和账号绑定。token 响应不包含 refresh_token,也不支持 offline_access scope。第三方完成 id_token 验证后,应使用自己的本地 session 维持用户登录。
登录流程
1. 生成登录请求
第三方服务端生成并保存以下临时状态:
| 字段 | 要求 |
|---|---|
state | 高熵随机值,防 CSRF,回调时必须校验 |
nonce | 高熵随机值,防 id_token 重放,验签后必须校验 |
code_verifier | PKCE 原始随机值,服务端临时保存 |
code_challenge | BASE64URL(SHA256(code_verifier)) |
生成规则:
state、nonce至少 128 bit 熵。code_verifier使用 43-128 个 PKCE 合法字符,推荐 32 字节随机数的 base64url 无填充表示。code_challenge使用 SHA-256 后的 base64url 无填充表示。- 登录 attempt 应保存在服务端 session、Redis、数据库或加密 HttpOnly Cookie 中,TTL 建议 5-10 分钟。
把用户重定向到小应授权端点:
GET https://api.xiaoying.life/oidc/auth
?response_type=code
&client_id=xyc_...
&redirect_uri=https%3A%2F%2Fthird.example%2Fauth%2Fxiaoying%2Fcallback
&scope=openid%20profile
&state=<random_state>
&nonce=<random_nonce>
&code_challenge=<pkce_challenge>
&code_challenge_method=S256WebView 中可以通过普通页面跳转发起授权:
window.location.href = authorizationUrl2. 处理回调
小应会跳转回第三方注册的 redirect_uri。
成功示例:
GET https://third.example/auth/xiaoying/callback?code=...&state=...错误示例:
GET https://third.example/auth/xiaoying/callback?error=access_denied&error_description=...&state=...回调处理要求:
- 必须校验
state与发起登录时保存的值一致。 - 如果有
error,结束本次登录尝试并展示失败或重新登录入口。 - 如果有
code,用服务端保存的code_verifier换 token。 code只能使用一次。- 无论成功、失败还是取消,当前登录 attempt 都应删除或标记为已消费。
3. 换 token
请求:
POST /oidc/token HTTP/1.1
Host: api.xiaoying.life
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=<authorization_code>&
redirect_uri=https%3A%2F%2Fthird.example%2Fauth%2Fxiaoying%2Fcallback&
code_verifier=<original_code_verifier>响应示例:
{
"access_token": "opaque-or-jwt-access-token",
"expires_in": 3600,
"id_token": "eyJ...",
"scope": "openid profile",
"token_type": "Bearer"
}实现要求:
Authorization使用 HTTP Basic,值为Basic+base64(client_id + ":" + client_secret)。- 请求体必须是
application/x-www-form-urlencoded,不是 JSON。 redirect_uri必须与发起授权请求和小应登记值完全一致。code_verifier必须是发起授权时保存的原始值,不是code_challenge。- token endpoint 错误响应也使用 OAuth 标准错误字段,接入方不要从错误响应中创建登录态。
4. 验证 id_token
第三方服务端必须验证 id_token:
| 检查项 | 要求 |
|---|---|
| 签名 | 使用 jwks_uri 对应公钥验证 |
iss | 必须等于 https://api.xiaoying.life/oidc |
aud | 必须等于自己的 client_id |
exp | 未过期 |
iat | 合理时间窗口内 |
nonce | 必须等于发起登录时保存的 nonce |
sub | 存在且非空 |
alg / kid | 使用 JWT header 中的 kid 从 jwks_uri 匹配 key;不要把开发环境公钥写死到代码里 |
关键 claims:
| Claim | 说明 |
|---|---|
iss | 小应 issuer |
aud | 第三方 client_id |
sub | 小应对该第三方主体稳定分配的用户标识 |
exp / iat | 标准时间字段 |
nonce | 授权请求携带时必须回显 |
注意:
id_token是登录凭据,验签通过前不要信任其中任何字段。access_token不是登录凭据;本地登录态应基于已验证的id_token。nickname、picture是展示资料,不是身份主键。- JWKS 可以按
Cache-Control和kid缓存;遇到未知kid时重新拉取 discovery/JWKS。
5. 建立第三方本地登录态
第三方本地账号绑定规则:
- 唯一键使用
iss + sub。 - 同一小应开发者主体下多个 client 对同一小应用户会得到相同
sub。 - 不同小应开发者主体对同一小应用户会得到不同
sub。 - 保存完整
iss + sub,避免多身份源场景下冲突。
参考本地表字段:
| 字段 | 说明 |
|---|---|
provider | 固定 xiaoying |
issuer | https://api.xiaoying.life/oidc |
subject | id_token 的 sub |
localUserId | 第三方自己的用户 ID |
createdAt / updatedAt | 绑定时间 |
用户信息
openid profile 下返回以下用户信息:
{
"sub": "xysub_...",
"nickname": "小应用户",
"picture": "https://..."
}使用规则:
sub是唯一身份标识。nickname和picture用于展示,可以变化,可以为空。
第三方可以从 /userinfo 获取可展示资料。若调用 /userinfo,使用 token 响应中的 access_token:
GET /oidc/userinfo HTTP/1.1
Host: api.xiaoying.life
Authorization: Bearer <access_token>处理要求:
/userinfo返回的sub必须与已验证id_token.sub一致。/userinfo失败不应影响已经验签成功的登录主流程;可以只创建账号,稍后再补展示资料。- 不要长期保存小应账号快捷登录的
access_token,除非你的系统确实需要在短时间内重新拉取/userinfo。
安全检查清单
第三方上线前必须确认:
client_secret只存在服务端密钥系统或环境变量中。- 回调接口校验
state。 - token 换取请求携带原始
code_verifier。 id_token已验签。- 已校验
iss、aud、exp、nonce。 - 已校验
/userinfo.sub === id_token.sub,如果调用了/userinfo。 - 本地账号绑定使用
iss + sub。 - 登录 attempt 有 TTL,成功或失败后会删除。
redirect_uri与小应官方登记值完全一致。access_token只用于/oidc/userinfo。- 没有把小应
access_token当作第三方自己的登录 session。 - 没有把
nickname、picture当作用户唯一标识。
常见错误
| 错误或现象 | 原因 | 修复 |
|---|---|---|
invalid_client | client_id / client_secret 错误,或 Basic Auth 格式错误 | 检查小应官方交付的客户端配置和 Authorization: Basic |
invalid_grant | code 已过期、已使用、redirect_uri 不一致或 code_verifier 错误 | 重新发起登录,检查 PKCE 和 redirect URI |
invalid_request | 缺少必需参数 | 检查 authorization request |
access_denied | 用户取消授权 | 不创建登录态,允许用户重新登录 |
invalid_scope | scope 超出允许范围 | 使用 openid profile |
redirect_uri mismatch | 回调地址与登记值不完全一致 | 使用小应官方登记的精确 URI |
unsupported_response_type | 使用了非 code flow | 只使用 Authorization Code |
invalid_request + PKCE 相关描述 | 缺少 code_challenge 或 code_challenge_method 不正确 | 使用 S256,并保存原始 code_verifier |
jwks_no_matching_key / kid 不匹配 | 本地 JWKS 缓存过旧或验签库未重新拉取 | 清理 JWKS 缓存并重新读取 discovery / JWKS |
discovery issuer 与本地配置不一致 | 第三方本地配置的 issuer 不是 discovery 返回的 issuer | 以小应官方交付的 issuer 为准;生产应为 https://api.xiaoying.life/oidc |
state 无效或找不到登录 attempt | 登录 attempt 丢失、过期,或回调不是当前登录请求发起的 | 不创建登录态,重新发起登录;检查 session/cookie/Redis 是否能跨回调保持状态 |
nonce 不一致 | id_token 不属于当前登录 attempt,或本地保存的 nonce 丢失 | 不创建登录态,重新发起登录;检查 attempt 存储 |
token endpoint 返回 server_error | 服务端暂时无法完成 token 签发或客户端配置异常 | 不创建登录态;记录请求时间、client_id、redirect_uri 和错误响应,联系小应官方技术支持 |
测试用例
第三方接入完成后至少测试:
- 首次使用小应登录,能创建或绑定第三方本地账号。
- 再次登录同一小应账号,命中同一个第三方本地账号。
- 回调
state被篡改时登录失败。 nonce不一致时登录失败。code_verifier错误时 token 换取失败。- 用户取消授权时不创建第三方登录态。
client_secret不出现在浏览器 bundle、日志、URL、前端配置中。iss + sub唯一约束生效。/userinfo.sub与id_token.sub不一致时登录失败。- 重复使用同一个 authorization code 时第二次 token 换取失败。
- client secret 错误时 token 换取失败,且不会创建本地登录态。
- JWKS 缓存刷新后仍能验证新
kid签发的id_token。 - 在小应 App WebView 内打开第三方页面,可以完整完成登录。
- 在小应 App WebView 外打开第三方页面时,页面能给出合适提示或隐藏小应登录入口。
接入完成标准
第三方交付前应能证明:
- 可以从 discovery 获取端点和
jwks_uri。 - 可以发起
response_type=code、scope=openid profile、code_challenge_method=S256的授权请求。 - 在小应 App WebView 内完成授权后,回调
state校验通过。 - 服务端使用
client_secret_basic和原始code_verifier成功换取 token。 - 服务端使用
jwks_uri验证id_token签名,并校验iss、aud、exp、nonce、sub。 - 本地账号表以
issuer + subject建唯一约束。 - 可选调用
/userinfo时,会校验返回的sub。 - 浏览器、前端 bundle、日志和错误上报中都没有
client_secret、authorization code、id_token或小应账号快捷登录access_token明文泄漏。