// +---------------------------------------------------------------------- namespace crmeb\services\easywechat\open3rd; use crmeb\exceptions\ApiException; use crmeb\services\HttpService; /** * Class AccessTokenServeService * @package crmeb\services */ class AccessToken extends HttpService { /** * 第三方平台 appid * @var string */ protected $component_appid; /** * 第三方平台 appsecret * @var string */ protected $component_appsecret; /** * 微信后台推送的 ticket * @var string */ protected $component_verify_ticket; /** * @var Cache|null */ protected $cache; /** * 第三方平台token * @var string */ protected $component_access_token; /** * 授权方appid * @var string */ protected $authorizer_appid; /** * 接口调用令牌(在授权的公众号/小程序具备 API 权限时,才有此返回值) * @var string */ protected $authorizer_access_token; /** * 刷新令牌(在授权的公众号具备API权限时,才有此返回值),刷新令牌主要用于第三方平台获取和刷新已授权用户的 authorizer_access_token。一旦丢失,只能让用户重新授权,才能再次拿到新的刷新令牌。用户重新授权后,之前的刷新令牌会失效 * @var string */ protected $authorizer_refresh_token; /** * @var string */ protected $cacheTokenPrefix = "component_access_token_crmeb"; /** * 获取第三方平台token * @var string */ const TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/component/api_component_token'; /** * 使用授权码获取授权信息 */ const AUTH_INFO = 'https://api.weixin.qq.com/cgi-bin/component/api_query_auth'; /** * 获取、刷新接口调用token */ const AUTHORIZER_TOKEN = 'https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token'; /** * AccessTokenServeService constructor. * @param string $component_appid * @param string $component_appsecret * @param string $component_verify_ticket * @param string $authorizer_appid * @param Cache|null $cache */ public function __construct(string $component_appid, string $component_appsecret, string $component_verify_ticket, string $authorizer_appid = '', $cache = null) { if (!$cache) { $cache = app()->make(\crmeb\services\CacheService::class); } $this->component_appid = $component_appid; $this->component_appsecret = $component_appsecret; $this->component_verify_ticket = $component_verify_ticket; $this->authorizer_appid = $authorizer_appid; $this->cache = $cache; } /** * 获取配置 * @return array */ public function getConfig() { return [ 'component_appid' => $this->component_appid, 'component_appsecret' => $this->component_appsecret, 'component_verify_ticket' => $this->component_verify_ticket, ]; } /** * @return string */ public function getComponentAppid() { return $this->component_appid; } /** * @return string */ public function getComponentAppsecret() { return $this->component_appsecret; } /** * @return string */ public function getAuthorizerAppid() { return $this->authorizer_appid; } /** * 获取第三方缓存token * @return mixed * @throws \Psr\SimpleCache\InvalidArgumentException */ public function getComponentToken() { $accessTokenKey = md5($this->component_appid . '_' . $this->component_appid . '_' . $this->component_verify_ticket . '_' . $this->cacheTokenPrefix); $component_access_token = $this->cache->get($accessTokenKey); if (!$component_access_token) { $getToken = $this->getTokenFromServer(); $this->cache->set($accessTokenKey, $getToken['component_access_token'], $getToken['expires_in'] ? $getToken['expires_in'] - 200 : 7000); $component_access_token = $getToken['component_access_token']; } $this->component_access_token = $component_access_token; return $component_access_token; } /** * 从服务器获取token * @return mixed */ public function getTokenFromServer() { $config = $this->getConfig(); if (!$config['component_appid'] || !$config['component_appsecret']) { throw new ApiException('请先配置第三方component_appid、component_appsecret'); } if (!$config['component_verify_ticket']) { throw new ApiException('未配置微信开放平台或者未收到推送ticket,请等待10分钟后再试'); } $res = $this->postRequest(self::TOKEN_URL, $config); $res = json_decode($res, true); if (!$res || $res['errcode'] != 0 || !isset($res['component_access_token']) || !$res['component_access_token']) { throw new ApiException('获取component_access_token失败,原因:' . $res['errmsg']); } return $res; } /** * 获取授权方token * @param $authorizer_appid * @return mixed */ public function getAccessToken($authorizer_appid) { $accessTokenKey = md5('authorizer_access_token' . $authorizer_appid . '_' . $this->cacheTokenPrefix); $authorizer_access_token = $this->cache->get($accessTokenKey); if (!$authorizer_access_token) { $refreshTokenKey = md5('authorizer_refresh_token' . $authorizer_appid . '_' . $this->cacheTokenPrefix); $authorizer_refresh_token = $this->cache->get($refreshTokenKey); if (!$authorizer_refresh_token) { throw new ApiException('请重新授权'); } $res = $this->freshAuthorizationToken($authorizer_appid, $authorizer_refresh_token); $this->cache->set(md5('authorizer_access_token' . $res['authorizer_appid'] . '_' . $this->cacheTokenPrefix), $res['authorizer_access_token'], $res['expires_in'] ? $res['expires_in'] - 200 : 7000); $this->cache->set(md5('authorizer_refrssh_token' . $res['authorizer_appid'] . '_' . $this->cacheTokenPrefix), $res['authorizer_refresh_token'], 30 * 24 * 3600); $authorizer_access_token = $res['authorizer_access_token']; } return $authorizer_access_token; } /** * 获取授权信息 * @param $authorization_code 授权码 * @return authorizer_appid string 授权方 appid * @return authorizer_access_token string 接口调用令牌(在授权的公众号/小程序具备 API 权限时,才有此返回值) * @return authorizer_refresh_token string 刷新令牌(在授权的公众号具备API权限时,才有此返回值),刷新令牌主要用于第三方平台获取和刷新已授权用户的 authorizer_access_token。一旦丢失,只能让用户重新授权,才能再次拿到新的刷新令牌。用户重新授权后,之前的刷新令牌会失效 * @return array|bool|mixed */ public function getAuthorizationInfo($authorization_code) { $res = $this->httpRequest(self::AUTH_INFO, ['authorization_code' => $authorization_code], false); if (!$res || $res['errcode'] != 0 || !isset($res['authorization_info']) || !$res['authorization_info']) { throw new ApiException('获取authorizer_access_token失败'); } $res = $res['authorization_info']; $this->cache->set(md5('authorizer_access_token' . $res['authorizer_appid'] . '_' . $this->cacheTokenPrefix), $res['authorizer_access_token'], $res['expires_in'] ? $res['expires_in'] - 200 : 7000); //在授权的公众号具备API权限时,才有此返回值 if (isset($res['authorizer_refrsh_token'])) { $this->cache->set(md5('authorizer_refresh_token' . $res['authorizer_appid'] . '_' . $this->cacheTokenPrefix), $res['authorizer_refrsh_token'], 30 * 24 * 3600); } return $res; } /** * 获取/刷新接口调用令牌 * @param $authorizer_appid 授权方appid * @param $authorizer_refresh_token 刷新令牌,获取授权信息时得到 * @return authorizer_access_token string 授权方令牌 * @return authorizer_refresh_token string 刷新令牌 * @return array|bool|mixed */ public function freshAuthorizationToken($authorizer_appid, $authorizer_refresh_token) { $res = $this->postRequest(self::AUTHORIZER_TOKEN, ['component_access_token' => $this->component_access_token, 'authorizer_appid' => $authorizer_appid, 'authorizer_refresh_token' => $authorizer_refresh_token]); if (!$res || $res['errcode'] != 0 || !isset($res['authorizer_access_token']) || !$res['authorizer_access_token']) { throw new ApiException('刷新authorizer_access_token失败,请重新获取授权'); } return $res; } /** * 请求 * @param string $url * @param array $data * @param string $method * @param bool $isHeader * @return array|mixed */ public function httpRequest(string $url, array $data = [], bool $is_atuh = true, string $method = 'POST') { if (!$is_atuh) { $this->getComponentToken(); if (!$this->component_access_token) { throw new ApiException('配置已更改或component_access_token已失效'); } $url .= '?component_access_token=' . $this->component_access_token; $data = array_merge($data, ['component_appid' => $this->component_appid]); } else { if (!$this->authorizer_appid) { throw new ApiException('缺少授权方authorizer_appid'); } $access_token = $this->getAccessToken($this->authorizer_appid); if (!$access_token) { throw new ApiException('配置已更改或授权已失效请重新授权'); } $url .= '?access_token=' . $access_token; } $res = $this->request($url, $method, $data); if (!$res) { throw new ApiException('请求微信服务器发生异常,请稍后重试'); } return json_decode($res, true) ?: false; } /** * XML数据转换成array数组 * @param string $xml * @return array */ public function xmlToArray($xml) { // 禁止引用外部xml实体 libxml_disable_entity_loader(true); $res = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); return (array)$res; } /** * 对解密后的明文进行补位删除 * @param decrypted 解密后的明文 * @return 删除填充补位后的明文 */ public function decode($text) { $pad = ord(substr($text, -1)); if ($pad < 1 || $pad > 32) { $pad = 0; } return substr($text, 0, (strlen($text) - $pad)); } /** * 对密文进行解密 * @param string $encodingAesKey 解密 * @param string $encrypted 需要解密的密文 * @return string 解密得到的明文 */ public function decrypt($encodingAesKey, $encrypted) { try { //使用BASE64对需要解密的字符串进行解码 $ciphertext_dec = base64_decode($encrypted); $iv = substr(base64_decode($encodingAesKey . "="), 0, 16); $decrypted = openssl_decrypt($ciphertext_dec, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); } catch (\Throwable $e) { throw new ApiException($e->getMessage()); } try { //去除补位字符 $result = $this->decode($decrypted); //去除16位随机字符串,网络字节序和AppId if (strlen($result) < 16) return ""; $content = substr($result, 16, strlen($result)); $len_list = unpack("N", substr($content, 0, 4)); $xml_len = $len_list[1]; $xml_content = substr($content, 4, $xml_len); } catch (\Throwable $e) { throw new ApiException($e->getMessage()); } return $xml_content; } }