Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

微信小程序登录接入指南(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 参考

所有端点均为 POSTContent-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>"——token aud 即下游 client_id,下游现有 fail-closed 校验直接通过,下游零改动;refresh 续期不掉受众(2026-09-05 上线)。
    • B:下游把 issuer 加入 aud 白名单aud ∈ {client_id, "https://auth.ai-as.cc"})——适用于不便传 audience 的存量接入。详见 INTEGRATION §5。

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 24hrefresh_token 换新(§3.7)
403permissions 缺下游要求的码管理台默认角色集补对应权限码(老号走角色提升申请)
401(auth 侧)会话被踢(改密/解绑等)重新走 login

6. 约束与红线(接入方必读)

说明
code 一次性每个 wx.login code 只能换一次,失败重试要重新 wx.login
wx_token 一次性、5 分钟register/bind/bind-code 消费即失效;过期让用户重新走 login
确认码错 5 次作废用户在电脑上输错 5 次需重新发起
session_keyauth 侧不使用不下发——小程序端也不要存它(微信红线)
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 被停用。