|
@@ -0,0 +1,304 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Servers\Wechat;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 微信网站应用
|
|
|
|
|
+ * @author 唐远望
|
|
|
|
|
+ * @version 1.0
|
|
|
|
|
+ * @date 2026-01-19
|
|
|
|
|
+ */
|
|
|
|
|
+class WeChatWebApp
|
|
|
|
|
+{
|
|
|
|
|
+ private $appId;
|
|
|
|
|
+ private $appSecret;
|
|
|
|
|
+ private $redirectUri;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构造函数
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $appId 应用唯一标识
|
|
|
|
|
+ * @param string $appSecret 应用密钥
|
|
|
|
|
+ * @param string $redirectUri 授权回调地址
|
|
|
|
|
+ */
|
|
|
|
|
+ public function __construct()
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->appId = config('wechat.openplat.app_id',[]);
|
|
|
|
|
+ $this->appSecret = config('wechat.openplat.secret',[]);
|
|
|
|
|
+ $this->redirectUri = urlencode(config('wechat.openplat.release_host_url',[]));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 第一步:生成授权URL,引导用户跳转到微信授权页面
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $scope 应用授权作用域
|
|
|
|
|
+ * snsapi_base - 静默授权,不弹出授权页面,只能获取openid
|
|
|
|
|
+ * snsapi_userinfo - 弹出授权页面,可获取用户信息
|
|
|
|
|
+ * @param string $state 重定向后会带上state参数,开发者可以填写任意参数值
|
|
|
|
|
+ * @return string 授权URL
|
|
|
|
|
+ */
|
|
|
|
|
+ public function getAuthorizeUrl($scope = 'snsapi_base', $state = 'STATE')
|
|
|
|
|
+ {
|
|
|
|
|
+ $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";
|
|
|
|
|
+ return $url;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 第二步:通过code获取access_token和openid
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $code 授权code
|
|
|
|
|
+ * @return array|false 成功返回数组,失败返回false
|
|
|
|
|
+ */
|
|
|
|
|
+ public function getAccessTokenByCode($code)
|
|
|
|
|
+ {
|
|
|
|
|
+ $url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={$this->appId}&secret={$this->appSecret}&code={$code}&grant_type=authorization_code";
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->httpGet($url);
|
|
|
|
|
+
|
|
|
|
|
+ if ($result) {
|
|
|
|
|
+ $data = json_decode($result, true);
|
|
|
|
|
+
|
|
|
|
|
+ if (!isset($data['errcode'])) {
|
|
|
|
|
+ return $data; // 成功返回
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $this->logError("获取access_token失败", $data);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 刷新access_token
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $refreshToken 刷新token
|
|
|
|
|
+ * @return array|false 成功返回数组,失败返回false
|
|
|
|
|
+ */
|
|
|
|
|
+ public function refreshAccessToken($refreshToken)
|
|
|
|
|
+ {
|
|
|
|
|
+ $url = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid={$this->appId}&grant_type=refresh_token&refresh_token={$refreshToken}";
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->httpGet($url);
|
|
|
|
|
+
|
|
|
|
|
+ if ($result) {
|
|
|
|
|
+ $data = json_decode($result, true);
|
|
|
|
|
+
|
|
|
|
|
+ if (!isset($data['errcode'])) {
|
|
|
|
|
+ return $data; // 成功返回
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $this->logError("刷新access_token失败", $data);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取用户信息(需要scope为snsapi_userinfo)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $accessToken 接口调用凭证
|
|
|
|
|
+ * @param string $openid 用户唯一标识
|
|
|
|
|
+ * @return array|false 成功返回数组,失败返回false
|
|
|
|
|
+ */
|
|
|
|
|
+ public function getUserInfo($accessToken, $openid)
|
|
|
|
|
+ {
|
|
|
|
|
+ $url = "https://api.weixin.qq.com/sns/userinfo?access_token={$accessToken}&openid={$openid}&lang=zh_CN";
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->httpGet($url);
|
|
|
|
|
+
|
|
|
|
|
+ if ($result) {
|
|
|
|
|
+ $data = json_decode($result, true);
|
|
|
|
|
+
|
|
|
|
|
+ if (!isset($data['errcode'])) {
|
|
|
|
|
+ return $data; // 成功返回
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $this->logError("获取用户信息失败", $data);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 验证access_token是否有效
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $accessToken 接口调用凭证
|
|
|
|
|
+ * @param string $openid 用户唯一标识
|
|
|
|
|
+ * @return bool 是否有效
|
|
|
|
|
+ */
|
|
|
|
|
+ public function checkAccessToken($accessToken, $openid)
|
|
|
|
|
+ {
|
|
|
|
|
+ $url = "https://api.weixin.qq.com/sns/auth?access_token={$accessToken}&openid={$openid}";
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->httpGet($url);
|
|
|
|
|
+
|
|
|
|
|
+ if ($result) {
|
|
|
|
|
+ $data = json_decode($result, true);
|
|
|
|
|
+
|
|
|
|
|
+ if (isset($data['errcode']) && $data['errcode'] == 0) {
|
|
|
|
|
+ return true; // 有效
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false; // 无效
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 完整的授权流程处理
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return array|false 成功返回用户信息数组,失败返回false
|
|
|
|
|
+ */
|
|
|
|
|
+ public function handleAuthorization()
|
|
|
|
|
+ {
|
|
|
|
|
+ // 检查是否有授权code
|
|
|
|
|
+ if (!isset($_GET['code'])) {
|
|
|
|
|
+ // 没有code,跳转到授权页面
|
|
|
|
|
+ $url = $this->getAuthorizeUrl('snsapi_userinfo', 'authorize');
|
|
|
|
|
+ header("Location: {$url}");
|
|
|
|
|
+ exit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取授权code
|
|
|
|
|
+ $code = $_GET['code'];
|
|
|
|
|
+
|
|
|
|
|
+ // 通过code获取access_token
|
|
|
|
|
+ $tokenData = $this->getAccessTokenByCode($code);
|
|
|
|
|
+
|
|
|
|
|
+ if (!$tokenData) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 存储token信息到session
|
|
|
|
|
+ session_start();
|
|
|
|
|
+ $_SESSION['wechat_access_token'] = $tokenData['access_token'];
|
|
|
|
|
+ $_SESSION['wechat_refresh_token'] = $tokenData['refresh_token'];
|
|
|
|
|
+ $_SESSION['wechat_openid'] = $tokenData['openid'];
|
|
|
|
|
+ $_SESSION['wechat_token_expire'] = time() + $tokenData['expires_in'];
|
|
|
|
|
+
|
|
|
|
|
+ return $tokenData;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取当前有效的access_token(自动刷新)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return string|false 有效的access_token
|
|
|
|
|
+ */
|
|
|
|
|
+ public function getValidAccessToken()
|
|
|
|
|
+ {
|
|
|
|
|
+ session_start();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查session中是否有token信息
|
|
|
|
|
+ if (!isset($_SESSION['wechat_access_token'])) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $accessToken = $_SESSION['wechat_access_token'];
|
|
|
|
|
+ $refreshToken = $_SESSION['wechat_refresh_token'];
|
|
|
|
|
+ $openid = $_SESSION['wechat_openid'];
|
|
|
|
|
+ $expireTime = $_SESSION['wechat_token_expire'];
|
|
|
|
|
+
|
|
|
|
|
+ // 检查token是否即将过期(提前5分钟刷新)
|
|
|
|
|
+ if (time() > $expireTime - 300) {
|
|
|
|
|
+ // 刷新token
|
|
|
|
|
+ $newTokenData = $this->refreshAccessToken($refreshToken);
|
|
|
|
|
+
|
|
|
|
|
+ if ($newTokenData) {
|
|
|
|
|
+ // 更新session中的token信息
|
|
|
|
|
+ $_SESSION['wechat_access_token'] = $newTokenData['access_token'];
|
|
|
|
|
+ $_SESSION['wechat_refresh_token'] = $newTokenData['refresh_token'];
|
|
|
|
|
+ $_SESSION['wechat_token_expire'] = time() + $newTokenData['expires_in'];
|
|
|
|
|
+
|
|
|
|
|
+ $accessToken = $newTokenData['access_token'];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 刷新失败,需要重新授权
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $accessToken;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * HTTP GET 请求
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $url 请求URL
|
|
|
|
|
+ * @return string|false 响应内容
|
|
|
|
|
+ */
|
|
|
|
|
+ private function httpGet($url)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (function_exists('curl_init')) {
|
|
|
|
|
+ $ch = curl_init();
|
|
|
|
|
+ curl_setopt($ch, CURLOPT_URL, $url);
|
|
|
|
|
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
|
|
|
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
|
|
|
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
|
|
|
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
|
|
|
|
+
|
|
|
|
|
+ $response = curl_exec($ch);
|
|
|
|
|
+
|
|
|
|
|
+ if (curl_errno($ch)) {
|
|
|
|
|
+ $this->logError("CURL错误", curl_error($ch));
|
|
|
|
|
+ curl_close($ch);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ curl_close($ch);
|
|
|
|
|
+ return $response;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 备用方法:使用file_get_contents
|
|
|
|
|
+ $context = stream_context_create([
|
|
|
|
|
+ 'ssl' => [
|
|
|
|
|
+ 'verify_peer' => false,
|
|
|
|
|
+ 'verify_peer_name' => false,
|
|
|
|
|
+ ]
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return file_get_contents($url, false, $context);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 错误日志记录
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $message 错误信息
|
|
|
|
|
+ * @param mixed $data 错误数据
|
|
|
|
|
+ */
|
|
|
|
|
+ private function logError($message, $data = null)
|
|
|
|
|
+ {
|
|
|
|
|
+ $log = date('Y-m-d H:i:s') . " - {$message}";
|
|
|
|
|
+
|
|
|
|
|
+ if ($data !== null) {
|
|
|
|
|
+ $log .= " - " . (is_array($data) ? json_encode($data) : $data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ error_log($log . PHP_EOL, 3, 'wechat_oauth_error.log');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取当前openid
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return string|false openid
|
|
|
|
|
+ */
|
|
|
|
|
+ public function getOpenId()
|
|
|
|
|
+ {
|
|
|
|
|
+ session_start();
|
|
|
|
|
+ return isset($_SESSION['wechat_openid']) ? $_SESSION['wechat_openid'] : false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 清除session中的授权信息
|
|
|
|
|
+ */
|
|
|
|
|
+ public function clearSession()
|
|
|
|
|
+ {
|
|
|
|
|
+ session_start();
|
|
|
|
|
+ unset(
|
|
|
|
|
+ $_SESSION['wechat_access_token'],
|
|
|
|
|
+ $_SESSION['wechat_refresh_token'],
|
|
|
|
|
+ $_SESSION['wechat_openid'],
|
|
|
|
|
+ $_SESSION['wechat_token_expire']
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|