一文搞定微信小程序双登录模式:授权登录 vs 手机号登录流程详解

在开发微信小程序时,很多新手会被 code、openid、access_token 等名词绕晕。 本文将带你通过"前后端联动"的方式,彻底搞定微信授权登录和手机号快捷登录。不仅有代码,还有Access_token 缓存优化的最佳实践。

1. 区别和流程对比

功能 流程步骤 核心标识
微信登录 1. 前端 uni.login()code2. 后端 jscode2sessionopenid + session_key3. 用 openid 查库/注册用户 4. 生成系统 token openid
手机号登录 1. 前端 wx.getPhoneNumber()phoneCode2. 后端先用 loginCodeopenid + session_key3. 用 phoneCode + session_key解密获取手机号 4. 用 openid 查库/注册用户 5. 生成系统 token phoneNumberopenid

解释一下其中有几个名词的意思:

1.1. loginCode

来源: 前端通过 uni.login() 拿到的 code。

作用: 这是一个临时凭证,有效期很短(5 分钟)。

用途: 必须发送给后端,后端再去请求微信接口(jscode2session),才能换取真正的用户标识(openidsession_key)。

类比:好比拿到了一张 "临时兑换券",要拿去微信那里兑换成真实身份。

1.2. openid

来源: 后端用 loginCode 请求微信 jscode2session 接口返回的。

作用: 这是用户用户在小程序里面的唯一标识(对同一个小程序,openid 永远唯一)。

用途: 可以拿它来判断用户是不是注册过,类似数据库里面的 userId

类比:相当于微信告诉你「这个用户在你这里的身份证号」。

1.3. session_key

来源: 同样是 jscode2session 接口返回的。

作用: 是一个 会话密钥,用来解密用户敏感数据,比如手机号。

用途: session_key不能直接暴露给前端, 后端用 session_key解密能得到明文手机号。

类比:一把解锁手机信息的钥匙。

1.4. phoneCode

来源: 前端按钮 <button open-type="getPhoneNumber"> 获取的 code。

作用:这个 phoneCode 也是一个临时凭证。

用途:需要传递给后端,后端调用微信的 手机号获取接口 解密才能拿到真实的号码

类比:一张「手机号兑换券」,需要后端再去微信换。

接下来看看前后端如何实现的,这里我就直接在 Controller 里面写了,大家开发的时候一定要分层!

2. 实现小程序授权登录

2.1. 总流程

2.2. 前端如何实现

html 复制代码
<template>
  <button @click="wxLogin">微信授权登录</button>
</template>

<script setup>
  const wxLogin = () => {
    // 第一步:调用 uni.getUserProfile() 获取用户信息
    uni.getUserProfile({
      desc: '用于完善会员资料', /
      success: (res) => {
        console.log('用户信息:', res.userInfo)
        // userInfo.value = res.userInfo // 这里可以存储一下
        // userInfo.value 包含:
        // nickName、avatarUrl、gender、province、city、country
      },
      fail: (err) => {
        console.log('用户拒绝授权', err)
      }
    })
    // 第二步:调用
    uni.login({
      success(res) {
        const loginCode = res.code
        // 第三步:请求后端接口,把 loginCode 传给后端,让后端处理
        const url = 'http://localhost:8080/wxUser/wxLogin';

        uni.request({
          url,
          method: 'POST',
          data: {
            loginCode,
          },
          success: (res) => {
            console.log(res)
            
           
          },
          fail: (err) => {
            console.log(err)
          }
        })
      }
    })
  }
</script>

注意:调用 uni.getUserProfile 我的基础库是选择的 2.16.1 版本,目前新版本无法触发官方的弹窗,并且拿到信息也是匿名的。

总结一下前端需要做的事是:uni.getUserProfile()(获取用户信息) 调用 uni.login()(获取 loginCode),最重要的就是这个 loginCode 需要把这个传递给后端。

2.3. 后端如何实现

先在 微信公众平台 拿到 appIdsecret (进入到管理,点击开发管理,点击开发设置)

typescript 复制代码
@RestController
@RequestMapping("/wxUser")
public class WxUserController {

    @PostMapping("/wxLogin")
    public Result wxUser(@RequestBody WxUser wxUser){
        System.out.println(wxUser);

        // 第一步:拿到前端传递的 loginCode
        String loginCode = wxUser.getLoginCode();  // 这里是 wx.login 返回的 code

        // 第二步:拿到自己的 appId + secret
        String appId = "wxd00000000000";
        String secret = "00000000000000000000";
        RestTemplate restTemplate = new RestTemplate();

        // 第三步:用 loginCode + appId + secret 换 session_key + openid
        String sessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId
        + "&secret=" + secret
        + "&js_code=" + loginCode
        + "&grant_type=authorization_code";
        String sessionResponse = restTemplate.getForObject(sessionUrl, String.class);
        JSONObject sessionJson = JSONObject.parseObject(sessionResponse);
        String openid = sessionJson.getString("openid");

        System.out.println("openid:" + openid);

        // 剩下逻辑就拿着 openid 去库里查找有没有这个用户,如果没有就入库然后JWT生成token返回给前端,如果有的话就直接生成Token给前端即可

        return Result.success("登录成功");
    }
}

总结一下后端需要做的事是:拿到后端传过来的 loginCode 然后拿 appId + secret + loginCode 请求微信官方 api 去换 session_keyopenid,然后拿这个 openid来判断用户是否第一次登录,如果是第一次就入库注册然后生成 Token,如果不是第一次就生成 Token,然后 Token 以及其他需要的返回给前端。

3. 实现小程序手机号登录

3.1. 总流程

3.2. 前端如何实现

注意:要实现手机号授权,涉及到用户隐私,必须拥有调用该接口的权限,否则会报错。

微信小程序的 getPhoneNumber 接口仅对"非个人主体"且"已完成微信认证"的小程序开放,个人开发者账号无法使用此功能。

解决方案(二选一):

方案一:认证为企业小程序(推荐)

  • 登录 微信公众平台
  • 进入【设置】>【基本设置】>【微信认证】,按提示完成企业认证(需支付,1-3个工作日审核)
  • 认证成功后,重新启动微信开发者工具即可正常使用 getPhoneNumber 接口

方案二:使用微信测试号进行开发调试

  • 微信提供了测试号,允许开发者在未认证的情况下体验接口
xml 复制代码
<template>
  <!-- 关键在于 open-type="getPhoneNumber" 和 @getphonenumber 事件 -->
  <button open-type="getPhoneNumber" @getphonenumber="handleGetPhoneNumber">
    手机号一键登录
  </button>
</template>

<script setup>
  const handleGetPhoneNumber = async (e) => {
    console.log('手机号授权回调', e.detail);
    
    if (!e.detail.code) {
      uni.showToast({
        title: '您取消了授权',
        icon: 'none'
      });
      return;
    }

    // 1. 获取手机号登录凭证
    const phoneCode = e.detail.code;

    // 2. 获取登录凭证 code
    const loginRes = await uni.login();
    const loginCode = loginRes.code;

    // 3. 请求后端接口把 loginCode 和 phoneCode 传给后后端
    // 为什么传 loginCode:后端换取 opendi 生成JWT
    // 为什么传 phoneCode:后端通过这个 code 解码来拿到真实手机号返回给前端
    const url = 'http://localhost:8080/wxUser/wxPhoneLogin';

    uni.request({
      url,
      method: 'POST',
      data: { loginCode, phoneCode },
      success: (res) => {
        console.log(res)
      },
      fail: (err) => {
        console.log(err)
      }
    })

  }
</script>

<style>

</style>

总结一下前端需要做什么事:通过 open-type="getPhoneNumber" 和 @getphonenumber 事件拿到 phoneCode,通过 uni.login() 拿到 loginCode,把 phoneCodeloginCode 传给后端。

3.3. 后端如何实现

3.3.1. 创建一个专门管理 access_token 的服务

1. 为什么这么做:

防止每次调用手机号登录接口,都会去请求微信服务器换取一次 access_token。微信对 access_token 的获取有严格的调用频率限制。当用户量稍微大一点,你的服务器会因为频繁请求而被微信暂时封禁,导致所有手机号登录功能全部瘫痪。

2. 原理:

access_token 的有效期是 2 小时,并且在有效期内是全局唯一的。我们应该在它快过期前才去重新获取。

3. 如何做

  • 引入全局缓存 :必须将 access_token 存放在一个全局的、可持久化的缓存中(比如 Redis,或者简单的内存缓存也可以)。
  • 收到请求后,先去缓存(如 Redis)里尝试获取 access_token,如果能获取到,并且没过期,直接使用,如果获取不到,或者已过期,再去调用微信接口获取一个新的 access_token,并立刻存入缓存,同时设置好过期时间(比如 7000 秒)
kotlin 复制代码
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class WxAccessTokenService {

    // 使用 @Value 注解从配置文件 application.properties 中读取配置
    @Value("${wx.appId}")
    private String appId;

    @Value("${wx.secret}")
    private String secret;

    // --- 内存缓存的核心 ---
    private String accessToken;
    private long expiresAt; // access_token 的过期时间点(毫秒)
    // --------------------

    /**
     * 获取 access_token 的主方法
     * 增加了 synchronized 关键字,确保在多线程环境下只有一个线程能获取新 token,防止并发问题。
     * @return 有效的 access_token
     */
    public synchronized String getAccessToken() {
        // 检查当前 token 是否存在且未过期
        if (this.accessToken != null && System.currentTimeMillis() < this.expiresAt) {
            System.out.println("从内存缓存中获取 accessToken");
            return this.accessToken;
        }

        // 如果 token 不存在或已过期,则重新获取
        System.out.println("缓存中 accessToken 已过期或不存在,重新获取...");
        fetchNewAccessToken();
        return this.accessToken;
    }

    /**
     * 从微信服务器获取新的 access_token 并更新缓存
     */
    private void fetchNewAccessToken() {
        RestTemplate restTemplate = new RestTemplate();
        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
                + "&appid=" + this.appId
                + "&secret=" + this.secret;

        String tokenJsonResponse = restTemplate.getForObject(url, String.class);
        JSONObject tokenJson = JSONObject.parseObject(tokenJsonResponse);

        if (tokenJson != null && tokenJson.getString("access_token") != null) {
            this.accessToken = tokenJson.getString("access_token");
            long expiresIn = tokenJson.getLongValue("expires_in"); // 获取到的有效期,单位秒

            // 计算下一次的过期时间点。
            // 微信返回的 expires_in 是 7200 秒(2小时)。
            // 我们提前 10 分钟(600秒)让它过期,以防止因网络延迟等问题导致 token 恰好在临界点失效。
            this.expiresAt = System.currentTimeMillis() + (expiresIn - 600) * 1000;
            
            System.out.println("成功获取新的 accessToken: " + this.accessToken);
            System.out.println("accessToken 将在 " + new java.util.Date(this.expiresAt) + " 过期");
        } else {
            // 处理获取失败的情况
            System.err.println("获取 accessToken 失败: " + tokenJsonResponse);
            // 在实际项目中,这里应该抛出异常或进行更详细的错误处理
        }
    }
}

3.3.2. 创建 Controller

typescript 复制代码
@RestController
@RequestMapping("/wxUser")
public class WxUserController {

    // 注入新创建的 access_token 管理服务
    @Autowired
    private WxAccessTokenService wxAccessTokenService;

    // 同样从配置文件读取,保持一致性
    @Value("${wx.appId}")
    private String appId;
    @Value("${wx.secret}")
    private String secret;

    @PostMapping("/wxPhoneLogin")
    public Result phoneWxUser(@RequestBody WxUser wxUser){
        System.out.println(wxUser);

        String phoneCode = wxUser.getPhoneCode();
        String loginCode = wxUser.getLoginCode();
        
        RestTemplate restTemplate = new RestTemplate();

        // 第一步:用 loginCode 换 openid
        String sessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId
                + "&secret=" + secret
                + "&js_code=" + loginCode
                + "&grant_type=authorization_code";
        String sessionResponse = restTemplate.getForObject(sessionUrl, String.class);
        JSONObject sessionJson = JSONObject.parseObject(sessionResponse);
        String openid = sessionJson.getString("openid");
        System.out.println("openid:" + openid);

        // 第二步:从我们的服务中获取 access_token
        String accessToken = wxAccessTokenService.getAccessToken();
        System.out.println("获取到的 accessToken:" + accessToken);

        // 如果 accessToken 获取失败,直接返回错误
        if (accessToken == null) {
            return Result.error("获取 accessToken 失败,请稍后重试");
        }
        
        // 第三步:用 accessToken + phoneCode 调用官方 api 换真实手机号 (这部分逻辑不变)
        String phoneUrl = UriComponentsBuilder
                .fromHttpUrl("https://api.weixin.qq.com/wxa/business/getuserphonenumber")
                .queryParam("access_token", accessToken)
                .toUriString();

        Map<String, String> params = new HashMap<>();
        params.put("code", phoneCode);

        String phoneResponse = restTemplate.postForObject(phoneUrl, params, String.class);
        System.out.println(phoneResponse);
        
        JSONObject phoneJson = JSONObject.parseObject(phoneResponse);
        if (phoneJson != null && phoneJson.getInteger("errcode") == 0) {
            JSONObject phoneInfo = phoneJson.getJSONObject("phone_info");
            String phoneNumber = phoneInfo.getString("phoneNumber");
            System.out.println("用户手机号:" + phoneNumber);
            // 这里可以继续业务逻辑,比如用 openid 或 phoneNumber 查找用户、注册、生成JWT等
        } else {
            String errmsg = phoneJson != null ? phoneJson.getString("errmsg") : "未知错误";
            System.out.println("获取手机号失败:" + errmsg);
            return Result.error("获取手机号失败: " + errmsg);
        }

        return Result.success("登录成功");
    }
}

3.3.3. 添加配置

为了让 @Value 注解生效,你需要在 src/main/resources/application.properties 文件中添加你的小程序配置:

ini 复制代码
wx.appId=wxd000000000
wx.secret=111111111111111111

3.3.4. 导入依赖

xml 复制代码
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>2.0.42</version> <!-- 使用最新稳定版本 -->
</dependency>

总结一下后端应该做什么:首先拿到前端传过来的phoneCodeloginCode ,用 loginCode + appId + secretsession_key + openid ,然后用 openidaccess_token(缓存) ,然后用 accessToken + phoneCode 调用官方 api 换真实手机号,然后拿 openid来判断用户是否第一次登录,如果是第一次就入库注册然后生成 Token,如果不是第一次就生成 Token,然后 Token 以及其他需要的返回给前端。

相关推荐
得物技术9 小时前
AI驱动:从运营行为到自动化用例的智能化实践|得物技术
前端·ai编程·全栈
博丽灵梦1 天前
Playwright的wait funtion测试
全栈
子兮曰1 天前
独立开发者主流技术栈(2026最新)
前端·后端·全栈
霪霖笙箫1 天前
「JS全栈AI学习」十一、Multi-Agent 系统设计:可观测性与生产实践
前端·面试·全栈
Cosolar1 天前
文生图竞技场变局:GPT-Image-2 以 1512 分登顶,多模态格局重塑
人工智能·开源·全栈
~ rainbow~1 天前
前端转型全栈(五)——NestJS 文件上传功能开发复盘
前端·全栈
Pkmer2 天前
古法编程: 装饰器模式
设计模式·全栈
小兵张健3 天前
3 个 Codex 提效小技巧
chatgpt·全栈
小兵张健4 天前
Codex 切换 Provider 后恢复历史对话
chatgpt·openai·全栈