在开发微信小程序时,很多新手会被 code、openid、access_token 等名词绕晕。 本文将带你通过"前后端联动"的方式,彻底搞定微信授权登录和手机号快捷登录。不仅有代码,还有Access_token 缓存优化的最佳实践。
1. 区别和流程对比
| 功能 | 流程步骤 | 核心标识 |
|---|---|---|
| 微信登录 | 1. 前端 uni.login()拿 code2. 后端 jscode2session换 openid + session_key3. 用 openid 查库/注册用户 4. 生成系统 token |
openid |
| 手机号登录 | 1. 前端 wx.getPhoneNumber()拿 phoneCode2. 后端先用 loginCode 换 openid + session_key3. 用 phoneCode + session_key解密获取手机号 4. 用 openid 查库/注册用户 5. 生成系统 token |
phoneNumber或 openid |
解释一下其中有几个名词的意思:
1.1. loginCode
来源: 前端通过 uni.login() 拿到的 code。
作用: 这是一个临时凭证,有效期很短(5 分钟)。
用途: 必须发送给后端,后端再去请求微信接口(jscode2session),才能换取真正的用户标识(openid、session_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. 后端如何实现
先在 微信公众平台 拿到 appId 和 secret (进入到管理,点击开发管理,点击开发设置)

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_key 和 openid,然后拿这个 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,把 phoneCode 和 loginCode 传给后端。
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>
总结一下后端应该做什么:首先拿到前端传过来的phoneCode 和 loginCode ,用 loginCode + appId + secret 换 session_key + openid ,然后用 openid 换 access_token(缓存) ,然后用 accessToken + phoneCode 调用官方 api 换真实手机号,然后拿 openid来判断用户是否第一次登录,如果是第一次就入库注册然后生成 Token,如果不是第一次就生成 Token,然后 Token 以及其他需要的返回给前端。