Skip to content
微信小程序登录体系:code2Session、手机号授权与状态机
概述
微信小程序的登录不依赖账号密码,其本质是一次密钥交换:客户端调用 wx.login() 获取临时 code,服务端携带 code、appId、appSecret 向微信接口换取该用户的 openId 和 session_key。
openId 是微信用户在当前小程序中的唯一标识,同一用户在不同小程序中对应的 openId 不同。若开发者需要跨应用的统一标识,必须将小程序绑定到微信开放平台账号,此时 code2Session 接口会额外返回 unionId。
静默登录、用户授权登录、手机号登录共用同一条 code2Session 链路,差别在于拿到 openId 之后还需要用户提供哪些额外信息。
基本概念
code
wx.login() 返回的临时凭证。有效期 5 分钟,且 code2Session 调用一次后立即失效。重复使用会被微信服务端拒绝,返回 errcode: 40163("code been used")。
session_key
随 openId 一同由 code2Session 返回的会话密钥,用于解密微信接口返回的加密数据(如旧版手机号、运动数据等)。
session_key 不应传输到前端。它属于服务端持有的密钥材料,任何人持有 session_key 都可以解密该用户的敏感数据。将其放在 API 响应体或客户端 storage 中属于信息泄露。
session_key 会因为以下几种情况过期:用户长期未打开小程序、同一微信账号在其他设备登录、用户主动退出并重新登录。服务端解密时若遇到 errcode: 41001("session_key expired"),需要引导客户端重新执行 wx.login()。
openId 与 unionId
openId 的作用域是“微信用户 ↔ 单个小程序”。同一个微信用户在不同小程序中的 openId 不同。
unionId 的作用域是“微信用户 ↔ 微信开放平台账号”。将多个小程序或公众号绑定到同一开放平台账号后,同一用户在这些应用中的 unionId 一致。unionId 仅在满足条件时由 code2Session 返回。
access_token
access_token 是服务端调用微信开放平台 API 的全局凭证,与用户无关。它的获取需要 appId 和 appSecret,通过 https://api.weixin.qq.com/cgi-bin/token 接口返回。
access_token 有每日调用次数限制,且有效期较短(默认 7200 秒)。服务端必须缓存 access_token,在过期前主动刷新,不能每次调用 API 都重新获取,否则会迅速耗尽配额。常见的做法是在内存或 Redis 中维护一个全局 token,由一个定时任务或惰性检查在过期前(如提前 5 分钟)刷新。
在手机号授权流程中,服务端需要 access_token 才能调用 getPhoneNumber 接口。缓存的实现方式通常是:
javascript
// 服务端 access_token 缓存
let cachedToken = { value: null, expiresAt: 0 };
async function getAccessToken() {
if (Date.now() < cachedToken.expiresAt) {
return cachedToken.value;
}
const { data } = await axios.get(
'https://api.weixin.qq.com/cgi-bin/token',
{
params: {
appid: process.env.WX_APPID,
secret: process.env.WX_SECRET,
grant_type: 'client_credential'
}
}
);
cachedToken.value = data.access_token;
// 提前5分钟过期,避免边界情况
cachedToken.expiresAt = Date.now() + (data.expires_in - 300) * 1000;
return cachedToken.value;
}工作原理
核心链路 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
处理业务
← 返回业务数据code2Session 的一次性约束决定了客户端每次登录都需要重新调用 wx.login() 获取新 code,不能缓存 code 重复使用。
服务端实现 POST /login 接口需要完成的步骤:
- 接收客户端上传的 code
- 调用微信
code2Session接口换取openId和session_key - 在数据库中查找或创建该
openId对应的用户记录 - 生成自定义登录凭证(token),将 token 与
openId关联并持久化 - 将 token 返回给客户端,
session_key保留在服务端
基本用法
启动时建立登录态
用户进入小程序时,在 onLaunch 或根组件生命周期中自动执行静默登录,目的仅为获取 openId 并建立登录态,不弹出任何授权框。
javascript
// 小程序端:检测并补全登录态
async function ensureLogin() {
let token = uni.getStorageSync('__mp_token__');
if (token) {
const valid = await validateToken(token);
if (valid) return;
}
const { code } = await wx.login();
const { token: newToken } = await request.post('/api/login', { code });
uni.setStorageSync('__mp_token__', newToken);
}validateToken 通常是一次轻量请求(如 GET /api/user/check),由服务端判定 token 是否有效。不应在客户端自行判断过期时间:客户端时钟不可靠,且服务端可能已经主动使 token 失效。
请求携带 token
后续网络请求统一在拦截器中附加 token:
javascript
// 请求拦截器
uni.addInterceptor('request', {
invoke(args) {
const token = uni.getStorageSync('__mp_token__');
if (token) {
args.header.Authorization = `Bearer ${token}`;
}
}
});当服务端返回 401 状态码时,由响应拦截器驱动刷新流程,具体实现见“Token 管理”一节。
登录模式
静默登录
不弹出任何授权框,用户无感知。适用于首屏业务不需要用户身份信息的小程序,用户打开后即可浏览内容,登录在后台完成。实现逻辑即上一节中 ensureLogin 的代码。
用户信息获取
在需要获取微信用户昵称、头像等信息时触发。
2021 年 4 月起,wx.getUserInfo 不再弹出授权框,静默调用仅返回匿名信息(默认灰色头像 + “微信用户”昵称)。wx.getUserProfile(基础库 2.10.4+)可以弹出授权框,但必须由用户点击按钮触发,不能自动调用。从基础库 2.21.2 开始,wx.getUserProfile 返回的昵称和头像开始出现默认值。
从基础库 2.27.1 开始,头像和昵称的获取方式变为“用户主动填写”的组件模式:
html
<!-- 头像选择 -->
<button open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image :src="avatarUrl" />
</button>
<!-- 昵称输入 -->
<input type="nickname" @blur="onNicknameBlur" />当前版本中不再存在“一键授权获取微信头像昵称”的 API。头像和昵称需要单独的 UI 交互步骤,用户在组件中主动选择或输入,脱离了微信登录流程,成为独立的用户资料采集步骤。
手机号获取
获取用户微信绑定的手机号。手机号授权使用独立的 code 体系,不与 wx.login() 的 code 互通。
html
<button open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
获取手机号
</button>javascript
onGetPhoneNumber(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') return;
// e.detail.code 是手机号专用 code,不可用于 code2Session
request.post('/api/phone', { code: e.detail.code });
}旧版方式需要客户端传 encryptedData + iv,服务端用 session_key 解密。新版方式(基础库 2.21.2+)服务端直接使用手机号 code 请求微信 getPhoneNumber 接口,微信直接返回手机号,不依赖 session_key,也不再需要客户端传递加密数据。
服务端处理 /api/phone 接口的流程:
javascript
// Node.js 示例:新版手机号 code 换手机号
async function getPhoneByCode(phoneCode) {
const accessToken = await getAccessToken(); // 服务端缓存的 access_token
const { data } = await axios.post(
'https://api.weixin.qq.com/wxa/business/getuserphonenumber',
{ code: phoneCode },
{ params: { access_token: accessToken } }
);
// data.phone_info.phoneNumber 即为手机号
return data.phone_info.phoneNumber;
}几点约束:
- 必须由用户点击按钮触发,不能程序化调用。
- 每次点击消耗一次授权机会,用户在弹窗中可以选择拒绝。频繁弹出容易引起用户反感,通常只在注册、下单、支付等关键业务节点触发。
- 手机号 code 和 login code 互不通用。手机号 code 只能用于
getPhoneNumber接口,login code 只能用于code2Session。 - 手机号快速验证接口有调用额度。免费额度为每月 1000 次,超出后按量计费。服务端应记录用户手机号授权状态,已获取过手机号的用户不应再次发起调用。
Token 管理
双令牌机制
text
access token:
- 有效期较短(15 分钟 ~ 2 小时)
- 每次请求携带在 Authorization 头中
- 存储在客户端 storage
refresh token:
- 有效期较长(7 天 ~ 30 天)
- 仅用于刷新 access token
- 存储在客户端 storage分离两个令牌的目的是限制长期凭证的暴露面。access token 随每次请求传输,被截获的风险更高,短有效期可控制泄露后的影响窗口。refresh token 仅在与认证服务通信时使用,传输频率低得多。
客户端自动刷新
刷新过程由响应拦截器驱动,不依赖客户端主动判定 token 是否过期。服务端以 401 状态码表示 token 无效,客户端据此触发刷新:
javascript
// 响应拦截器中处理 401
uni.addInterceptor('response', {
async success(res) {
if (res.statusCode !== 401) return res;
try {
const refreshToken = uni.getStorageSync('__mp_refresh_token__');
if (refreshToken) {
const { accessToken, refreshToken: newRt } =
await request.post('/api/refresh', { refreshToken });
uni.setStorageSync('__mp_token__', accessToken);
uni.setStorageSync('__mp_refresh_token__', newRt);
// 用新 token 重试原请求
res.config.header.Authorization = `Bearer ${accessToken}`;
return uni.request(res.config);
}
} catch (_) {
// refresh token 失效,重新静默登录
uni.removeStorageSync('__mp_token__');
uni.removeStorageSync('__mp_refresh_token__');
const { code } = await wx.login();
const { accessToken, refreshToken } =
await request.post('/api/login', { code });
uni.setStorageSync('__mp_token__', accessToken);
uni.setStorageSync('__mp_refresh_token__', refreshToken);
res.config.header.Authorization = `Bearer ${accessToken}`;
return uni.request(res.config);
}
}
});刷新过程对业务调用方透明。调用方只调用 request.get('/user/info'),不感知 token 已过期并被刷新。重试原请求的结果仍通过 Promise 返回给调用方。
服务端 /api/refresh 接口:
javascript
// Node.js 示例:刷新 access token
async function refreshToken(refreshToken) {
const record = await db.findRefreshToken(refreshToken);
if (!record || record.expiresAt < Date.now()) {
throw new Error('refresh token expired');
}
// 使旧 refresh token 失效,防止重放
await db.deleteRefreshToken(refreshToken);
// 生成新令牌对
const newAccessToken = generateAccessToken(record.openId);
const newRefreshToken = generateRefreshToken(record.openId);
await db.saveRefreshToken(newRefreshToken);
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}登录状态机
从用户打开小程序到登录态稳定,状态流转如下:
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:access token 过期但 refresh token 仍有效。该状态对用户透明,不需要弹窗或加载提示,静默完成刷新。
- NO_TOKEN:首次使用或 token 已被清除。执行完整的
wx.login()→ code2Session 流程。
应用:三种模式的组合使用
实际项目中并非“选一种模式”,而是按业务节点组合:
text
用户打开小程序
→ 静默登录(获取 openId + token)
→ 浏览首页、查看列表 → 不需要额外授权
→ 点击“我的” → 引导头像昵称填写(用户信息模式)
→ 点击“下单” → 弹出手机号授权(手机号模式)
→ 下单完成 → 服务端关联 openId + 手机号 + 用户档案授权弹窗应按需触发,不在用户进入小程序的前几秒内连续弹出多个。每个授权弹窗应出现在用户明确需要对应功能之后。
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() | — |
条件编译是处理跨端登录差异的常规做法:
javascript
// 静默登录的条件编译
// #ifdef MP-WEIXIN
const { code } = await uni.login();
const res = await request.post('/api/login/wechat', { code });
// #endif
// #ifdef MP-ALIPAY
const { authCode } = await uni.getAuthCode({ scopes: 'auth_user' });
const res = await request.post('/api/login/alipay', { authCode });
// #endif各平台服务端对应的接口也不同。微信服务端调用 code2Session,支付宝调用 alipay.system.oauth.token。登录模块通常是跨端项目中条件编译分支最多的区域。
示例
服务端 code2Session 接口
javascript
// Node.js 示例:POST /api/login
async function login(req, res) {
const { code } = req.body;
const wxRes = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: {
appid: process.env.WX_APPID,
secret: process.env.WX_SECRET,
grant_type: 'authorization_code',
js_code: code
}
});
const { openid, session_key, unionid, errcode, errmsg } = wxRes.data;
if (errcode) {
// errcode: 40029 — code 无效
// errcode: 45011 — 调用频率超限
// errcode: 40163 — code 已被使用
return res.status(400).json({ error: errmsg });
}
// openid 映射到内部用户体系
let user = await db.findUserByOpenId(openid);
if (!user) {
user = await db.createUser({ openId: openid });
}
// 生成双令牌
const accessToken = generateAccessToken(user.id);
const refreshToken = generateRefreshToken(user.id);
await db.saveRefreshToken(refreshToken, user.id);
// session_key 仅存储在服务端
await db.saveSessionKey(openid, session_key);
res.json({ accessToken, refreshToken });
}code2Session 返回的 session_key 不包含在 API 响应中。存储到服务端数据库或缓存后,仅在后续需要解密用户敏感数据时使用。
客户端登录流程与失败重试
客户端在 App.onLaunch 中执行静默登录。若静默登录本身失败(例如网络问题),不应阻塞用户进入小程序。失败时只记录日志,后续需要登录态的业务请求会触发自动恢复:
javascript
// 小程序端 App.vue:onLaunch 中的登录检查
async function initLogin() {
try {
const accessToken = uni.getStorageSync('__mp_token__');
const refreshToken = uni.getStorageSync('__mp_refresh_token__');
if (accessToken) {
const valid = await request.get('/api/user/check');
if (valid) return;
}
if (refreshToken) {
try {
const { accessToken: newAt, refreshToken: newRt } =
await request.post('/api/refresh', { refreshToken });
uni.setStorageSync('__mp_token__', newAt);
uni.setStorageSync('__mp_refresh_token__', newRt);
return;
} catch (e) {
uni.removeStorageSync('__mp_token__');
uni.removeStorageSync('__mp_refresh_token__');
}
}
const { code } = await wx.login();
const { accessToken: at, refreshToken: rt } =
await request.post('/api/login', { code });
uni.setStorageSync('__mp_token__', at);
uni.setStorageSync('__mp_refresh_token__', rt);
} catch (e) {
// 静默登录失败不阻塞用户进入小程序。
// 当用户发起需要登录态的请求时,响应拦截器会捕获 401,
// 并自动执行 wx.login() 重试(参见 Token 管理一节)。
console.warn('initLogin failed:', e);
}
}此处失败后的恢复依赖之前展示的响应拦截器逻辑:任意业务请求返回 401 且本地无有效 refresh token 时,拦截器会重新调用 wx.login() 并向 /api/login 发起登录请求,得到新 token 后自动重试原请求。这样,即使用户进入小程序时网络异常导致静默登录失败,后续正常网络环境下的首次业务请求也能自动完成登录恢复。
注意点
code 被重复使用
wx.login() 每次调用返回新 code,旧 code 立即失效。如果前端短时间内多次调用 wx.login() 并将前后两次的 code 都发给服务端,后到达的旧 code 会触发 errcode: 40163。
网络超时重试也可能导致此问题:第一次请求已到达服务端并成功处理,但客户端因超时未收到响应,使用同一个 code 发起重试。
处理方式:
- 客户端每次
wx.login()拿到新 code 后,在内存中标记旧 code 失效。 - 服务端对 code 做幂等处理:同一个 code 的第二次请求直接返回首次处理的结果。
session_key 过期
服务端使用 session_key 解密数据时可能遇到 errcode: 41001。原因是用户长期未打开小程序导致 session_key 过期,或用户在其他设备登录同一微信账号导致 session_key 被替换。
处理方式:服务端检测到 session_key 过期后,返回特定错误码给客户端。客户端重新执行 wx.login(),服务端通过 code2Session 获取新 session_key 后重试解密。如果使用的是新版手机号 code 换手机号的方式,则不依赖 session_key,可以绕开此问题。
手机号授权被拒绝
getPhoneNumber 回调中 e.detail.errMsg 不为 'getPhoneNumber:ok' 时表示用户拒绝授权。这是平台允许的正常行为,产品层面需提供降级方案,如允许用户手动输入手机号完成当前操作。不应反复弹出授权框催促用户。
openId 暴露风险
openId 是用户的唯一标识,在前端环境存在泄露风险。前端应使用服务端映射后返回的自定义 userId 替代 openId 进行埋点和日志记录。
手机号授权接口调用额度
手机号快速验证接口每月有 1000 次免费额度,超出后按量计费。不应实现为“每次打开小程序都检查手机号授权状态并尝试获取”。服务端应记录用户手机号授权状态,已获取过手机号的用户不再发起调用。
参考链接
- 微信小程序登录:
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html - code2Session 接口:
https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html - 手机号快速验证:
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html - 头像昵称填写:
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html - uni-app 登录:
https://uniapp.dcloud.net.cn/api/plugins/login.html - 微信 access_token:
https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
