小程序登录与常规 Web 登录有本质差异:微信不直接向开发者开放用户的密码体系,而是通过 wx.login 获取临时凭证 code,再由开发者服务端调用微信接口换取用户的 OpenID。理解这条「code → code2session → 自定义 Token」链路,是构建小程序用户体系的基石。本文将从登录整体流程出发,深入拆解登录态 Token 设计、Session 管理、手机号快速验证、多端账号打通与登录态安全,帮助开发者搭建一套可靠、可扩展、可审计的登录鉴权方案。
一、登录整体流程:从 wx.login 到 code2session
小程序登录采用「前端换 code、后端换身份」的双段式设计,前端永远接触不到微信核心密钥。
1. 小程序前端调用 wx.login → 得到临时登录凭证 code
2. 前端将 code 通过 HTTPS 传给自有后端
3. 后端用 code + appid + secret 调用微信接口 code2session
4. 微信返回 openid / session_key /(可能含)unionid
5. 后端查库或建库,生成自定义登录态 Token 返回前端
6. 前端存储 Token,后续所有请求携带它完成身份认证
1.1 前端获取 code
wx.login 是最基础的 API,其回调 success 返回的 code 有效期约 5 分钟,且只能使用一次,使用后即失效:
// utils/auth.js
function wxLogin() {
return new Promise((resolve, reject) => {
wx.login({
timeout: 10000, // 超时时间,默认 1000ms,推荐调大
success: (res) => {
if (res.code) {
resolve(res.code);
} else {
reject(new Error('获取 code 失败:' + res.errMsg));
}
},
fail: (err) => reject(err)
});
});
}
一句话:
wx.login不返回用户身份,只返回一张「5 分钟内有效、只能消费一次」的临时入场券 code,真正的身份换取必须发生在服务端。
1.2 服务端 code2session
服务端拿到 code 后,调用微信开放接口 code2session 换取身份信息:
// 服务端 Node.js(使用标准 fetch,无需额外 SDK)
async function code2session(code) {
const url = 'https://api.weixin.qq.com/sns/jscode2session' +
'?appid=' + APPID +
'&secret=' + process.env.WX_SECRET + // 密钥只存在服务端
'&js_code=' + code +
'&grant_type=authorization_code';
const res = await fetch(url);
const data = await res.json();
// 成功响应:{ openid, session_key, unionid? }
// 失败响应:{ errcode, errmsg }
if (data.errcode) {
throw new Error(`code2session 失败 ${data.errcode}: ${data.errmsg}`);
}
return data;
}
微信返回的 session_key 是会话密钥,用于解密用户敏感数据(如手机号);openid 是该小程序下的用户唯一标识;unionid 只有在用户将多个应用绑定到同一个开放平台账号下时才会返回。
| 字段 | 说明 | 典型用途 |
|---|---|---|
| openid | 当前小程序内用户唯一 ID | 用户表主键、业务数据归属 |
| session_key | 会话密钥,用于解密敏感数据 | 解密手机号、encryptedData 数据 |
| unionid | 开放平台下多应用统一 ID | 多端(小程序/公众号/App)打通 |
二、自定义登录态 Token 设计
微信的 session_key 不适合直接作为业务登录态下发前端:其一,它语义是「密钥」而非「凭证」;其二,它不由后端控制过期与撤销。实践中必须由服务端签发自有 Token。
2.1 JWT 无状态 Token
JWT 将用户身份与过期时间自包含在签名体中,适合微服务、多端共用的场景:
// 服务端:签发 JWT 登录态
const jwt = require('jsonwebtoken');
function issueToken(user) {
return jwt.sign(
{
uid: user.id, // 用户表主键
openid: user.openid,
role: user.role // 权限字段按需放最小集
},
process.env.JWT_SECRET,
{ expiresIn: '7d' } // 登录态有效期
);
}
2.2 有状态 Session Token
对于单体应用或云开发场景,也可以使用「随机 Token + 服务端缓存」的有状态方案:Token 本身是一串无含义的随机值,用户信息存 Redis 或云数据库,注销时直接删除记录即可立刻失效。
// 服务端:有状态 Token 方案
const crypto = require('crypto');
async function createSession(user) {
const token = crypto.randomBytes(32).toString('hex');
await redis.set(`session:${token}`, JSON.stringify(user), 'EX', 7 * 24 * 3600);
return token;
}
| 维度 | JWT(无状态) | Opaque Session(有状态) |
|---|---|---|
| 存储 | 不需要服务端存储 | 需要 Redis/DB 存储 |
| 失效/撤销 | 需要黑名单机制 | 直接删除记录即刻失效 |
| 分布式友好 | 天然支持多实例 | 需共享存储 |
| 适用场景 | 微服务、多端共用 | 单体、云开发、强管控 |
一句话:无论哪种 Token,前端都只能拿到随机凭证,绝不能拿到
openid明文用于业务鉴权——身份判定必须由服务端根据 Token 反查。
三、Session 管理与过期刷新
3.1 前端存储与携带
前端将 Token 存入本地,请求时通过 Header 携带:
// 登录成功后
wx.setStorageSync('token', token);
wx.setStorageSync('expireAt', Date.now() + 7 * 24 * 3600 * 1000);
3.2 静默续期与 wx.checkSession
wx.checkSession 用于校验微信侧的会话(session_key)是否仍有效。注意它校验的是微信会话而非业务 Token,两者需协同处理:
// app.js —— 启动时静默恢复登录态
App({
onLaunch() {
this.ensureLogin().catch(() => {});
},
async ensureLogin() {
const token = wx.getStorageSync('token');
if (token) {
// 先校验微信会话是否过期
const sessionAlive = await new Promise((resolve) => {
wx.checkSession({
success: () => resolve(true),
fail: () => resolve(false)
});
});
if (sessionAlive) {
// 会话未过期:用刷新令牌续期,无需重新走微信登录
return this.refreshToken();
}
}
// 会话过期或没有 Token:重新 wx.login 走完整登录
return this.fullLogin();
}
});
一句话:推荐「短期业务 Token + 长期刷新 Token」双层设计:业务 Token 过期用刷新 Token 静默续期,刷新 Token 失效才触发
wx.login重新登录,避免用户频繁看到登录框。
3.3 用户资料(头像昵称)新规范
微信自 2022 年 10 月起不再建议通过 wx.getUserProfile 获取头像昵称,改为「头像昵称填写能力」,由用户主动填写:
<!-- 昵称输入框 -->
<input type="nickname" placeholder="请输入昵称" bindblur="onNickInput" />
<!-- 头像选择 -->
<button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image src="{{avatarUrl}}" mode="aspectFill" />
</button>
// pages/profile/profile.js
Page({
data: { avatarUrl: '/img/default-avatar.png', nickname: '' },
onChooseAvatar(e) {
const tempPath = e.detail.avatarUrl; // 用户选择的临时头像路径
// 上传头像到服务器/云存储,取回 CDN 地址持久化
wx.cloud.uploadFile({
cloudPath: `avatar/${Date.now()}.png`,
filePath: tempPath
}).then((res) => this.setData({ avatarUrl: res.fileID }));
},
onNickInput(e) {
this.setData({ nickname: e.detail.value });
}
});
头像与昵称属于用户敏感信息,应按隐私规范声明用途、仅在用户主动填写时获取,避免在启动阶段静默采集。
四、手机号快速验证
「手机号快捷登录」是小程序最常用的实名化手段,早期依赖 encryptedData 解密,如今官方推荐「code 换手机号」方案。
4.1 前端按钮组件
<!-- pages/login/login.wxml -->
<button
class="phone-btn"
open-type="getPhoneNumber"
bindgetphonenumber="onGetPhoneNumber"
>
手机号快捷登录
</button>
// pages/login/login.js
Page({
onGetPhoneNumber(e) {
const { code, errMsg } = e.detail;
if (code) {
// 将 phoneCode 交给服务端,服务端换取手机号
this.request('/api/phone-login', { code });
} else {
wx.showToast({ title: '已取消授权', icon: 'none' });
console.warn('手机号授权失败', errMsg);
}
}
});
4.2 服务端换手机号
服务端使用接口 getuserphonenumber,需要 access_token:
// 服务端:用 phoneCode 换取手机号
async function getPhoneNumber(code) {
const url = 'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=' + accessToken;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const { errcode, errmsg, phone_info } = await res.json();
if (errcode !== 0) throw new Error(`${errcode}: ${errmsg}`);
// phone_info: { phoneNumber, purePhoneNumber, countryCode }
return phone_info.purePhoneNumber;
}
该接口仅对已认证小程序开放,code 有效期约 5 分钟。手机号明文只在服务端处理,前端拿到的应是脱敏展示或直接不展示,避免敏感信息落客户端。
五、多端账号打通:unionid 与绑定体系
当同一主体运营多款小程序、公众号或 App 时,需要识别「同一用户」。unionid 是官方提供的跨应用统一标识,前提是:
- 所有应用绑定到同一个微信开放平台账号(需主体一致)。
- 用户在这些应用中均完成了微信授权。
对于无法获得 unionid 的场景(如不同主体),可通过「手机号 + 验证码」或「手机号快捷验证」将各端账号绑定到自有统一账号中心:
| 打通方式 | 前提 | 统一标识 | 适用场景 |
|---|---|---|---|
| unionid | 同主体绑定开放平台 | unionid | 同主体多应用 |
| 手机号绑定 | 自有账号体系 | 手机号 | 跨主体、跨端 |
| 扫码绑定 | 需要二维码 | 自有账号 ID | PC/小程序互跳 |
六、登录态安全加固
登录鉴权是攻击面最集中的环节,必须遵守以下红线:
- secret 绝不落地前端:
code2session只能发生在服务端,appsecret一旦泄露等于整个用户体系沦陷。 - code 单次使用:服务端应将消费过的 code 视为无效,防止重放。
- Token 存储安全:不把 Token 写入
page层可直接读取的全局变量之外的位置,避免 XSS/工具注入;重要操作二次校验。 - 敏感数据不下发:
session_key、openid不随接口响应直接返回给业务层以外的地方。 - 传输加密:全部走 HTTPS,配置域名白名单,禁止明文传输。
- 风控与频控:登录接口加 IP/设备限流,异常高频
code2session直接告警拉黑。
| 安全项 | 措施 | 风险等级 |
|---|---|---|
| 密钥管理 | secret 仅服务端、定期轮换 | 高 |
| code 防重放 | 记录并拒绝重复 code | 高 |
| Token 加密传输 | HTTPS + 请求签名 | 高 |
| 会话固定防护 | 每次登录签发新 Token | 中 |
| 数据脱敏 | 手机号等敏感字段不下发 | 中 |
七、云开发场景下的登录简化
如果使用云开发,登录链路会被大幅简化:云函数自动携带调用者身份,无需手动管理 code2session 与 access_token。
// cloudfunctions/login/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
exports.main = async () => {
const { OPENID, APPID, UNIONID } = cloud.getWXContext();
const db = cloud.database();
const { data } = await db.collection('users')
.where({ _openid: OPENID }).limit(1).get();
if (data.length === 0) {
// 首次登录,创建用户
await db.collection('users').add({ data: { _openid: OPENID, createdAt: db.serverDate() } });
}
return { openid: OPENID, unionid: UNIONID };
};
一句话:云开发通过
cloud.getWXContext()把微信身份直接注入云函数,省去前端 code 透传与服务端密钥管理,代价是与腾讯云生态绑定更深。
八、常见问题与调试
- code 一直失效:检查是否重复消费、是否超 5 分钟、是否用错
js_code参数名。 - 拿不到 unionid:确认应用已绑定开放平台、主体一致、用户已完成授权。
- 手机号返回失败:确认小程序已认证、基础库版本 ≥ 2.21.2、
code未过期。 getUserProfile已不推荐:2022 年后头像昵称改用input type="nickname"与open-type="chooseAvatar",应避免继续依赖getUserProfile。- 真机 vs 开发者工具差异:开发者工具可使用测试号与模拟数据,部分能力(如真实手机号)需在真机与正式配置下验证。
九、总结
| 环节 | 核心 API / 机制 | 关键要点 |
|---|---|---|
| 获取凭证 | wx.login | code 临时、单次、5 分钟有效 |
| 换取身份 | code2session | 服务端持有 secret,返回 openid/unionid/session_key |
| 业务登录态 | 自定义 Token | JWT 或 Opaque Session,服务端反查 |
| 过期刷新 | 刷新 Token + wx.checkSession | 双层续期,静默重登 |
| 手机号验证 | open-type="getPhoneNumber" | code 换手机号,明文不下发 |
| 多端打通 | unionid / 手机号绑定 | 开放平台绑定或自有账号中心 |
| 安全加固 | 服务端校验 / HTTPS / 频控 | secret 不落地,code 防重放 |
| 云开发简化 | cloud.getWXContext() | 免密钥管理,天然身份注入 |
登录鉴权是小程序全站安全的第一道门。正确理解 wx.login → code2session → 自定义 Token 三层链路,做到「前端不接触密钥、服务端不信任前端、敏感数据不下发」,才能为后续的订单、支付、隐私数据等核心业务建立可信的身份底座。在此基础上,可进一步参考本专题的安全与合规实践与全栈项目实战,将登录体系接入完整业务闭环。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。