Appearance
微信小程序登录
结论先行
微信小程序的"登录"不是传统意义的账号密码登录。它的核心是一对密钥交换:小程序端用 wx.login() 拿到临时 code,后端用 code + appId + appSecret 向微信服务器换取 openId 和 session_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 请求微信服务器返回手机号,不再需要客户端传
encryptedData和iv。更安全,且不依赖 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 万的小程序,如果实现为"每次打开都检查手机号授权状态并尝试获取",免费额度在几小时内用完。正确的做法:后端记录用户手机号授权状态,已获取手机号的用户不再发起授权请求。
