Skip to content

微信小程序登录

结论先行

微信小程序的"登录"不是传统意义的账号密码登录。它的核心是一对密钥交换:小程序端用 wx.login() 拿到临时 code,后端用 code + appId + appSecret 向微信服务器换取 openIdsession_key。openId 是用户在当前小程序中的唯一标识——同一个微信用户在不同小程序中的 openId 不同。unionId 才是跨小程序的同一个微信开放平台账号下的统一标识。

三种登录模式——静默登录、用户授权登录、手机号登录——都基于同一条 code2Session 链路。区别在于拿到 openId 之后,还需要用户授权哪些额外信息。


核心链路:code → openId

text
小程序端                              后端                           微信服务器
────────                    ────────                       ──────────
wx.login() ───────────────→
  返回 code (5分钟有效)
                            POST /login { code }
                            code2Session(code) ─────────→
                                                          返回:
                                                            openId
                                                            session_key
                                                            unionId (条件)

                            生成自定义 token
                            关联 openId ↔ token
                            ← 返回 token
存储 token 到 storage
后续请求带 token ──────────→
                            验证 token
                            查 openId
                            处理业务
                            ← 返回业务数据

几个必须记住的约束:

  • code 只能用一次wx.login() 返回的 code 在 code2Session 接口被调用一次即失效。重复调用会返回 errcode: 40163, "code been used"
  • code 有效期 5 分钟。拿到 code 后要尽快发送到后端,不要在客户端持有太久。
  • session_key 不能传到前端code2Session 返回的 session_key 应该只留在后端。任何把 session_key 放到前端或 API 响应中的做法都是安全风险——有了 session_key 就可以解密用户的敏感数据(手机号、运动数据等)。
  • session_key 会过期。用户长期不打开小程序或者主动退出重新登录,旧的 session_key 失效。后端在使用 session_key 解密数据时如果遇到 errcode: 41001, "session_key expired",需要引导客户端重新 wx.login()

三种登录模式

模式 1:静默登录

不弹任何授权框,用户无感知。只获取 openId。

text
小程序端流程:
  onLaunch / App.vue mounted
    → checkLoginStatus()
      → storage 中有 token?
        → 有:验证 token 有效性
          → 有效:登录态正常,继续
          → 无效/过期:执行静默登录
        → 无:执行静默登录

  静默登录:
    wx.login() → code
    POST /api/login { code }
    ← { token, userId }
    写入 storage

适用:不强制要求用户信息的场景。大多数小程序的首屏业务靠这个模式就能跑通。用户看到的流程:打开小程序 → 看到首页 → 浏览内容。全程没有登录框。

模式 2:用户授权登录

需要获取用户微信昵称、头像等信息时触发。

2021 年 4 月之后,微信调整了 wx.getUserInfo / wx.getUserProfile 的获取策略。旧接口 wx.getUserInfo 不再弹出授权框,只能获取匿名信息(默认灰色头像 + "微信用户"昵称)。新的获取方式:

js
// 必须由用户点击按钮触发,不能自动弹出
<button open-type="getUserInfo" @getuserinfo="onGetUserInfo">
  获取微信昵称和头像
</button>

// 或使用 wx.getUserProfile(基础库 2.10.4+)
wx.getUserProfile({
  desc: '用于完善用户资料',
  success: (res) => {
    // res.userInfo: { nickName, avatarUrl, gender, country, province, city }
    // 注意:从基础库 2.21.2 开始,返回的 nickName 和 avatarUrl 可能是默认值
  }
})

从基础库 2.27.1 开始,微信进一步收紧了获取策略。头像昵称的获取改为"头像昵称填写"组件:

html
<!-- 头像 -->
<button open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
  <image :src="avatarUrl" />
</button>

<!-- 昵称 -->
<input type="nickname" @blur="onNicknameBlur" />

这意味着当前版本的微信小程序中,用户主动填写头像和昵称,而不是"授权获取"。这对登录流程的影响:头像和昵称不再是 wx.login()wx.getUserProfile() 一步获取的,需要单独的 UI 交互步骤。

模式 3:手机号登录

获取用户微信绑定的手机号。这是一个独立的授权通道,不和 wx.login() 的 code 共享。

html
<button open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
  获取手机号
</button>
js
onGetPhoneNumber(e) {
  if (e.detail.errMsg !== 'getPhoneNumber:ok') return

  // e.detail.code —— 手机号 code,不是 login 的 code
  // e.detail.iv / e.detail.encryptedData —— 加密数据(旧版方式)
  // 新版(基础库 2.21.2+)直接用 code 换手机号

  // 新版方式(推荐):
  // POST /api/phone { code: e.detail.code }
  // 后端用 code 调 getPhoneNumber 接口查手机号
}

手机号授权的完整时序:

text
小程序端                      后端                        微信服务器
────────                ────────                    ──────────
用户点击按钮
getPhoneNumber
获得 code (phone)
                        POST /api/phone { code }
                        getPhoneNumber(code) ───────→
                                                     返回手机号

                        关联 openId ↔ phone
                        ← 返回成功
更新本地用户信息

几个约束:

  • 必须用户点击按钮触发,不能自动调用。
  • 每次点击消耗一次授权,用户可以在弹窗中拒绝。频繁弹手机号授权框会导致用户反感——通常在注册/下单/支付等关键节点才触发。
  • 手机号 code 和 login code 是不同的。手机号 code 专门用于 getPhoneNumber 接口,不能用于 code2Session,反之亦然。
  • 新版接口(code 换手机号)取代了旧版(encryptedData + iv + session_key 解密)。新版本中后端直接用 code 请求微信服务器返回手机号,不再需要客户端传 encryptedDataiv。更安全,且不依赖 session_key。

Token 管理

双令牌设计

text
access token:
  - 有效期较短(15 分钟 ~ 2 小时)
  - 每次请求携带,用于鉴权
  - 存储在 storage

refresh token:
  - 有效期较长(7 天 ~ 30 天)
  - 专门用于刷新 access token
  - 存储在 storage(安全要求高时可存储在更安全的位置)
js
// 请求拦截器:自动刷新
async function request(config) {
  if (isTokenExpired()) {
    try {
      const { accessToken, refreshToken } = await refreshAccessToken()
      saveTokens(accessToken, refreshToken)
    } catch (e) {
      // refresh token 也过期 → 重新 login
      await silentLogin()
    }
  }
  config.header.Authorization = `Bearer ${getAccessToken()}`
  return uni.request(config)
}

token 过期后的静默刷新

text
请求 → 后端返回 401
  → 拦截器捕获
    → 拿 refresh token 请求 /api/refresh
      → 成功:更新 token,重放原请求
      → 失败(refresh 也过期):
        → 调用 wx.login() 重新获取 code
        → POST /api/login { code }
        → 更新 token,重放原请求
        → (如果这次也失败 → 清除本地状态,引导用户重新授权)

这里的核心原则:刷新 token 的过程对业务代码透明。调用方只写 request.get('/user/info'),不感知 token 已过期被刷新这一事实。

uni-app 中的 token 存储

js
// storage 的键名建议加命名空间,避免多小程序共存时冲突
const TOKEN_KEY = '__mp_token__'
const REFRESH_KEY = '__mp_refresh_token__'
const USER_KEY = '__mp_user_info__'

// 同步读写(uni-app storage 是同步 API,和微信原生一致)
uni.setStorageSync(TOKEN_KEY, token)
const token = uni.getStorageSync(TOKEN_KEY)

注意:微信小程序的 storage 上限是 10MB,单个 key 的 value 不超过 1MB。但这不是 token 应该关心的——token 通常是几十字节的字符串。


登录状态机

从用户打开小程序到登录态稳定,经过的状态:

text
              ┌──────┐
    启动 App  │ UNKNOWN │  ← 初始状态
              └──┬───┘

         storage 中有 token?
         ┌─ 是 ───┐  否 ──┐
         ↓        ↓       ↓
    ┌────────┐    │   ┌──────────┐
    │ TOKEN  │    │   │NO_TOKEN  │
    │EXISTING│    │   └────┬─────┘
    └───┬────┘    │        │
        │         │   wx.login() → code
    验证 token     │   POST /api/login
        │         │        │
    ┌───┴───┐     │        ↓
  有效    过期     │   ┌──────────┐
    │       │     │   │LOGGED_IN │
    ↓       ↓     │   └────┬─────┘
 ┌──────┐ ┌──────────┐    │
 │LOGGED│ │REFRESHING│←───┘
 │_IN   │ └────┬─────┘
 └──────┘      │
        refresh token 有效?
         ┌─ 是 ─┐ 否 ──┐
         ↓      ↓      ↓
    刷新成功  刷新失败 → wx.login() 重新登录
         ↓              ↓
    ┌──────────┐   ┌──────────┐
    │LOGGED_IN │   │LOGGED_IN │
    └──────────┘   └──────────┘

关键状态:

  • UNKNOWN:启动时的瞬态,在 App.onLaunch 中读取 storage 后立即转换到下一个状态。
  • REFRESHING:token 过期但 refresh token 仍有效。这个状态对用户透明——用户不看到任何弹窗或加载,静默完成。
  • NO_TOKEN:首次使用或 token 被清除。走完整的 wx.login() → code2Session 流程。

三种模式的组合实践

实际项目中,登录不是"选一种模式",而是"按业务节点组合三种模式":

text
用户打开小程序
  → 静默登录(获取 openId + token)
    → 浏览首页、查看列表 → 不需要额外授权,静默态足够
    → 点击"我的" → 引导头像昵称授权(模式 2)
    → 点击"下单" → 弹手机号授权(模式 3)
    → 下单成功 → 后端关联 openId + 手机号 + 用户档案

这个流程的设计原则:按需触发授权,不提前一次性索要所有权限。用户打开小程序的 3 秒内不希望看到 3 个授权弹窗。每个授权弹窗都应出现在用户明确需要该功能之后。


uni-app 跨端差异

uni-app 封装了各平台的登录 API,但不同平台的登录模型有本质差异:

平台静默登录用户信息获取手机号获取备注
微信小程序uni.login() → code头像昵称填写组件getPhoneNumber 按钮openId 按小程序隔离
支付宝小程序uni.getAuthCode()my.getOpenUserInfo()my.getPhoneNumber()userId 代替 openId
百度小程序swan.login()swan.getUserInfo()不支持
字节小程序tt.login()tt.getUserInfo()tt.getPhoneNumber()

uni-app 提供了 uni.login({ provider: 'weixin' }) 的统一接口,但实际上只封装了微信小程序的 wx.login()。支付宝小程序需要 provider 指定为 alipay 时才走对应 API。跨端项目中需要条件编译:

js
// #ifdef MP-WEIXIN
const { code } = await uni.login()
// #endif

// #ifdef MP-ALIPAY
const { authCode: code } = await uni.getAuthCode({ scopes: 'auth_user' })
// #endif

不是所有平台的登录逻辑都能统一封装。做跨端小程序的项目,登录模块通常是条件编译分支最多的部分之一。


常见问题排查

code 被多次使用

表现:后端返回 errcode: 40163, "code been used"

原因一:前端多次调用了 wx.login(),并把前后两次的 code 都发给了后端。wx.login() 每次返回新 code,旧 code 立即失效。

原因二:网络超时重试机制。第一次请求实际已到达后端且成功,但响应超时,前端重试用了同一个 code。

解决:每次 wx.login() 拿到新 code 后立即失效旧的 code(在客户端内存中标记)。后端侧做幂等处理——同一个 code 的第二次请求直接返回首次处理的结果。

静默登录失败,没有 code

表现:wx.login() 的 fail 回调被触发。

可能原因:

  • 微信版本过低,不支持 wx.login
  • 小程序 AppID 未配置或配置错误(开发工具中可能正常,真机上失败)
  • 网络异常,无法连接微信服务器

处理原则:静默登录失败时不要阻塞用户进入小程序。记录失败日志,在下一次网络请求时重试。

session_key 过期导致解密失败

表现:后端解密手机号时返回 errcode: 41001, "session_key expired"

原因:用户长期未使用小程序(session_key 过期),或用户在其他设备上登录了同一微信账号(session_key 被替换)。

解决:后端检测到 session_key 过期后,返回特定错误码给前端。前端调用 wx.login() 获取新 code,后端重新 code2Session 获取新 session_key,再解密。如果解密的是手机号(新版接口),直接用 phone code 换手机号,绕开 session_key 依赖。

手机号授权被用户拒绝

表现:e.detail.errMsg 不为 'getPhoneNumber:ok'

这是正常行为——用户有权拒绝。前端处理:记录拒绝事件,在产品层面决定是否仍然允许用户完成当前操作(如不获取手机号也可以手动填写手机号完成下单)。不要反复弹授权框催促用户。


几个实际判断

不要把 openId 暴露到前端日志或埋点中。 openId 是用户的唯一标识,泄露后有账号关联风险。前端使用自定义的 userId(后端映射 openId → userId 后返回)替代 openId。

静默登录 + 按需授权的模式适用于 90% 的小程序。 只有强依赖用户身份的品类(如社交、金融服务)才需要在首屏立即要求授权。多数工具型、内容型、电商型小程序的首屏业务(浏览、搜索)不需要用户信息就能跑通。

双令牌机制不是过度设计。 即使小程序看起来是一个"短会话"场景(打开用完就关),但微信的"最近使用的小程序"列表和后台保活机制让用户可能在几分钟内多次进出。token 在 storage 中持久化意味用户不需要每次都 login——打开速度更快。没有 refresh token 则意味着每次 token 过期都是一次完整的 wx.login() 重新登录。

手机号授权的成本被低估了。 不是技术成本——手机号快速验证接口每月有免费额度(1000 次/月),超出后按次数收费。对于一个日活 1 万的小程序,如果实现为"每次打开都检查手机号授权状态并尝试获取",免费额度在几小时内用完。正确的做法:后端记录用户手机号授权状态,已获取手机号的用户不再发起授权请求。