WeChatWebApp.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. <?php
  2. namespace App\Servers\Wechat;
  3. /**
  4. * 微信网站应用
  5. * @author 唐远望
  6. * @version 1.0
  7. * @date 2026-01-19
  8. */
  9. class WeChatWebApp
  10. {
  11. private $appId;
  12. private $appSecret;
  13. private $redirectUri;
  14. /**
  15. * 构造函数
  16. *
  17. * @param string $appId 应用唯一标识
  18. * @param string $appSecret 应用密钥
  19. * @param string $redirectUri 授权回调地址
  20. */
  21. public function __construct()
  22. {
  23. $this->appId = config('wechat.openplat.app_id',[]);
  24. $this->appSecret = config('wechat.openplat.secret',[]);
  25. $this->redirectUri = urlencode(config('wechat.openplat.release_host_url',[]));
  26. }
  27. /**
  28. * 第一步:生成授权URL,引导用户跳转到微信授权页面
  29. *
  30. * @param string $scope 应用授权作用域
  31. * snsapi_base - 静默授权,不弹出授权页面,只能获取openid
  32. * snsapi_userinfo - 弹出授权页面,可获取用户信息
  33. * @param string $state 重定向后会带上state参数,开发者可以填写任意参数值
  34. * @return string 授权URL
  35. */
  36. public function getAuthorizeUrl($scope = 'snsapi_base', $state = 'STATE')
  37. {
  38. $url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={$this->appId}&redirect_uri={$this->redirectUri}&response_type=code&scope={$scope}&state={$state}#wechat_redirect";
  39. return $url;
  40. }
  41. /**
  42. * 第二步:通过code获取access_token和openid
  43. *
  44. * @param string $code 授权code
  45. * @return array|false 成功返回数组,失败返回false
  46. */
  47. public function getAccessTokenByCode($code)
  48. {
  49. $url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={$this->appId}&secret={$this->appSecret}&code={$code}&grant_type=authorization_code";
  50. $result = $this->httpGet($url);
  51. if ($result) {
  52. $data = json_decode($result, true);
  53. if (!isset($data['errcode'])) {
  54. return $data; // 成功返回
  55. } else {
  56. $this->logError("获取access_token失败", $data);
  57. return false;
  58. }
  59. }
  60. return false;
  61. }
  62. /**
  63. * 刷新access_token
  64. *
  65. * @param string $refreshToken 刷新token
  66. * @return array|false 成功返回数组,失败返回false
  67. */
  68. public function refreshAccessToken($refreshToken)
  69. {
  70. $url = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid={$this->appId}&grant_type=refresh_token&refresh_token={$refreshToken}";
  71. $result = $this->httpGet($url);
  72. if ($result) {
  73. $data = json_decode($result, true);
  74. if (!isset($data['errcode'])) {
  75. return $data; // 成功返回
  76. } else {
  77. $this->logError("刷新access_token失败", $data);
  78. return false;
  79. }
  80. }
  81. return false;
  82. }
  83. /**
  84. * 获取用户信息(需要scope为snsapi_userinfo)
  85. *
  86. * @param string $accessToken 接口调用凭证
  87. * @param string $openid 用户唯一标识
  88. * @return array|false 成功返回数组,失败返回false
  89. */
  90. public function getUserInfo($accessToken, $openid)
  91. {
  92. $url = "https://api.weixin.qq.com/sns/userinfo?access_token={$accessToken}&openid={$openid}&lang=zh_CN";
  93. $result = $this->httpGet($url);
  94. if ($result) {
  95. $data = json_decode($result, true);
  96. if (!isset($data['errcode'])) {
  97. return $data; // 成功返回
  98. } else {
  99. $this->logError("获取用户信息失败", $data);
  100. return false;
  101. }
  102. }
  103. return false;
  104. }
  105. /**
  106. * 验证access_token是否有效
  107. *
  108. * @param string $accessToken 接口调用凭证
  109. * @param string $openid 用户唯一标识
  110. * @return bool 是否有效
  111. */
  112. public function checkAccessToken($accessToken, $openid)
  113. {
  114. $url = "https://api.weixin.qq.com/sns/auth?access_token={$accessToken}&openid={$openid}";
  115. $result = $this->httpGet($url);
  116. if ($result) {
  117. $data = json_decode($result, true);
  118. if (isset($data['errcode']) && $data['errcode'] == 0) {
  119. return true; // 有效
  120. }
  121. }
  122. return false; // 无效
  123. }
  124. /**
  125. * 完整的授权流程处理
  126. *
  127. * @return array|false 成功返回用户信息数组,失败返回false
  128. */
  129. public function handleAuthorization()
  130. {
  131. // 检查是否有授权code
  132. if (!isset($_GET['code'])) {
  133. // 没有code,跳转到授权页面
  134. $url = $this->getAuthorizeUrl('snsapi_userinfo', 'authorize');
  135. header("Location: {$url}");
  136. exit;
  137. }
  138. // 获取授权code
  139. $code = $_GET['code'];
  140. // 通过code获取access_token
  141. $tokenData = $this->getAccessTokenByCode($code);
  142. if (!$tokenData) {
  143. return false;
  144. }
  145. // 存储token信息到session
  146. session_start();
  147. $_SESSION['wechat_access_token'] = $tokenData['access_token'];
  148. $_SESSION['wechat_refresh_token'] = $tokenData['refresh_token'];
  149. $_SESSION['wechat_openid'] = $tokenData['openid'];
  150. $_SESSION['wechat_token_expire'] = time() + $tokenData['expires_in'];
  151. return $tokenData;
  152. }
  153. /**
  154. * 获取当前有效的access_token(自动刷新)
  155. *
  156. * @return string|false 有效的access_token
  157. */
  158. public function getValidAccessToken()
  159. {
  160. session_start();
  161. // 检查session中是否有token信息
  162. if (!isset($_SESSION['wechat_access_token'])) {
  163. return false;
  164. }
  165. $accessToken = $_SESSION['wechat_access_token'];
  166. $refreshToken = $_SESSION['wechat_refresh_token'];
  167. $openid = $_SESSION['wechat_openid'];
  168. $expireTime = $_SESSION['wechat_token_expire'];
  169. // 检查token是否即将过期(提前5分钟刷新)
  170. if (time() > $expireTime - 300) {
  171. // 刷新token
  172. $newTokenData = $this->refreshAccessToken($refreshToken);
  173. if ($newTokenData) {
  174. // 更新session中的token信息
  175. $_SESSION['wechat_access_token'] = $newTokenData['access_token'];
  176. $_SESSION['wechat_refresh_token'] = $newTokenData['refresh_token'];
  177. $_SESSION['wechat_token_expire'] = time() + $newTokenData['expires_in'];
  178. $accessToken = $newTokenData['access_token'];
  179. } else {
  180. // 刷新失败,需要重新授权
  181. return false;
  182. }
  183. }
  184. return $accessToken;
  185. }
  186. /**
  187. * HTTP GET 请求
  188. *
  189. * @param string $url 请求URL
  190. * @return string|false 响应内容
  191. */
  192. private function httpGet($url)
  193. {
  194. if (function_exists('curl_init')) {
  195. $ch = curl_init();
  196. curl_setopt($ch, CURLOPT_URL, $url);
  197. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  198. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  199. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  200. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  201. $response = curl_exec($ch);
  202. if (curl_errno($ch)) {
  203. $this->logError("CURL错误", curl_error($ch));
  204. curl_close($ch);
  205. return false;
  206. }
  207. curl_close($ch);
  208. return $response;
  209. } else {
  210. // 备用方法:使用file_get_contents
  211. $context = stream_context_create([
  212. 'ssl' => [
  213. 'verify_peer' => false,
  214. 'verify_peer_name' => false,
  215. ]
  216. ]);
  217. return file_get_contents($url, false, $context);
  218. }
  219. }
  220. /**
  221. * 错误日志记录
  222. *
  223. * @param string $message 错误信息
  224. * @param mixed $data 错误数据
  225. */
  226. private function logError($message, $data = null)
  227. {
  228. $log = date('Y-m-d H:i:s') . " - {$message}";
  229. if ($data !== null) {
  230. $log .= " - " . (is_array($data) ? json_encode($data) : $data);
  231. }
  232. error_log($log . PHP_EOL, 3, 'wechat_oauth_error.log');
  233. }
  234. /**
  235. * 获取当前openid
  236. *
  237. * @return string|false openid
  238. */
  239. public function getOpenId()
  240. {
  241. session_start();
  242. return isset($_SESSION['wechat_openid']) ? $_SESSION['wechat_openid'] : false;
  243. }
  244. /**
  245. * 清除session中的授权信息
  246. */
  247. public function clearSession()
  248. {
  249. session_start();
  250. unset(
  251. $_SESSION['wechat_access_token'],
  252. $_SESSION['wechat_refresh_token'],
  253. $_SESSION['wechat_openid'],
  254. $_SESSION['wechat_token_expire']
  255. );
  256. }
  257. }