WeChat.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <?php
  2. namespace Overtrue\Socialite\Providers;
  3. use Overtrue\Socialite\Exceptions\InvalidArgumentException;
  4. use Overtrue\Socialite\User;
  5. use Psr\Http\Message\ResponseInterface;
  6. /**
  7. * @see http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html [WeChat - 公众平台OAuth文档]
  8. * @see https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN
  9. * [网站应用微信登录开发指南]
  10. */
  11. class WeChat extends Base
  12. {
  13. public const NAME = 'wechat';
  14. protected string $baseUrl = 'https://api.weixin.qq.com/sns';
  15. protected array $scopes = ['snsapi_login'];
  16. protected bool $withCountryCode = false;
  17. protected ?array $component = null;
  18. protected ?string $openid = null;
  19. public function __construct(array $config)
  20. {
  21. parent::__construct($config);
  22. if ($this->getConfig()->has('component')) {
  23. $this->prepareForComponent((array) $this->getConfig()->get('component'));
  24. }
  25. }
  26. /**
  27. * @param string $openid
  28. *
  29. * @return $this
  30. */
  31. public function withOpenid(string $openid): self
  32. {
  33. $this->openid = $openid;
  34. return $this;
  35. }
  36. public function withCountryCode()
  37. {
  38. $this->withCountryCode = true;
  39. return $this;
  40. }
  41. /**
  42. * @param string $code
  43. *
  44. * @return array
  45. * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException|\GuzzleHttp\Exception\GuzzleException
  46. */
  47. public function tokenFromCode(string $code): array
  48. {
  49. $response = $this->getTokenFromCode($code);
  50. return $this->normalizeAccessTokenResponse($response->getBody()->getContents());
  51. }
  52. /**
  53. * @param array $componentConfig ['id' => xxx, 'token' => xxx]
  54. *
  55. * @return \Overtrue\Socialite\Providers\WeChat
  56. * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException
  57. */
  58. public function withComponent(array $componentConfig)
  59. {
  60. $this->prepareForComponent($componentConfig);
  61. return $this;
  62. }
  63. public function getComponent()
  64. {
  65. return $this->component;
  66. }
  67. protected function getAuthUrl(): string
  68. {
  69. $path = 'oauth2/authorize';
  70. if (in_array('snsapi_login', $this->scopes)) {
  71. $path = 'qrconnect';
  72. }
  73. return $this->buildAuthUrlFromBase("https://open.weixin.qq.com/connect/{$path}");
  74. }
  75. /**
  76. * @param string $url
  77. *
  78. * @return string
  79. */
  80. protected function buildAuthUrlFromBase(string $url): string
  81. {
  82. $query = http_build_query($this->getCodeFields(), '', '&', $this->encodingType);
  83. return $url . '?' . $query . '#wechat_redirect';
  84. }
  85. protected function getCodeFields(): array
  86. {
  87. if (!empty($this->component)) {
  88. $this->with(array_merge($this->parameters, ['component_appid' => $this->component['id']]));
  89. }
  90. return array_merge([
  91. 'appid' => $this->getClientId(),
  92. 'redirect_uri' => $this->redirectUrl,
  93. 'response_type' => 'code',
  94. 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator),
  95. 'state' => $this->state ?: md5(uniqid()),
  96. 'connect_redirect' => 1,
  97. ], $this->parameters);
  98. }
  99. protected function getTokenUrl(): string
  100. {
  101. if (!empty($this->component)) {
  102. return $this->baseUrl . '/oauth2/component/access_token';
  103. }
  104. return $this->baseUrl . '/oauth2/access_token';
  105. }
  106. /**
  107. * @param string $code
  108. *
  109. * @return \Overtrue\Socialite\User
  110. * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException|\GuzzleHttp\Exception\GuzzleException
  111. */
  112. public function userFromCode(string $code): User
  113. {
  114. if (in_array('snsapi_base', $this->scopes)) {
  115. return $this->mapUserToObject(\json_decode($this->getTokenFromCode($code)->getBody()->getContents(), true) ?? []);
  116. }
  117. $token = $this->tokenFromCode($code);
  118. $this->withOpenid($token['openid']);
  119. $user = $this->userFromToken($token[$this->accessTokenKey]);
  120. return $user->setRefreshToken($token['refresh_token'])
  121. ->setExpiresIn($token['expires_in']);
  122. }
  123. /**
  124. * @param string $token
  125. *
  126. * @return array
  127. * @throws \GuzzleHttp\Exception\GuzzleException
  128. */
  129. protected function getUserByToken(string $token): array
  130. {
  131. $language = $this->withCountryCode ? null : (isset($this->parameters['lang']) ? $this->parameters['lang'] : 'zh_CN');
  132. $response = $this->getHttpClient()->get($this->baseUrl . '/userinfo', [
  133. 'query' => array_filter([
  134. 'access_token' => $token,
  135. 'openid' => $this->openid,
  136. 'lang' => $language,
  137. ]),
  138. ]);
  139. return \json_decode($response->getBody()->getContents(), true) ?? [];
  140. }
  141. /**
  142. * @param array $user
  143. *
  144. * @return \Overtrue\Socialite\User
  145. */
  146. protected function mapUserToObject(array $user): User
  147. {
  148. return new User([
  149. 'id' => $user['openid'] ?? null,
  150. 'name' => $user['nickname'] ?? null,
  151. 'nickname' => $user['nickname'] ?? null,
  152. 'avatar' => $user['headimgurl'] ?? null,
  153. 'email' => null,
  154. ]);
  155. }
  156. /**
  157. * @param string $code
  158. *
  159. * @return array
  160. */
  161. protected function getTokenFields(string $code): array
  162. {
  163. if (!empty($this->component)) {
  164. return [
  165. 'appid' => $this->getClientId(),
  166. 'component_appid' => $this->component['id'],
  167. 'component_access_token' => $this->component['token'],
  168. 'code' => $code,
  169. 'grant_type' => 'authorization_code',
  170. ];
  171. }
  172. return [
  173. 'appid' => $this->getClientId(),
  174. 'secret' => $this->getClientSecret(),
  175. 'code' => $code,
  176. 'grant_type' => 'authorization_code',
  177. ];
  178. }
  179. /**
  180. * @param string $code
  181. *
  182. * @return \Psr\Http\Message\ResponseInterface
  183. * @throws \GuzzleHttp\Exception\GuzzleException
  184. */
  185. protected function getTokenFromCode(string $code): ResponseInterface
  186. {
  187. return $this->getHttpClient()->get($this->getTokenUrl(), [
  188. 'headers' => ['Accept' => 'application/json'],
  189. 'query' => $this->getTokenFields($code),
  190. ]);
  191. }
  192. protected function prepareForComponent(array $component)
  193. {
  194. $config = [];
  195. foreach ($component as $key => $value) {
  196. if (\is_callable($value)) {
  197. $value = \call_user_func($value, $this);
  198. }
  199. switch ($key) {
  200. case 'id':
  201. case 'app_id':
  202. case 'component_app_id':
  203. $config['id'] = $value;
  204. break;
  205. case 'token':
  206. case 'app_token':
  207. case 'access_token':
  208. case 'component_access_token':
  209. $config['token'] = $value;
  210. break;
  211. }
  212. }
  213. if (2 !== count($config)) {
  214. throw new InvalidArgumentException('Please check your config arguments is available.');
  215. }
  216. if (1 === count($this->scopes) && in_array('snsapi_login', $this->scopes)) {
  217. $this->scopes = ['snsapi_base'];
  218. }
  219. $this->component = $config;
  220. }
  221. }