一文搞定微信小程序双登录模式:授权登录 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 以及其他需要的返回给前端。

相关推荐
用户946883150575013 小时前
一、elpis 基于 nodejs 实现服务端内核引擎
全栈
前端双越老师3 天前
我开发 AI Agent 项目踩过的 5个坑
前端·agent·全栈
飘尘4 天前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
onething3656 天前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 5 —— SSE 流式输出 + 打字机效果
人工智能·后端·全栈
onething3656 天前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 6 —— 业务完善 + 会话消息预览
人工智能·后端·全栈
东坡白菜6 天前
破局全栈:一个前端开发的Java入门实战记录(1)
java·全栈
程序员黑豆8 天前
AI全栈开发系列开篇:从Java全栈到AI应用实战
前端·ai编程·全栈
chengliu05089 天前
从前端转型全栈、 Agent 开发
程序员·全栈
智码看视界11 天前
老梁聊全栈系列 JavaScript语言本质:从原型链到异步编程的深度解析
开发语言·javascript·全栈·javascript核心
To_OC11 天前
我一直以为 Ajax 是个黑盒,直到我写了这 50 行代码
前端·后端·全栈