微信小程序踩坑实录:getAccessToken 配额耗尽45009引发的登录问题修复与流程优化

254 阅读5分钟

2025年11月26日 20:33:47 这周遇到了getAccessToken导致所有小程序用户无法登录的吓死人BUG, 不过还好解决了, 至今心有余悸...感谢ai救命...呼呼呼zzz

1️⃣微信小程序getAccessToken导致用户登录失败的问题

  1. 🍎问题与修复。一定!一定!一定要先联系后台开发者把getAccessToken方法替换为getStableAccessToken, 因为前者每日限制2000次极易导致45009(配额耗尽)问题, 遇到日新增用户比较高时会出大事!!!后者则是"该接口调用频率限制为 1万次 每分钟,每天限制调用 50万 次"。再把getStableAccessToken获取到的access_tokenRedis缓存下来重复使用, 注意有效期一般为2小时(7200秒)。我就是因为既没用对接口也没缓存token导致的问题!!!此时我的线上用户登录的问题就已经解决了, 后面是一些前后端整体登录流程的优化方案;
  2. 微信小程序手机号快速验证配额消耗过快的解决方案。在微信小程序端登录过程中一定会给后台两个code值, 一个是wx.login的称其为loginCode, 另一个是登录按钮open-type="getPhoneNumber"的称其为phoneCode, 每成功调用一次open-type="getPhoneNumber"就消费一次额度即0.03元, 一旦遇到大量用户涌入或恶意点击时你的配额会被快速消耗掉。解决方案如下:

✨后台先通过jscode2session + loginCode得到$openid = $data['openid'];, 在openidgetUserPhoneNumber之间加入一个缓存机制: 先用openid尝试从Redis中查找用户。如果Redis没找到再去mysql里找。如果找到对应用户, 直接返回前端登录成功信息(与正常登录一致), 没找到该用户就注册用户并返回登录成功信息!!!

  1. 缓存phoneCode避免手机号验证额度消耗过快。open-type="getPhoneNumber"返回的phoneCode也有5分钟生命, 可以缓存下来重复使用减少手机号快速验证的额度消耗;
  2. 其它限制。当然,登录流程也可以加上ip, openid, 设备识别码等限制, 个人觉得绝大部分小程序其实没搞得这么复杂;
  3. 多说点。微信小程序登录可以配合jwt机制更好用, checkSession已不再推荐, 接口后台的openidsessionKey一定不能返回给前端, 还有wx.loginopen-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, secretgrant_type, appid, secret, force_refresh(建议false
每日配额约2000次/天(常见)更高(官方未公开,显著高于普通版)
推荐场景开发环境、低并发场景、临时回退生产环境、高并发场景、稳定性要求高的业务
失败策略不建议作为主入口失败时可降级到普通版getAccessToken
缓存策略expires_in - 300s(Redis缓存,提前5分钟过期)同上(统一缓存策略)
典型错误码45009(配额耗尽)、40013(appid/secret异常)同样返回errcode/errmsg,配额问题触发概率更低
响应字段access_token, expires_in与普通版一致

4️⃣参考文档