跳转至

微信小程序登录与账号体系

核对日期:2026-07-29
示例技术栈:微信原生小程序、Java 8、Spring Boot 2.2.x、Redis


1. 目标与边界

这套账号体系解决以下问题:

  • 使用 wx.login() 识别当前微信用户。
  • 使用后端业务 Token 维持登录态。
  • 将微信 openidunionid、手机号与业务账号正确关联。
  • 处理同一个用户通过手机号、多个小程序或企业微信登录时的账号合并。

几个标识的职责不能混淆:

标识 作用 能否作为业务主键
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 建立业务登录态
    -> 用户点击 getPhoneNumber
    -> 后端换取手机号
    -> 将手机号绑定到当前业务用户

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

外部身份唯一索引:

create unique index uk_external_identity
    on external_identity(provider, app_id, external_user_id);

登录服务的关键逻辑:

/**
 * 根据微信身份查找或创建业务账号,并签发业务登录凭证。
 */
@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. 与手机号授权衔接

登录完成后再调用已有手机号接口:

业务 Access Token
    -> getPhoneNumber code
    -> 后端校验业务登录态
    -> 微信换取手机号
    -> 手机号加密保存到当前 user_id

详细实现参见:微信与企业微信获取手机号实现指南


9. 上线检查

  • code2Session 只能由后端调用。
  • AppSecret 和 session_key 不进入前端与日志。
  • openidappid 隔离,不能跨小程序直接比较。
  • 不强依赖 unionid 一定存在。
  • 创建身份记录有数据库唯一索引兜底。
  • Access Token 短期有效,Refresh Token 可撤销并轮换。
  • 手机号绑定必须校验当前业务登录态。
  • 账号合并需要二次验证、用户确认和审计记录。
  • 登录、刷新、手机号和合并接口均有限流。

10. 官方文档