2025年11月26日 20:33:47这周遇到了getAccessToken导致所有小程序用户无法登录的吓死人BUG, 不过还好解决了, 至今心有余悸...感谢ai救命...呼呼呼zzz
1️⃣微信小程序getAccessToken导致用户登录失败的问题
- 🍎问题与修复。一定!一定!一定要先联系后台开发者把
getAccessToken方法替换为getStableAccessToken, 因为前者每日限制2000次极易导致45009(配额耗尽)问题, 遇到日新增用户比较高时会出大事!!!后者则是"该接口调用频率限制为 1万次 每分钟,每天限制调用 50万 次"。再把getStableAccessToken获取到的access_token值Redis缓存下来重复使用, 注意有效期一般为2小时(7200秒)。我就是因为既没用对接口也没缓存token导致的问题!!!此时我的线上用户登录的问题就已经解决了, 后面是一些前后端整体登录流程的优化方案; - 微信小程序
手机号快速验证配额消耗过快的解决方案。在微信小程序端登录过程中一定会给后台两个code值, 一个是wx.login的称其为loginCode, 另一个是登录按钮open-type="getPhoneNumber"的称其为phoneCode, 每成功调用一次open-type="getPhoneNumber"就消费一次额度即0.03元, 一旦遇到大量用户涌入或恶意点击时你的配额会被快速消耗掉。解决方案如下:
✨后台先通过
jscode2session + loginCode得到$openid = $data['openid'];, 在openid和getUserPhoneNumber之间加入一个缓存机制: 先用openid尝试从Redis中查找用户。如果Redis没找到再去mysql里找。如果找到对应用户, 直接返回前端登录成功信息(与正常登录一致), 没找到该用户就注册用户并返回登录成功信息!!!
- 缓存
phoneCode避免手机号验证额度消耗过快。open-type="getPhoneNumber"返回的phoneCode也有5分钟生命, 可以缓存下来重复使用减少手机号快速验证的额度消耗; - 其它限制。当然,登录流程也可以加上ip, openid, 设备识别码等限制, 个人觉得绝大部分小程序其实没搞得这么复杂;
- 多说点。微信小程序登录可以配合
jwt机制更好用,checkSession已不再推荐, 接口后台的openid和sessionKey一定不能返回给前端, 还有wx.login和open-type="getPhoneNumber"是两个独立的步骤, 我记得以前是嵌套。如果有更好的意见或建议请指出。
2️⃣后端核心代码
三个PHP方法userlogin,getAccessToken,getUserPhoneNumber
/**
* 微信小程序登录接口
*/
public function userlogin()
{
// 兼容GET和POST请求方式获取参数
$loginCode = Request::instance()->param('loginCode');
$phoneCode = Request::instance()->param('phoneCode');
$url = "https://api.weixin.qq.com/sns/jscode2session?appid={$this->appid}&secret={$this->secret}&js_code={$loginCode}&grant_type=authorization_code";
$url = base64_encode($url);
$data = file_get_contents($this->url.'?url='.$url);
$data = json_decode($data, true);
//var_dump($data);die;
// 数据库记录用户的 session_key 和 openid 数据, 注 session_key 只能保留在服务器端!
$session_key = $data['session_key'];
$openid = $data['openid'];
//🍎🍎🍎注意: $openid = $data['openid'];和getUserPhoneNumber之间加入一个机制,
//用loginCode取到的openid先去redis里面去找这个用户,
//如果没有再去mysql里找,如果找的了直接返回给前端数据(数据结构和正常登录成功保持一致)后面代码就不用执行了,
//没找到该用户就注册用户并返回登录成功信息!!!!!!!!!!!!!!
$access_token = $this->getAccessToken($this->appid, $this->secret);
if($access_token === false){
$msg = array('code'=>0,'msg'=>'access_token acquisition failed','data'=>array());
$return = json_encode($msg);
echo $return;die;
}
//获取成功,加上小程序传来的临时令牌, 再去请求详情 $code $access_token -> getuserphonenumber
$data2 = array(
//注: $access_token 必须放到url上, post参数仅有code一个
//'access_token' => $access_token,
'code' => $phoneCode
);
$phone_info = $this->getUserPhoneNumber("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=".$access_token,$data2);
//业务代码省略
//echo $phone_info["phoneNumber"];//提取从微信后台拿到的手机号
}
/**
* 从微信获取access_token(带缓存机制)
* 优先使用稳定版接口 getStableAccessToken,配额更高更稳定
*/
public function getAccessToken($appid,$secret)
{
// 1. 先从Redis缓存中获取
$cache_key = 'wechat_access_token_' . $appid;
$cached_token = $this->Redis->get($cache_key);
if($cached_token) {
// 缓存命中,直接返回
return $cached_token;
}
// 2. 缓存未命中,调用微信API获取
// 优先使用 getStableAccessToken 接口(配额更高,更稳定)
// 官方文档: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html
$stable_url = "https://api.weixin.qq.com/cgi-bin/stable_token";
$post_data = json_encode([
'grant_type' => 'client_credential',
'appid' => $appid,
'secret' => $secret,
'force_refresh' => false // 使用微信缓存,减少调用次数
]);
$stable_url_encoded = base64_encode($stable_url);
$data = @file_get_contents($this->url.'?url='.$stable_url_encoded.'&postfields='.$post_data);
if($data !== false) {
$result = json_decode($data, true);
// 稳定版接口成功返回
if(isset($result["access_token"])){
// 缓存到Redis(有效期7200秒,提前5分钟过期避免边界问题)
$expire_time = isset($result["expires_in"]) ? intval($result["expires_in"]) - 300 : 6900;
$this->Redis->setex($cache_key, $expire_time, $result["access_token"]);
return $result["access_token"];
}
}
// 3. 稳定版接口失败,降级使用普通接口
$fallback_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=".$appid."&secret=".$secret;
$fallback_url_encoded = base64_encode($fallback_url);
$data = @file_get_contents($this->url.'?url='.$fallback_url_encoded);
if($data !== false) {
$result = json_decode($data, true);
if(isset($result["access_token"])){
// 缓存到Redis
$expire_time = isset($result["expires_in"]) ? intval($result["expires_in"]) - 300 : 6900;
$this->Redis->setex($cache_key, $expire_time, $result["access_token"]);
return $result["access_token"];
} else {
echo "<br>getAccessToken Failed: ".$result["errcode"].";".$result["errmsg"]."<br>";
return false;
}
}
// 4. 所有尝试都失败
echo "<br>getAccessToken Failed: Unable to connect to WeChat API<br>";
return false;
}
/**
* 从微信端获取手机号
*/
function getUserPhoneNumber($remote_server, $data)
{
$json_data = json_encode($data);
$url = base64_encode($remote_server);
$data = file_get_contents($this->url.'?url='.$url.'&postfields='.$json_data);
$result = json_decode($data,true);
//echo '获取用户手机号: '.print_r($result,true).'<br>';
if(isset($result["phone_info"])){
return $result["phone_info"];
}else{
echo "<br>getUserPhoneNumber Failed: ".$result["errcode"].";".$result["errmsg"]."<br>";
return false;
}
}
3️⃣getAccessToken 对比 getStableAccessToken
获取access_token是对接微信接口的基础,普通版与稳定版的差异直接影响登录稳定性,以下为关键对比(来源:ZqrbVote/doc/手机号验证组件问题排查报告.md):
| 对比项 | 普通版 getAccessToken | 稳定版 getStableAccessToken |
|---|---|---|
| 调用路径 | /cgi-bin/token?grant_type=client_credential&appid=...&secret=...(GET) | /cgi-bin/stable_token(POST JSON) |
| 请求参数 | grant_type, appid, secret | grant_type, appid, secret, force_refresh(建议false) |
| 每日配额 | 约2000次/天(常见) | 更高(官方未公开,显著高于普通版) |
| 推荐场景 | 开发环境、低并发场景、临时回退 | 生产环境、高并发场景、稳定性要求高的业务 |
| 失败策略 | 不建议作为主入口 | 失败时可降级到普通版getAccessToken |
| 缓存策略 | expires_in - 300s(Redis缓存,提前5分钟过期) | 同上(统一缓存策略) |
| 典型错误码 | 45009(配额耗尽)、40013(appid/secret异常) | 同样返回errcode/errmsg,配额问题触发概率更低 |
| 响应字段 | access_token, expires_in | 与普通版一致 |
4️⃣参考文档
- [🍎!!!!!每一句话都要看!!!!!]获取稳定版接口调用凭据access_token(getStableAccessToken):developers.weixin.qq.com/miniprogram…
- [坑爹1] 获取接口调用凭据(getAccessToken):developers.weixin.qq.com/miniprogram…
- [坑爹2]服务端API对接文档:developers.weixin.qq.com/miniprogram…
- 手机号快速验证组件(获取用户手机号接口):developers.weixin.qq.com/miniprogram…
- 手机号计费误差问题排查指南:developers.weixin.qq.com/miniprogram…