请问大家都是如何替换 easywechat 的 access_token 缓存的

1. 运行环境

1). 当前使用的 Laravel 版本?

9.27.0

2). 当前使用的 php/php-fpm 版本?

PHP 版本:

8.1.9

php-fpm 版本:

使用 octane 加速器

3). 当前系统

apline:3.16

4). 业务环境

开发环境

2. 问题描述?

我正在开发一个扩展包,使用 easywechat 访问公众平台的服务,考虑到以后可能会部署到集群上,所以希望将 access_token 保存到数据库和分布式缓存中。请问大家都是怎么做的?

我去看了一下目前执行的结果,发现 easywechat 默认使用的是 Symfony\Component\Cache\Adapter\FilesystemAdapter 将数据缓存到系统默认的临时目录中的文件中,我想这样在集群中会导致 access_token 被不停的刷新。所以想采用分布式缓存替换掉这个存储。

另外发现一个问题:

easywechat 中的代码中采用了 appid 和 秘钥明文 拼接缓存key进行存储,实际缓存文件名也确实如此。代码如下(版本 6.7.4):

    public function getKey(): string
    {
        return $this->key ?? $this->key = sprintf('official_account.access_token.%s.%s', $this->appId, $this->secret);
    }

这样我认为不太安全,我认为至少应该用秘钥的哈希值构造缓存 key 。

《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
1年前 评论
sanders (楼主) 1年前
putyy (作者) 1年前
讨论数量: 11
1年前 评论
sanders (楼主) 1年前
putyy (作者) 1年前

easywechat.com/4.x/customize/cache...
接受微信回传的时候用redis去缓存,获取初始化的时候也要绑定一次,不然好像会报错,因为默认取得是文件缓存

1年前 评论
o扬帆远航x 1年前
sanders (楼主) 1年前
o扬帆远航x 1年前
sanders (楼主) 1年前
sanders (楼主) 1年前

好像有个方法是 setAccessToken 吧

1年前 评论
sanders

先再次感谢楼上两位朋友。问题最终解决了,但与4.x和5.x版本的文档中不同,6.7版本中找不到 rebind() 方法,但可以通过 setCache() 方法,来替换缓存实例,同时也可以替换 AccessToken 实例。

我替换缓存的方法

缓存实例需要实现 CacheInterface 接口,我目前还是用的 Symfony\Component\Cache\Psr16Cache ,其构造函数参数需要是个实现了 Psr\Cache\CacheItemPoolInterface 的实例,我看到 app('cache.psr6') 注入的实例实现了该接口,于是用其进行替换。同时替换的还有 AccessToken 实例。

    /**
     * 根据配置获取公众号应用
     *
     * @param array $options
     * @return Application|null
     * @throws InvalidArgumentException
     */
    public function getWechatOfficialApplicationByOptions(array $options): ?Application
    {
        $wechatApplication  = new Application(
            array_merge(config('wechat_official_account.app'), [
                'app_id'    => data_get($options,'appId'),
                'secret'    => data_get($options,'secret'),
                'token'     => data_get($options,'token'),
                'aes_key'   => data_get($options,'aesKey'),
            ])
        );
        $wechatApplication->setCache(new Psr16Cache(app('cache.psr6')));
        $wechatApplication->setAccessToken(new AccessToken(
            appId: data_get($options,'appId'),
            secret: data_get($options,'secret'),
            cache: $wechatApplication->getCache(),
            httpClient: $wechatApplication->getHttpClient()
        ));

        return  $this->wechatOfficialApplication($wechatApplication);
    }

对 AccessToken 的强化

替换的AccessToken实例对缓存键中的秘钥部分做了简单的哈希编码,使用 laravel 的加密方法对token做了加密后存储,对请求令牌失败的情况写入了日志。由于是给自己的业务用的,所以直接用了框架的特性,没有考虑太多通用性。

/**
 * 使用 Laravel 框架特性重写 EasyWeChat\OfficialAccount\AccessToken 部分方法
 *
 * 安全:秘钥不直接存储入缓存引擎
 * 安全:令牌加密存储
 * 监控:记录请求获取令牌失败到日志
 */

use EasyWeChat\Kernel\Exceptions\HttpException;
use EasyWeChat\OfficialAccount\AccessToken as EasyWechatAccessToken;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Psr\SimpleCache\InvalidArgumentException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

class AccessToken extends EasyWechatAccessToken
{
    /**
     * 重写获取缓存键 秘钥计算哈希值后构造键
     *
     * @return string
     */
    public function getKey(): string
    {
        return $this->key ?? $this->key = sprintf('official_account.access_token.%s.%s', $this->appId, md5($this->secret));
    }

    /**
     * 重写获取令牌逻辑 缓存令牌解密 获取异常写入日志
     *
     * @return string
     * @throws HttpException
     * @throws InvalidArgumentException
     * @throws ClientExceptionInterface
     * @throws DecodingExceptionInterface
     * @throws RedirectionExceptionInterface
     * @throws ServerExceptionInterface
     * @throws TransportExceptionInterface
     */
    public function getToken(): string
    {
        $token = $this->cache->get($this->getKey());

        if (!!$token && is_string($token)) {

            return Crypt::decryptString($token);
        }

        try {
            return $this->refresh();
        } catch (HttpException $exception) {
            Log::channel('wechat_official_account_access_token_fail')->critical($exception->getMessage());
            throw $exception;
        }
    }

    /**
     * 重写令牌刷新逻辑 令牌加密存储
     *
     * @return string
     * @throws ClientExceptionInterface
     * @throws DecodingExceptionInterface
     * @throws HttpException
     * @throws InvalidArgumentException
     * @throws RedirectionExceptionInterface
     * @throws ServerExceptionInterface
     * @throws TransportExceptionInterface
     */
    public function refresh(): string
    {
        $response = $this->httpClient->request(
            'GET',
            'cgi-bin/token',
            [
                'query' => [
                    'grant_type' => 'client_credential',
                    'appid' => $this->appId,
                    'secret' => $this->secret,
                ],
            ]
        )->toArray(false);

        if (empty($response['access_token'])) {
            throw new HttpException('Failed to get access_token: '.json_encode($response, JSON_UNESCAPED_UNICODE));
        }

        $this->cache->set($this->getKey(), Crypt::encryptString($response['access_token']), intval($response['expires_in']));

        return $response['access_token'];
    }
}
1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!