微信第三方应用的授权设计与实现(component_verify_ticket、component_access_token、pre_auth_code)

1、查看网址:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/creat_token.html 。微信服务器 每隔 10 分钟会向第三方的消息接收地址推送一次 component_verify_ticket,用于获取第三方平台接口调用凭据。在接口:接收验证票据(接收微信服务器推送) 实现中,获取与缓存第三方平台令牌、获取与缓存第三方平台预授权码。

// 解密成功
if ($errCode === 0) {
    $ticketData = simplexml_load_string($msg, 'SimpleXMLElement', LIBXML_NOCDATA);
    if (isset($ticketData->ComponentVerifyTicket)) {
        $redisCommandkeyPrefix = Yii::$app->params['redisCommand']['keyPrefix'];
        $componentVerifyTicketKey = $redisCommandkeyPrefix . 'component_verify_ticket:' . $ticketData->AppId;
        Yii::$app->redis->set($componentVerifyTicketKey, $ticketData->ComponentVerifyTicket);
        // 获取第三方平台令牌
        WxOpenAuthService::getComponentAccessToken($appId, $appSecret);
        // 获取第三方平台预授权码
        WxOpenAuthService::getComponentPreAuthCode($appId);
    }
}

2、获取第三方平台令牌的实现中,参考网址:https://www.shuijingwanwq.com/2021/07/05/5020/ 。代码如下

    /**
     * 获取第三方平台令牌
     *
     * @param $appId string  第三方平台 appId
     * @param $appSecret string 第三方平台 appSecret
     * @return bool
     * @throws ServerErrorHttpException
     * @throws NotFoundHttpException
     */    public static function getComponentAccessToken($appId, $appSecret)
    {
        $time = time();
        $redis = Yii::$app->redis;
        $redisCommandkeyPrefix = Yii::$app->params['redisCommand']['keyPrefix'];
        $componentVerifyTicketKey = $redisCommandkeyPrefix . 'component_verify_ticket:' . $appId;
        $componentVerifyTicket = $redis->get($componentVerifyTicketKey);
        if (!$componentVerifyTicket) {
            throw new NotFoundHttpException(Yii::t('error', 205059), 205059);
        }
        $componentAccessTokenKey = $redisCommandkeyPrefix . 'component_access_token:' . $appId;
        // 获取 Redis 中的 component_access_token
        $componentAccessToken = $redis->get($componentAccessTokenKey);

        // 判断 Redis 中的 component_access_token 是否存在
        if ($componentAccessToken) {
            $componentAccessToken = unserialize($componentAccessToken);
        }
        if (!$componentAccessToken || (($componentAccessToken['expires_at'] - 900) < $time) ) {
            $data = [
                'component_appid' => $appId,
                'component_appsecret' => $appSecret,
                'component_verify_ticket' => $componentVerifyTicket,
            ];
            $httpWxAuthAccessToken = new HttpWxAuthAccessToken();
            // http 请求获取 component_access_token
            $httpWxAuthAccessTokenResult = $httpWxAuthAccessToken->wxComponentAccessToken($data);
            if ($httpWxAuthAccessTokenResult === false) {
                if ($httpWxAuthAccessToken->hasErrors()) {
                    $firstError = '';
                    foreach ($httpWxAuthAccessToken->getFirstErrors() as $message) {
                        $firstError = $message;
                        break;
                    }
                    throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '205058'), ['first_error' => $firstError])), 205058);
                } elseif (!$httpWxAuthAccessToken->hasErrors()) {
                    throw new ServerErrorHttpException('WeChat third-party platform HTTP requests fail for unknown reasons!');
                }
            }
            // 保存 第三方平台 access_token、有效截止时间 到 Redis
            $componentAccessToken = [
                'component_access_token' => $httpWxAuthAccessTokenResult['data']['component_access_token'],
                'expires_at' => $time + $httpWxAuthAccessTokenResult['data']['expires_in'],
            ];
            $redis->set($componentAccessTokenKey, serialize($componentAccessToken));
            file_put_contents(Yii::getAlias('@runtime') . '/frontend-services-WxOpenAuthService-componentAccessToken-' . microtime(true) . '-' . mt_rand()  . '.txt', print_r($componentAccessToken, true), FILE_APPEND | LOCK_EX);
        }
        return true;
    }

3、获取第三方平台预授权码的实现中,与获取第三方平台令牌的实现类似。代码如下

    /**
     * 获取第三方平台预授权码
     *
     * @param $appId string  第三方平台 appId
     * @return bool
     * @throws ServerErrorHttpException
     * @throws NotFoundHttpException
     */    public static function getComponentPreAuthCode($appId)
    {
        $time = time();
        $redis = Yii::$app->redis;
        $redisCommandkeyPrefix = Yii::$app->params['redisCommand']['keyPrefix'];
        $componentAccessTokenKey = $redisCommandkeyPrefix . 'component_access_token:' . $appId;
        $componentAccessToken = $redis->get($componentAccessTokenKey);
        if (!$componentAccessToken) {
            throw new NotFoundHttpException(Yii::t('error', 214017), 214017);
        }
        $componentAccessToken = unserialize($componentAccessToken);
        $componentPreAuthCodeKey = $redisCommandkeyPrefix . 'component_pre_auth_code:' . $appId;
        // 获取 Redis 中的 component_pre_auth_code
        $componentPreAuthCode = $redis->get($componentPreAuthCodeKey);

        // 判断 Redis 中的 component_pre_auth_code 是否存在
        if ($componentPreAuthCode) {
            $componentPreAuthCode = unserialize($componentPreAuthCode);
        }
        if (!$componentPreAuthCode || (($componentPreAuthCode['expires_at'] - 900) < $time) ) {
            $data = [
                'component_appid' => $appId,
            ];
            $httpWxAuthAccessToken = new HttpWxAuthAccessToken();
            // http 请求获取 pre_auth_code
            $httpWxAuthAccessTokenResult = $httpWxAuthAccessToken->wxComponentPreAuthCode($data, $componentAccessToken['component_access_token']);
            if ($httpWxAuthAccessTokenResult === false) {
                if ($httpWxAuthAccessToken->hasErrors()) {
                    $firstError = '';
                    foreach ($httpWxAuthAccessToken->getFirstErrors() as $message) {
                        $firstError = $message;
                        break;
                    }
                    throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '217004'), ['first_error' => $firstError])), 217004);
                } elseif (!$httpWxAuthAccessToken->hasErrors()) {
                    throw new ServerErrorHttpException('WeChat third-party platform HTTP requests fail for unknown reasons!');
                }
            }
            // 保存 第三方平台 pre_auth_code、有效截止时间 到 Redis
            $componentPreAuthCode = [
                'pre_auth_code' => $httpWxAuthAccessTokenResult['data']['pre_auth_code'],
                'expires_at' => $time + $httpWxAuthAccessTokenResult['data']['expires_in'],
            ];
            $redis->set($componentPreAuthCodeKey, serialize($componentPreAuthCode));
            file_put_contents(Yii::getAlias('@runtime') . '/frontend-services-WxOpenAuthService-componentPreAuthCode-' . microtime(true) . '-' . mt_rand()  . '.txt', print_r($componentPreAuthCode, true), FILE_APPEND | LOCK_EX);
        }
        return true;
    }

4、HTTP 模型代码如下

    /**
     * HTTP请求,微信第三方平台通过 component_verify_ticket 获取第三方平台的 component_access_token
     * @param array $data 数据
     * 格式如下:
     *
     * [
     *     'component_appid' => 'wxd7f67f1792c6e238', // 第三方平台 appId
     *     'component_appsecret' => '9853ba602cae7a31b4dacd7978ad75c6', // 第三方平台 appSecret
     *     'component_verify_ticket' => 'ticket@@@wM91oELf8K9_3g8QCXJ9gkPXs6JN2AZjNjI7JEDhFL9MEWi00eEMIqqhPH338OFnwJ5cpjksZUGWYTQXqjsLMw', // 微信后台推送的 ticket
     *  ]
     *
     * @return array|false
     *
     * 响应格式如下:
     *
     * [
     *     'component_access_token' => '20_qD79ll9kFdMh3--X43qS-Tw8E04cuFEnoAUrbldWodKbFVg11VtlpJrRXVuVMs7fsVyEXbLByWT__P0o9-lCCWw0rsxD3yxFT3skdxsznhsMBrAKxlhOlRvbUWBsZMJ57oTLSnCMlWtdDv8tGMKjAJAYEC', // 第三方平台 access_token
     *     'expires_in' => 7200, // 有效期,单位:秒
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */    public function wxComponentAccessToken($data)
    {
        $response = Yii::$app->wxAuthHttp->createRequest()
            ->setMethod('post')
            ->setFormat('json')
            ->setUrl('cgi-bin/component/api_component_token')
            ->setData($data)
            ->send();
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            // 检查业务逻辑是否成功
            if (!isset($response->data['errcode'])) {
                $responseData = ['message' => '', 'data' => $response->data];
                return $responseData;
            } else {
                $this->addError('id', $response->data['errmsg']);
                return false;
            }
        } else {
            throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202227'), ['status_code' => $response->statusCode, 'error_code' => $response->data['errcode'], 'error' => $response->data['errmsg']])), 202227);
        }
    }

    /**
     * HTTP请求,微信第三方平台通过 component_access_token 获取第三方平台的预授权码
     * @param array $data 请求数据
     * 格式如下:
     * [
     *     'component_appid' => 'wxd7f67f1792c6e238', // 第三方平台 appId
     * ]
     * @param string $componentAccessToken  第三方平台的 component_access_token
     * @return array|false
     *
     * 响应格式如下:
     *
     * [
     *     'pre_auth_code' => 'preauthcode@@@IexVwWK9bIkK-0pEd8plLnza0O8oalvXz1JWah5nfaHBJ0CN8Z8Kucu8rX2yA_4l', // 预授权码
     *     'expires_in' => 1800, // 有效期,单位:秒
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     * @throws ServerErrorHttpException
     */    public function wxComponentPreAuthCode($data, $componentAccessToken)
    {
        $response = Yii::$app->wxAuthHttp->createRequest()
            ->setMethod('post')
            ->setFormat('json')
            ->setUrl('cgi-bin/component/api_create_preauthcode?component_access_token=' . $componentAccessToken)
            ->setData($data)
            ->send();
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            // 检查业务逻辑是否成功
            if (!isset($response->data['errcode'])) {
                $responseData = ['message' => '', 'data' => $response->data];
                return $responseData;
            } else {
                $this->addError('id', $response->data['errmsg']);
                return false;
            }
        } else {
            throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202228'), ['status_code' => $response->statusCode, 'error_code' => $response->data['errcode'], 'error' => $response->data['errmsg']])), 202228);
        }
    }

5、最终的结果,查看生成的日志文件。http 请求获取 component_access_token 平均间隔时间:1小时50分钟。http 请求获取 http 请求获取 pre_auth_code 平均间隔时间:20分钟。此方案的缺陷在于:一旦 component_verify_ticket 接收失败(如微信推送 component_verify_ticket 的服务器发生故障),则微信的 component_access_token、pre_auth_code 皆会过期,无法获取到最新值。因为仅有此入口为更新触发点。后续计划实现2个定时命令(间隔10分钟运行一次),分别基于 component_verify_ticket 更新 component_access_token、基于 component_access_token 更新 pre_auth_code。避免出现因为 component_verify_ticket 接收失败而无法更新 component_access_token、pre_auth_code 的情况。官方建议。如图1

图1

6、查看 Redis 中的缓存数据。如图2

图2

7、查看生成的日志文件。以确认 http 请求的时间间隔。符合预期。http 请求获取 component_access_token 平均间隔时间:(21 09:39 – 20 18:55) / 8 = 110.5 分钟。http 请求获取 component_access_token 最大间隔时间:(20 22:39 – 20 20:44) = 115 分钟(小于过期时间 120 分钟)。http 请求获取 pre_auth_code 平均间隔时间:(21 09:59 – 20 18:55) / 45 = 20 分钟。http 请求获取 pre_auth_code 最大间隔时间:(20 21:59 – 20 21:34) = 25 分钟(小于过期时间 30 分钟)。

[root@api-64796bf684-4dk5b runtime]# ls -lrt
-rw-r--r-- 1 nginx nginx  231 Jul 20 18:55 frontend-services-WxOpenAuthService-componentAccessToken-1626778505.5976-2090872825.txt
-rw-r--r-- 1 nginx nginx  231 Jul 20 20:44 frontend-services-WxOpenAuthService-componentAccessToken-1626785088.6584-1698188199.txt
-rw-r--r-- 1 nginx nginx  231 Jul 20 22:39 frontend-services-WxOpenAuthService-componentAccessToken-1626791942.0405-1205529613.txt
-rw-r--r-- 1 nginx nginx  231 Jul 21 00:24 frontend-services-WxOpenAuthService-componentAccessToken-1626798260.5317-841190668.txt
-rw-r--r-- 1 nginx nginx  231 Jul 21 02:14 frontend-services-WxOpenAuthService-componentAccessToken-1626804844.4907-2046370607.txt
-rw-r--r-- 1 nginx nginx  231 Jul 21 04:04 frontend-services-WxOpenAuthService-componentAccessToken-1626811446.0864-1972564751.txt
-rw-r--r-- 1 nginx nginx  231 Jul 21 05:54 frontend-services-WxOpenAuthService-componentAccessToken-1626818067.4014-922194254.txt
-rw-r--r-- 1 nginx nginx  231 Jul 21 07:45 frontend-services-WxOpenAuthService-componentAccessToken-1626824703.4927-671650863.txt
-rw-r--r-- 1 nginx nginx  231 Jul 21 09:39 frontend-services-WxOpenAuthService-componentAccessToken-1626831557.828-269483974.txt
[root@api-64796bf684-4dk5b runtime]# 
[root@api-64796bf684-4dk5b runtime]# ls -lrt
-rw-r--r-- 1 nginx nginx  165 Jul 20 18:55 frontend-services-WxOpenAuthService-componentPreAuthCode-1626778505.8489-752113831.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 19:15 frontend-services-WxOpenAuthService-componentPreAuthCode-1626779710.0295-1883392791.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 19:35 frontend-services-WxOpenAuthService-componentPreAuthCode-1626780909.2572-2030512685.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 19:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626782090.5806-110245625.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 20:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626783290.1634-865268997.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 20:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626784491.401-904668390.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 20:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626785681.1526-312599467.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 21:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626786884.6617-148824611.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 21:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626788086.7058-993076861.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 21:59 frontend-services-WxOpenAuthService-componentPreAuthCode-1626789542.8081-1843679886.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 22:19 frontend-services-WxOpenAuthService-componentPreAuthCode-1626790746.9546-2130813835.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 22:39 frontend-services-WxOpenAuthService-componentPreAuthCode-1626791942.3167-1090847874.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 22:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626792884.6659-1225024664.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 23:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626794063.5141-313962662.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 23:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626795259.6539-1501709505.txt
-rw-r--r-- 1 nginx nginx  165 Jul 20 23:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626796457.9117-1972547675.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 00:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626797660.4519-582262606.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 00:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626798857.7039-272816531.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 00:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626800055.7545-1335033710.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 01:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626801254.9479-933596508.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 01:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626802448.9506-533002721.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 01:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626803664.2556-451414973.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 02:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626804844.7527-849127051.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 02:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626806043.7185-2047662025.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 02:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626807243.118-1751995970.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 03:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626808449.8901-1639170548.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 03:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626809644.1905-1681815156.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 03:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626810849.7381-1770515381.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 04:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626812045.9617-1614735470.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 04:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626813252.8063-1859536354.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 04:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626814453.7108-802504726.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 05:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626815669.4836-1412803818.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 05:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626816865.4676-278503141.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 05:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626818067.6856-1726415446.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 06:14 frontend-services-WxOpenAuthService-componentPreAuthCode-1626819272.89-123856324.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 06:34 frontend-services-WxOpenAuthService-componentPreAuthCode-1626820477.3397-694428578.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 06:54 frontend-services-WxOpenAuthService-componentPreAuthCode-1626821679.8346-1999505075.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 07:15 frontend-services-WxOpenAuthService-componentPreAuthCode-1626822907.5068-670400676.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 07:35 frontend-services-WxOpenAuthService-componentPreAuthCode-1626824107.4827-1497989531.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 07:55 frontend-services-WxOpenAuthService-componentPreAuthCode-1626825308.6138-127964288.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 08:19 frontend-services-WxOpenAuthService-componentPreAuthCode-1626826759.7268-2073402842.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 08:38 frontend-services-WxOpenAuthService-componentPreAuthCode-1626827916.6763-1465280404.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 08:59 frontend-services-WxOpenAuthService-componentPreAuthCode-1626829158.7873-2084121660.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 09:18 frontend-services-WxOpenAuthService-componentPreAuthCode-1626830319.8617-1775614952.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 09:39 frontend-services-WxOpenAuthService-componentPreAuthCode-1626831558.0899-1560152765.txt
-rw-r--r-- 1 nginx nginx  165 Jul 21 09:59 frontend-services-WxOpenAuthService-componentPreAuthCode-1626832767.0986-1848114078.txt
[root@api-64796bf684-4dk5b runtime]# 

 

永夜