请问大家都是如何替换 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

好像有个方法是 setAccessToken 吧

1年前 评论
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年前
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年前 评论

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