微信小程序登录与账号体系
核对日期:2026-07-29
示例技术栈:微信原生小程序、Java 8、Spring Boot 2.2.x、Redis
1. 目标与边界
这套账号体系解决以下问题:
- 使用
wx.login()识别当前微信用户。 - 使用后端业务 Token 维持登录态。
- 将微信
openid、unionid、手机号与业务账号正确关联。 - 处理同一个用户通过手机号、多个小程序或企业微信登录时的账号合并。
几个标识的职责不能混淆:
| 标识 | 作用 | 能否作为业务主键 |
|---|---|---|
openid |
用户在某个小程序下的唯一标识 | 可作为当前小程序的外部身份键 |
unionid |
同一微信开放平台账号下的跨应用标识 | 可用于发现可能属于同一微信用户的账号 |
session_key |
微信会话密钥,用于数据解密等场景 | 不能作为业务登录 Token |
| 手机号 | 可变联系方式和验证手段 | 不能作为永久身份主键 |
| 业务用户 ID | 系统内部稳定主键 | 应作为数据库关系和权限判断依据 |
不要把 session_key 返回给前端
session_key 只能保存在服务端短期使用,不能作为登录凭证下发,也不能写入普通业务日志。
2. 推荐登录流程
小程序调用 wx.login
-> 获得 5 分钟有效的登录 code
-> 前端把 code 发送给业务后端
-> 后端调用 code2Session
-> 获得 openid、unionid、session_key
-> 按 appid + openid 查找外部身份
-> 创建或读取业务用户
-> 后端签发短期 Access Token 和长期 Refresh Token
-> 小程序携带 Access Token 调用业务接口
手机号绑定是登录后的独立步骤:
wx.login 的 code 与 getPhoneNumber 的 code 不能混用。
3. 小程序前端关键代码
/**
* 调用微信登录并用业务后端响应建立应用登录态。
*/
function login() {
return new Promise(function (resolve, reject) {
wx.login({
success: function (loginResult) {
if (!loginResult.code) {
reject(new Error('微信未返回登录code'));
return;
}
wx.request({
url: 'https://api.example.com/api/auth/wechat/login',
method: 'POST',
header: { 'Content-Type': 'application/json' },
data: { code: loginResult.code },
success: function (response) {
if (response.statusCode !== 200) {
reject(new Error('业务登录失败'));
return;
}
wx.setStorageSync('accessToken', response.data.accessToken);
wx.setStorageSync('refreshToken', response.data.refreshToken);
resolve(response.data.user);
},
fail: reject
});
},
fail: reject
});
});
}
请求业务接口时统一携带业务 Access Token:
function request(options) {
const accessToken = wx.getStorageSync('accessToken');
return new Promise(function (resolve, reject) {
wx.request({
url: 'https://api.example.com' + options.url,
method: options.method || 'GET',
data: options.data,
header: {
'Content-Type': 'application/json',
'Authorization': accessToken ? 'Bearer ' + accessToken : ''
},
success: function (response) {
if (response.statusCode === 401) {
reject(new Error('登录已过期'));
return;
}
resolve(response.data);
},
fail: reject
});
});
}
生产项目应增加“只允许一个刷新请求”的 Token 续期逻辑,避免多个并发请求同时刷新。
4. 后端调用 code2Session
微信服务端接口:
GET https://api.weixin.qq.com/sns/jscode2session
?appid=APPID
&secret=APPSECRET
&js_code=LOGIN_CODE
&grant_type=authorization_code
Spring Boot 关键代码:
/**
* 使用 wx.login code 换取微信用户身份。
*
* @param code 五分钟有效的微信登录凭证
* @return 微信会话身份
*/
public WechatSession exchangeCode(String code) {
JsonNode response = restTemplate.getForObject(
"https://api.weixin.qq.com/sns/jscode2session"
+ "?appid={appid}&secret={secret}"
+ "&js_code={code}&grant_type=authorization_code",
JsonNode.class,
appId,
appSecret,
code
);
if (response == null || response.path("errcode").asInt(0) != 0) {
throw new IllegalStateException("微信登录凭证校验失败");
}
String openid = response.path("openid").asText();
String unionid = response.path("unionid").asText(null);
String sessionKey = response.path("session_key").asText();
if (openid.isEmpty() || sessionKey.isEmpty()) {
throw new IllegalStateException("微信登录响应字段不完整");
}
return new WechatSession(openid, unionid, sessionKey);
}
注意事项:
- code 只能短期使用,后端收到后应立即消费。
- AppSecret 只能保存在服务端。
- 不要记录 code、AppSecret、session_key 或完整接口 URL。
- 登录失败后让前端重新调用
wx.login(),不要重复消费旧 code。
5. 创建或关联业务账号
推荐数据库结构:
user_account
├── id
├── status
├── mobile_ciphertext
├── mobile_hash
├── created_at
└── updated_at
external_identity
├── id
├── user_id
├── provider WECHAT_MINI_PROGRAM / WECOM / PASSWORD
├── app_id
├── external_user_id openid、userid 等
├── union_id
├── created_at
└── last_login_at
外部身份唯一索引:
登录服务的关键逻辑:
/**
* 根据微信身份查找或创建业务账号,并签发业务登录凭证。
*/
@Transactional
public LoginResult loginByWechat(String code) {
WechatSession session = wechatClient.exchangeCode(code);
ExternalIdentity identity = identityRepository
.findByProviderAndAppIdAndExternalUserId(
"WECHAT_MINI_PROGRAM",
appId,
session.getOpenid()
)
.orElseGet(() -> createIdentityAndUser(session));
identity.updateUnionIdIfAbsent(session.getUnionid());
identity.markLoginNow();
String accessToken = tokenService.issueAccessToken(identity.getUserId());
String refreshToken = tokenService.issueRefreshToken(identity.getUserId());
return new LoginResult(accessToken, refreshToken, identity.getUserId());
}
创建账号时必须依赖数据库唯一索引处理并发,不能只依赖“先查询再插入”。出现唯一键冲突时,应重新查询已经创建的身份记录。
6. UnionID 与账号合并
只有小程序绑定到微信开放平台,并满足微信的 UnionID 返回条件时,才可能获得 unionid。因此:
- 不能假设每次
code2Session都有unionid。 - 首次只有
openid时可以正常创建账号。 - 后续获得
unionid时再补充身份记录。 - 同一个
unionid命中另一个业务账号时,不能直接静默合并。
安全的账号合并流程:
发现 unionid 或手机号已关联其他账号
-> 要求用户验证旧账号
-> 展示即将合并的数据范围
-> 用户确认
-> 事务内迁移身份、订单和权限关系
-> 保留合并审计记录
-> 旧账号失效并撤销全部 Token
禁止仅凭以下条件自动合并:
- 昵称相同。
- 头像相同。
- 手机号字符串相同但没有重新验证。
- 客户备注手机号相同。
7. Token 设计建议
| Token | 建议有效期 | 保存位置 | 用途 |
|---|---|---|---|
| Access Token | 15 至 30 分钟 | 小程序本地存储 | 调用业务接口 |
| Refresh Token | 7 至 30 天 | 小程序本地加密存储或设备会话 | 换取新 Access Token |
| session_key | 跟随微信会话 | 仅服务端短期缓存 | 微信数据解密,不对前端公开 |
刷新 Token 时应:
- Refresh Token 只允许使用一次并进行轮换。
- Redis 保存 Token ID、用户 ID、设备 ID 和过期时间。
- 用户退出、修改密码、账号合并或管理员禁用时撤销会话。
- 对登录、刷新和手机号绑定接口进行频率限制。
8. 与手机号授权衔接
登录完成后再调用已有手机号接口:
详细实现参见:微信与企业微信获取手机号实现指南。
9. 上线检查
- code2Session 只能由后端调用。
- AppSecret 和 session_key 不进入前端与日志。
-
openid按appid隔离,不能跨小程序直接比较。 - 不强依赖
unionid一定存在。 - 创建身份记录有数据库唯一索引兜底。
- Access Token 短期有效,Refresh Token 可撤销并轮换。
- 手机号绑定必须校验当前业务登录态。
- 账号合并需要二次验证、用户确认和审计记录。
- 登录、刷新、手机号和合并接口均有限流。