微信小程序登录接入指南(ExoMind 等下游小程序端)
读者:小程序前端开发者。auth 侧实现见
TENANT_UNIFIED_IDP§7;本文只讲你需要的接入契约。 生产 base:https://auth.ai-as.cc。当前已配置的小程序:wx5732c2ddcea11374(AI浪随身工具)。
1. 核心概念(30 秒)
- 静默登录:
wx.login()拿 code,换 auth 的 JWT——用户零输入零点击。 - 首登不建号:auth 第一次见到这个微信身份时不会自动建账号,返回
unbound+ 一张 5 分钟票据(wx_token),由用户选择「快速开通」还是「绑定已有账号」——防止老用户扫码进空号。 - 身份键 = unionid:同一微信用户在小程序/公众号/将来网页扫码下是同一个账号(零绑定操作)。
- token 同源:小程序拿到的 JWT 与网页登录、CLI 的 token 同构——
sub是统一 user_id,数据天然同源(ExoMind 按 sub 组织数据,小程序端看到的就是用户在网页端的那份)。
2. 全流程时序
小程序启动
└─ wx.login() → code
└─ POST /api/auth/wechat/login {code, appid}
├─ status:"bound" → 拿到 token 对,直接进入应用(99% 的日常路径)
└─ status:"unbound" + wx_token(首次登录,5 分钟有效)
├─ 新用户:POST /api/auth/wechat/register {wx_token} → 建号 + token 对
└─ 老用户二选一:
├─ 密码绑定:POST /api/auth/wechat/bind {wx_token, username, password[, mfa_code]}
└─ 确认码绑定(推荐,免输密码):
小程序 POST /api/auth/wechat/bind-code {wx_token} → 6 位码
→ 用户在电脑上已登录的 auth 管理台·个人中心「关联小程序」输码
→ 小程序轮询 POST /api/auth/wechat/poll-bind {code}(2s 间隔)
├─ pending → 继续轮询
├─ bound + token 对 → 进入应用
└─ expired → 提示重来
之后每次启动:wx.login() → login → bound → 直接用。除首次外全程无感。
3. API 参考
所有端点均为 POST、Content-Type: application/json、无需鉴权头(登录前没有 token)。
3.1 POST /api/auth/wechat/login — 静默登录第一步
// 请求
{ "code": "wx.login() 拿到的 code(一次性,用完即废)",
"appid": "wx5732c2ddcea11374",
"audience": "client_exomind" }
audience(可选但强烈推荐):目标下游的 client_id——签出的 token aud 即该值,下游 fail-closed 校验(aud == client_id)直接通过,下游零改动。续期(refresh)全生命周期继承该 aud。校验规则:必须是存在且启用的 OAuth client,否则报 audience 无效。register/bind/bind-code/poll 同样支持该参数,同一小程序内全程传同一个值。不传则 aud = issuer(下游需将 issuer 加入 aud 白名单,见 §5)。
// 响应一:已绑定(日常路径)
{ "success": true, "data": {
"status": "bound",
"login": {
"access_token": "eyJ...", // JWT,调业务 API 用 Bearer 头
"refresh_token": "eyJ...", // 过期续期用(见 3.6)
"user": { "id": 57, "username": "...", "org_id": "default", ... },
"permissions": [...]
}
} }
// 响应二:首次登录(未绑定)
{ "success": true, "data": { "status": "unbound", "wx_token": "wxt_xxx..." } }
错误:微信登录失败(errcode=40029):invalid code(code 无效/已用,重新 wx.login)/ 微信登录未开放(appid 未配置或已停用)(appid 填错)。
3.2 POST /api/auth/wechat/register — 快速开通(新用户)
{ "wx_token": "login 返回的票据" }
成功 → {status:"registered", login:{...}}(建无邮箱无密码的微信专属账号;用户之后可在网页端个人中心补绑邮箱/设置密码)。wx_token 一次性,消费即失效。
3.3 POST /api/auth/wechat/bind — 绑定已有账号(密码方式)
{ "wx_token": "...",
"username": "已有账号的邮箱或用户名",
"password": "该账号的密码",
"mfa_code": "可选;该账号开了双因子时必填 6 位码" }
成功 → {status:"bound", login:{...}}——token 是老号的(数据都在)。密码错误不烧票据可重试(有防爆破限流)。
3.4 POST /api/auth/wechat/bind-code — 确认码绑定·发起(推荐)
{ "wx_token": "..." }
// → { "success": true, "data": { "code": "302641", "expires_in": 300 } }
小程序把 6 位码显示给用户(大字号,提示「在电脑上打开 auth.ai-as.cc → 个人中心 → 关联小程序 输入此码」)。消费 wx_token。
3.5 POST /api/auth/wechat/poll-bind — 确认码绑定·轮询
{ "code": "302641" }
// → { "data": { "status": "pending" } } // 用户还没在电脑上确认,继续轮询(建议 2s 间隔)
// → { "data": { "status": "bound", "login": {...} } } // 确认完成,票据同时消费(一次性)
// → { "data": { "status": "expired" } } // 5 分钟超时/错 5 次作废,提示重新发起
3.6 配对码 Web 登录(电脑 ↔ 小程序)
让用户在电脑网页上用微信完成登录:方案A 手输码(小程序出 8 位码,电脑输码)与 方案C 扫码(电脑出二维码,小程序扫码确认)。两条路径共用一次性配对码与同一换 token 落点(3.6.5);会话与码均存 Redis、5 分钟有效;确认幂等;同一用户重复出码自动作废旧码。
| 端点 | 调用方 | 鉴权 |
|---|---|---|
POST /api/auth/wechat/pair-code | 小程序(方案A 出码) | Bearer token |
POST /api/auth/wechat/pair/qr/start | 电脑登录页(方案C 出码) | 公开 + IP 限流 |
POST /api/auth/wechat/pair/qr/status | 电脑登录页轮询 | 公开 + IP 限流 |
POST /api/auth/wechat/pair/qr/confirm | 小程序扫码确认页 | Bearer token |
POST /api/auth/wechat/pair-exchange | 电脑登录页(两方案共用) | 公开 + IP 限流 |
3.6.1 POST /api/auth/wechat/pair-code — 出 8 位配对码(方案A)
鉴权层(匿名拒绝;Bearer 小程序 token),无请求体:
// → { "success": true, "data": { "pair_code": "30264197", "expires_in": 300 } }
小程序把 8 位码大字号显示给用户:「电脑上打开登录页 → 微信登录 → 手动输码」。
3.6.2 POST /api/auth/wechat/pair/qr/start — 生成登录二维码(方案C)
{ "app_id": "可选;多小程序时指定目标 appid" }
// → { "success": true, "data": { "session_id": "…", "qr_image": "data:image/png;base64,…",
// "expires_in": 300, "page": "pages/web-login/index" } }
qr_image 直接渲染 <img>;page 为小程序确认页路径(服务端 env WX_PAIR_QR_PAGE 可配)。微信侧出码失败 → 500(message 含微信 errcode),登录页降级到 8 位码入口。
3.6.3 POST /api/auth/wechat/pair/qr/status — 电脑轮询(方案C)
{ "session_id": "…" }
// → { "data": { "status": "pending" } } // 未确认,建议 2s 间隔
// → { "data": { "status": "approved", "pair_code": "30264197" } } // 手机已确认,拿码走 3.6.5
session 过期/不存在 → 400「二维码已过期…」(AppError::Validation,error.rs),前端重新 start 刷新二维码。
3.6.4 POST /api/auth/wechat/pair/qr/confirm — 小程序扫码确认(方案C)
鉴权层(Bearer 小程序 token)。小程序从 scene 解析 session_id(decodeURIComponent(scene) → ps=<session_id>),先走 3.1 静默登录拿 token,再调:
{ "session_id": "…" }
// → { "data": { "status": "approved", "pair_code": "30264197", "expires_in": 180 } }
// expires_in 为会话剩余秒数;重复确认幂等,返回同一票据
pending → approved 并出一次性 8 位码;400 = 会话过期,提示「请在电脑刷新后重新扫码」。确认页注意防钓鱼心智:页面标题明示目标站点,禁用转发分享。
3.6.5 POST /api/auth/wechat/pair-exchange — 配对码换 token(两方案共用落点)
{ "pair_code": "30264197" }
// → data 即标准 LoginResponse,形状与 /api/auth/login 完全一致
8 位数字校验;一次性消费(校验前即删,失败不可重试同一码)。注意:本接口不支持 audience 参数,签出的 token aud = issuer——下游校验按 §5 方式 B(aud 白名单)适配。
3.7 token 的使用与续期
- 调 ExoMind 等业务 API:
Authorization: Bearer <access_token>(ExoMind 按 token 的sub识别用户,数据与网页端同源)。 - access_token 有效期 24h;过期用 refresh_token 换新(复用标准端点):
POST /api/auth/refresh { "refresh_token": "eyJ..." }
- 存储:小程序本地安全存储;登出调
POST /api/auth/logout。
4. 小程序端参考实现
const AUTH = 'https://auth.ai-as.cc'
const APPID = 'wx5732c2ddcea11374'
async function request(path, data) {
const r = await wx.request({ url: AUTH + path, method: 'POST',
data, header: { 'Content-Type': 'application/json' } })
if (r.statusCode !== 200 || !r.data.success) throw new Error(r.data.error || '网络错误')
return r.data.data
}
async function login() {
const { code } = await wx.login()
const d = await request('/api/auth/wechat/login', { code, appid: APPID })
if (d.status === 'bound' || d.status === 'registered') return d.login // 直接可用
// 首次:返回给 UI 层弹出二选一(携带 d.wx_token)
return { unbound: true, wxToken: d.wx_token }
}
async function quickRegister(wxToken) { // 新用户「快速开通」
const d = await request('/api/auth/wechat/register', { wx_token: wxToken })
return d.login
}
async function startConfirmBind(wxToken) { // 老用户「电脑确认码」
const d = await request('/api/auth/wechat/bind-code', { wx_token: wxToken })
wx.showModal({ title: '在电脑上确认绑定',
content: `打开 auth.ai-as.cc 登录后,进入「个人中心 → 关联小程序」,输入确认码:${d.code}(5 分钟内有效)` })
return new Promise((resolve, reject) => {
const timer = setInterval(async () => {
try {
const p = await request('/api/auth/wechat/poll-bind', { code: d.code })
if (p.status === 'bound') { clearInterval(timer); resolve(p.login) }
if (p.status === 'expired') { clearInterval(timer); reject(new Error('确认码已过期,请重新发起')) }
} catch (e) { clearInterval(timer); reject(e) }
}, 2000)
})
}
5. 调下游 API(ExoMind 等)——token 形态与下游校验
小程序拿到的 access_token 直调下游(Authorization: Bearer),下游验签有一处必须适配:
token payload 解剖
{
"iss": "https://auth.ai-as.cc",
"aud": "https://auth.ai-as.cc", // ⚠️ 第一方用户 token:aud=issuer,不是下游 client_id
"sub": "57", // 统一 user_id(与网页端登录同一账号同一 sub)
"permissions": ["read", "write", ...], // 用户权限码(下游按码判断,如 /ingest 要求 write)
"exp": 1735689600, "jti": "..."
}
- 签名 RS256、密钥与网页授权码流程同一把(
/.well-known/jwks.json,header 带 kid)。 - 两种通过下游校验的方式(二选一,推荐 A):
- A(推荐):登录请求带
audience: "<下游 client_id>"——tokenaud即下游 client_id,下游现有 fail-closed 校验直接通过,下游零改动;refresh 续期不掉受众(2026-09-05 上线)。 - B:下游把 issuer 加入 aud 白名单(
aud ∈ {client_id, "https://auth.ai-as.cc"})——适用于不便传 audience 的存量接入。详见 INTEGRATION §5。
- A(推荐):登录请求带
permissions 从哪来(治理依赖)
| 用户 | token 里的 permissions |
|---|---|
| 绑定老号 | 老号的全量权限码 |
| 快速开号新号 | 注册默认角色集(管理台「系统设置 → 注册默认角色」配置,未配置回退 user 角色)+「Agent 开发者」角色(2026-09-10 并入默认链) |
下游必备权限码(如 write)必须包含在注册默认角色集里,否则新用户调写入接口 403——这是配置态不是代码保证,改默认角色前先核对下游依赖。当前生产默认角色集含「ExoMind读写」(read/write)。
2026-09-10 起「Agent 开发者」并入注册默认链(注册/匿名转正/SSO/管理台建号四条路径自动获得,详见 AGENT_AUTHZ「授权域角色开放」):新号 token 的 permissions = 默认角色集 ∪ agent 域权限码(agent:resource:manage 等四码),不影响下游对 read/write 的判断。
冒烟验收(下游联通的判定步骤)
1. 小程序 wx.login → login → (首登)register → 拿 access_token
2. curl -X POST https://youhuale.cn/ingest \
-H "Authorization: Bearer <access_token>" -H "Content-Type: application/json" \
-d '{"content":"冒烟测试"}'
期望 200;401 → 查下游 aud 白名单;403 → 查注册默认角色是否含 write
401/403 排查表
| 症状 | 原因 | 修法 |
|---|---|---|
| 401(下游验签失败) | token aud=issuer 而下游只认 client_id | 登录请求加 audience(方式 A,推荐);或下游 aud 白名单加 issuer(方式 B) |
| 401(token 过期) | access_token 24h | refresh_token 换新(§3.7) |
| 403 | permissions 缺下游要求的码 | 管理台默认角色集补对应权限码(老号走角色提升申请) |
| 401(auth 侧) | 会话被踢(改密/解绑等) | 重新走 login |
6. 约束与红线(接入方必读)
| 项 | 说明 |
|---|---|
code 一次性 | 每个 wx.login code 只能换一次,失败重试要重新 wx.login |
wx_token 一次性、5 分钟 | register/bind/bind-code 消费即失效;过期让用户重新走 login |
| 确认码错 5 次作废 | 用户在电脑上输错 5 次需重新发起 |
| session_key | auth 侧不使用不下发——小程序端也不要存它(微信红线) |
| AppSecret | 只存在 auth 服务端(管理台 provider 配置),小程序端永远接触不到 |
| 限流 | login 有 IP 维度限流;bind 密码错误有防爆破锁定——收到 429/锁定文案就退避提示用户 |
7. FAQ
Q:换小程序(新 appid)用户数据怎么办? 同一微信开放平台账号下,unionid 不变 → 用户身份不变;管理台为新 appid 加一条 provider 配置即可,用户无感。
Q:用户换了微信号? 新微信号 = 新身份(首次登录会再走二选一)。老绑定不自动迁移——这是身份语义,不是 bug。
Q:绑定错了账号想换绑? 用户在 auth 个人中心解绑微信(带参 unbind),下次小程序登录重新走二选一。
Q:接口报「微信登录未开放」? appid 与管理台 provider 配置的 client_id 不一致,或该 provider 被停用。