AccessToken.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace crmeb\services\easywechat\open3rd;
  12. use crmeb\exceptions\ApiException;
  13. use crmeb\services\HttpService;
  14. /**
  15. * Class AccessTokenServeService
  16. * @package crmeb\services
  17. */
  18. class AccessToken extends HttpService
  19. {
  20. /**
  21. * 第三方平台 appid
  22. * @var string
  23. */
  24. protected $component_appid;
  25. /**
  26. * 第三方平台 appsecret
  27. * @var string
  28. */
  29. protected $component_appsecret;
  30. /**
  31. * 微信后台推送的 ticket
  32. * @var string
  33. */
  34. protected $component_verify_ticket;
  35. /**
  36. * @var Cache|null
  37. */
  38. protected $cache;
  39. /**
  40. * 第三方平台token
  41. * @var string
  42. */
  43. protected $component_access_token;
  44. /**
  45. * 授权方appid
  46. * @var string
  47. */
  48. protected $authorizer_appid;
  49. /**
  50. * 接口调用令牌(在授权的公众号/小程序具备 API 权限时,才有此返回值)
  51. * @var string
  52. */
  53. protected $authorizer_access_token;
  54. /**
  55. * 刷新令牌(在授权的公众号具备API权限时,才有此返回值),刷新令牌主要用于第三方平台获取和刷新已授权用户的 authorizer_access_token。一旦丢失,只能让用户重新授权,才能再次拿到新的刷新令牌。用户重新授权后,之前的刷新令牌会失效
  56. * @var string
  57. */
  58. protected $authorizer_refresh_token;
  59. /**
  60. * @var string
  61. */
  62. protected $cacheTokenPrefix = "component_access_token_crmeb";
  63. /**
  64. * 获取第三方平台token
  65. * @var string
  66. */
  67. const TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/component/api_component_token';
  68. /**
  69. * 使用授权码获取授权信息
  70. */
  71. const AUTH_INFO = 'https://api.weixin.qq.com/cgi-bin/component/api_query_auth';
  72. /**
  73. * 获取、刷新接口调用token
  74. */
  75. const AUTHORIZER_TOKEN = 'https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token';
  76. /**
  77. * AccessTokenServeService constructor.
  78. * @param string $component_appid
  79. * @param string $component_appsecret
  80. * @param string $component_verify_ticket
  81. * @param string $authorizer_appid
  82. * @param Cache|null $cache
  83. */
  84. public function __construct(string $component_appid, string $component_appsecret, string $component_verify_ticket, string $authorizer_appid = '', $cache = null)
  85. {
  86. if (!$cache) {
  87. $cache = app()->make(\crmeb\services\CacheService::class);
  88. }
  89. $this->component_appid = $component_appid;
  90. $this->component_appsecret = $component_appsecret;
  91. $this->component_verify_ticket = $component_verify_ticket;
  92. $this->authorizer_appid = $authorizer_appid;
  93. $this->cache = $cache;
  94. }
  95. /**
  96. * 获取配置
  97. * @return array
  98. */
  99. public function getConfig()
  100. {
  101. return [
  102. 'component_appid' => $this->component_appid,
  103. 'component_appsecret' => $this->component_appsecret,
  104. 'component_verify_ticket' => $this->component_verify_ticket,
  105. ];
  106. }
  107. /**
  108. * @return string
  109. */
  110. public function getComponentAppid()
  111. {
  112. return $this->component_appid;
  113. }
  114. /**
  115. * @return string
  116. */
  117. public function getComponentAppsecret()
  118. {
  119. return $this->component_appsecret;
  120. }
  121. /**
  122. * @return string
  123. */
  124. public function getAuthorizerAppid()
  125. {
  126. return $this->authorizer_appid;
  127. }
  128. /**
  129. * 获取第三方缓存token
  130. * @return mixed
  131. * @throws \Psr\SimpleCache\InvalidArgumentException
  132. */
  133. public function getComponentToken()
  134. {
  135. $accessTokenKey = md5($this->component_appid . '_' . $this->component_appid . '_' . $this->component_verify_ticket . '_' . $this->cacheTokenPrefix);
  136. $component_access_token = $this->cache->get($accessTokenKey);
  137. if (!$component_access_token) {
  138. $getToken = $this->getTokenFromServer();
  139. $this->cache->set($accessTokenKey, $getToken['component_access_token'], $getToken['expires_in'] ? $getToken['expires_in'] - 200 : 7000);
  140. $component_access_token = $getToken['component_access_token'];
  141. }
  142. $this->component_access_token = $component_access_token;
  143. return $component_access_token;
  144. }
  145. /**
  146. * 从服务器获取token
  147. * @return mixed
  148. */
  149. public function getTokenFromServer()
  150. {
  151. $config = $this->getConfig();
  152. if (!$config['component_appid'] || !$config['component_appsecret']) {
  153. throw new ApiException('请先配置第三方component_appid、component_appsecret');
  154. }
  155. if (!$config['component_verify_ticket']) {
  156. throw new ApiException('未配置微信开放平台或者未收到推送ticket,请等待10分钟后再试');
  157. }
  158. $res = $this->postRequest(self::TOKEN_URL, $config);
  159. $res = json_decode($res, true);
  160. if (!$res || $res['errcode'] != 0 || !isset($res['component_access_token']) || !$res['component_access_token']) {
  161. throw new ApiException('获取component_access_token失败,原因:' . $res['errmsg']);
  162. }
  163. return $res;
  164. }
  165. /**
  166. * 获取授权方token
  167. * @param $authorizer_appid
  168. * @return mixed
  169. */
  170. public function getAccessToken($authorizer_appid)
  171. {
  172. $accessTokenKey = md5('authorizer_access_token' . $authorizer_appid . '_' . $this->cacheTokenPrefix);
  173. $authorizer_access_token = $this->cache->get($accessTokenKey);
  174. if (!$authorizer_access_token) {
  175. $refreshTokenKey = md5('authorizer_refresh_token' . $authorizer_appid . '_' . $this->cacheTokenPrefix);
  176. $authorizer_refresh_token = $this->cache->get($refreshTokenKey);
  177. if (!$authorizer_refresh_token) {
  178. throw new ApiException('请重新授权');
  179. }
  180. $res = $this->freshAuthorizationToken($authorizer_appid, $authorizer_refresh_token);
  181. $this->cache->set(md5('authorizer_access_token' . $res['authorizer_appid'] . '_' . $this->cacheTokenPrefix), $res['authorizer_access_token'], $res['expires_in'] ? $res['expires_in'] - 200 : 7000);
  182. $this->cache->set(md5('authorizer_refrssh_token' . $res['authorizer_appid'] . '_' . $this->cacheTokenPrefix), $res['authorizer_refresh_token'], 30 * 24 * 3600);
  183. $authorizer_access_token = $res['authorizer_access_token'];
  184. }
  185. return $authorizer_access_token;
  186. }
  187. /**
  188. * 获取授权信息
  189. * @param $authorization_code 授权码
  190. * @return authorizer_appid string 授权方 appid
  191. * @return authorizer_access_token string 接口调用令牌(在授权的公众号/小程序具备 API 权限时,才有此返回值)
  192. * @return authorizer_refresh_token string 刷新令牌(在授权的公众号具备API权限时,才有此返回值),刷新令牌主要用于第三方平台获取和刷新已授权用户的 authorizer_access_token。一旦丢失,只能让用户重新授权,才能再次拿到新的刷新令牌。用户重新授权后,之前的刷新令牌会失效
  193. * @return array|bool|mixed
  194. */
  195. public function getAuthorizationInfo($authorization_code)
  196. {
  197. $res = $this->httpRequest(self::AUTH_INFO, ['authorization_code' => $authorization_code], false);
  198. if (!$res || $res['errcode'] != 0 || !isset($res['authorization_info']) || !$res['authorization_info']) {
  199. throw new ApiException('获取authorizer_access_token失败');
  200. }
  201. $res = $res['authorization_info'];
  202. $this->cache->set(md5('authorizer_access_token' . $res['authorizer_appid'] . '_' . $this->cacheTokenPrefix), $res['authorizer_access_token'], $res['expires_in'] ? $res['expires_in'] - 200 : 7000);
  203. //在授权的公众号具备API权限时,才有此返回值
  204. if (isset($res['authorizer_refrsh_token'])) {
  205. $this->cache->set(md5('authorizer_refresh_token' . $res['authorizer_appid'] . '_' . $this->cacheTokenPrefix), $res['authorizer_refrsh_token'], 30 * 24 * 3600);
  206. }
  207. return $res;
  208. }
  209. /**
  210. * 获取/刷新接口调用令牌
  211. * @param $authorizer_appid 授权方appid
  212. * @param $authorizer_refresh_token 刷新令牌,获取授权信息时得到
  213. * @return authorizer_access_token string 授权方令牌
  214. * @return authorizer_refresh_token string 刷新令牌
  215. * @return array|bool|mixed
  216. */
  217. public function freshAuthorizationToken($authorizer_appid, $authorizer_refresh_token)
  218. {
  219. $res = $this->postRequest(self::AUTHORIZER_TOKEN, ['component_access_token' => $this->component_access_token, 'authorizer_appid' => $authorizer_appid, 'authorizer_refresh_token' => $authorizer_refresh_token]);
  220. if (!$res || $res['errcode'] != 0 || !isset($res['authorizer_access_token']) || !$res['authorizer_access_token']) {
  221. throw new ApiException('刷新authorizer_access_token失败,请重新获取授权');
  222. }
  223. return $res;
  224. }
  225. /**
  226. * 请求
  227. * @param string $url
  228. * @param array $data
  229. * @param string $method
  230. * @param bool $isHeader
  231. * @return array|mixed
  232. */
  233. public function httpRequest(string $url, array $data = [], bool $is_atuh = true, string $method = 'POST')
  234. {
  235. if (!$is_atuh) {
  236. $this->getComponentToken();
  237. if (!$this->component_access_token) {
  238. throw new ApiException('配置已更改或component_access_token已失效');
  239. }
  240. $url .= '?component_access_token=' . $this->component_access_token;
  241. $data = array_merge($data, ['component_appid' => $this->component_appid]);
  242. } else {
  243. if (!$this->authorizer_appid) {
  244. throw new ApiException('缺少授权方authorizer_appid');
  245. }
  246. $access_token = $this->getAccessToken($this->authorizer_appid);
  247. if (!$access_token) {
  248. throw new ApiException('配置已更改或授权已失效请重新授权');
  249. }
  250. $url .= '?access_token=' . $access_token;
  251. }
  252. $res = $this->request($url, $method, $data);
  253. if (!$res) {
  254. throw new ApiException('请求微信服务器发生异常,请稍后重试');
  255. }
  256. return json_decode($res, true) ?: false;
  257. }
  258. /**
  259. * XML数据转换成array数组
  260. * @param string $xml
  261. * @return array
  262. */
  263. public function xmlToArray($xml)
  264. {
  265. // 禁止引用外部xml实体
  266. libxml_disable_entity_loader(true);
  267. $res = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
  268. return (array)$res;
  269. }
  270. /**
  271. * 对解密后的明文进行补位删除
  272. * @param decrypted 解密后的明文
  273. * @return 删除填充补位后的明文
  274. */
  275. public function decode($text)
  276. {
  277. $pad = ord(substr($text, -1));
  278. if ($pad < 1 || $pad > 32) {
  279. $pad = 0;
  280. }
  281. return substr($text, 0, (strlen($text) - $pad));
  282. }
  283. /**
  284. * 对密文进行解密
  285. * @param string $encodingAesKey 解密
  286. * @param string $encrypted 需要解密的密文
  287. * @return string 解密得到的明文
  288. */
  289. public function decrypt($encodingAesKey, $encrypted)
  290. {
  291. try {
  292. //使用BASE64对需要解密的字符串进行解码
  293. $ciphertext_dec = base64_decode($encrypted);
  294. $iv = substr(base64_decode($encodingAesKey . "="), 0, 16);
  295. $decrypted = openssl_decrypt($ciphertext_dec, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
  296. } catch (\Throwable $e) {
  297. throw new ApiException($e->getMessage());
  298. }
  299. try {
  300. //去除补位字符
  301. $result = $this->decode($decrypted);
  302. //去除16位随机字符串,网络字节序和AppId
  303. if (strlen($result) < 16)
  304. return "";
  305. $content = substr($result, 16, strlen($result));
  306. $len_list = unpack("N", substr($content, 0, 4));
  307. $xml_len = $len_list[1];
  308. $xml_content = substr($content, 4, $xml_len);
  309. } catch (\Throwable $e) {
  310. throw new ApiException($e->getMessage());
  311. }
  312. return $xml_content;
  313. }
  314. }