appId = $appId; $this->accessToken = $accessToken; $this->aes['sn'] = getenv('AES_SN'); $this->aes['key'] = getenv('AES_KEY'); $this->rsa['sn'] = getenv('RSA_SN'); $env = getenv('YII_ENV') == 'production' ? 'production' : 'dev'; $this->rsa['rsa-public-key'] = file_get_contents(__DIR__ . '/' . $env . '/rsa-public-key.txt'); $this->rsa['rsa-private-key'] = file_get_contents(__DIR__. '/' . $env . '/rsa-private-key.txt'); $this->cert['sn'] = getenv('CERT_SN'); $this->cert['cert-key'] = file_get_contents(__DIR__ . '/' . $env . '/api.cer'); } /** * Name:对外方法用于所有微信api的请求方法 * User: zcw * Date: 2023/7/14 * Time: 9:51 * @param $url * @param $reqData * @throws ErrorException * @throws \Exception */ public function request($url, $reqData) { $this->url = $url; $urls = $url . "?access_token=" . $this->accessToken; //1.数据加密 $newReData = $this->getRequestParam($url, $reqData); //2.获取签名 $signature = $this->getSignature($newReData); //本地验签 非必需 $checkLocalSig = $this->checkLocalSignature($newReData, $signature); if (!$checkLocalSig) { throw new ErrorException('本地验签错误'); } $appId = $this->appId; $headerArray = ['Wechatmp-Appid:' . $appId, 'Wechatmp-TimeStamp:' . $newReData['ts'], 'Wechatmp-Signature:' . $signature]; \Yii::info("请求头: " . json_encode($headerArray), 'wxapi_debug'); $data = $this->curlPost($urls, $newReData['reqData'], $headerArray); $headers = $this->httpParseHeaders($data['header']); $body = json_decode($data['body'], true); \Yii::info("响应: " . json_encode($body), 'wxapi_debug'); //请求平台报错 if (isset($body['errcode'])) { \Yii::error("微信API错误: " . json_encode($body), 'wxapi_debug'); throw new ErrorException($body['errmsg']); } // 3.响应参数验签 // $vertify = $this->vertifyResponse($data); // if (!$vertify) { // throw new ErrorException('响应参数验签失败'); // } //4.参数解密 return $this->jM($headers['Wechatmp-TimeStamp'], $body); } /** * Name:post请求 * User: zcw * Date: 2023/7/14 * Time: 9:19 * @param $url * @param $field * @param $header * @return array */ public function curlPost($url, $field, $header) { $headerArray = array("Content-type:application/json;charset=utf-8", "Accept:application/json"); $headerArray = array_merge($headerArray, $header); $curl = curl_init(); curl_setopt($curl, CURLOPT_HTTPHEADER, $headerArray); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, $field); //输出响应头部 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HEADER, true); $str = curl_exec($curl); $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); $headers = substr($str, 0, $headerSize); $body = substr($str, $headerSize); curl_close($curl); return ['body' => $body, 'header' => $headers]; } /** * Name:对外方法用于所有微信api的请求通道 * User: zcw * Date: 2023/7/14 * Time: 9:21 * @param $url * @param $reqData * @return array * @throws \Exception */ public function getRequestParam($url, $reqData) { $key = base64_decode($this->aes['key']); $sn = $this->aes['sn']; $appId = $this->appId; $time = time(); //16位随机字符 $nonce = rtrim(base64_encode(random_bytes(16)), '='); $addReq = ["_n" => $nonce, "_appid" => $appId, "_timestamp" => $time]; $realReq = array_merge($addReq, $reqData); $realReq = json_encode($realReq); //额外参数 $aad = $url . "|" . $appId . "|" . $time . "|" . $sn; //12位随机字符 $iv = random_bytes(12); $cipher = openssl_encrypt($realReq, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $tag, $aad); $iv = base64_encode($iv); $data = base64_encode($cipher); $authTag = base64_encode($tag); $reqData = ["iv" => $iv, "data" => $data, "authtag" => $authTag]; //校验本地加密是否正确 非必须 // $checkParam = $this->checkParam($key, $authTag, $iv, $data, $aad); return ['ts' => $time, 'reqData' => json_encode($reqData)]; } /** * Name:请求前本地验签 * User: zcw * Date: 2023/7/14 * Time: 9:57 * @param $key * @param $authTag * @param $iv * @param $data * @param $aad */ private function checkParam($key, $authTag, $iv, $data, $aad) { $iv = base64_decode($iv); $data = base64_decode($data); $authTag = base64_decode($authTag); return openssl_decrypt($data, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad); } /** * Name:获取签名 * User: zcw * Date: 2023/7/14 * Time: 10:03 * @param array $newRe */ private function getSignature(array $newRe) { $time = $newRe['ts']; $key = $this->rsa['rsa-private-key']; $url = $this->url; $appId = $this->appId; $reqData = $newRe['reqData']; $payload = "$url\n$appId\n$time\n$reqData"; $rsa = new RSA(); $rsa->loadKey($key); $rsa->setHash("sha256"); $rsa->setMGFHash("sha256"); $signature = $rsa->sign($payload); return base64_encode($signature); } /** * Name:请求前本地验签 * User: zcw * Date: 2023/7/14 * Time: 10:11 * @param array $newRe * @param string $signature */ private function checkLocalSignature(array $newRe, string $signature) { $signature = base64_decode($signature); $rsaPubKey = $this->rsa['rsa-public-key']; $appId = $this->appId; $url = $this->url; $time = $newRe['ts']; $reqData = $newRe['reqData']; $payload = "$url\n$appId\n$time\n$reqData"; $payload = utf8_encode($payload); $rsa = new RSA(); $rsa->loadKey($rsaPubKey); $rsa->setHash("sha256"); $rsa->setMGFHash("sha256"); return $rsa->verify($payload, $signature); } /** * Name:解析头部信息 * User: zcw * Date: 2023/7/14 * Time: 10:28 * @param $headerString * @return array */ private function httpParseHeaders($headerString) { $headers = []; $lines = explode("\r\n", $headerString); foreach ($lines as $line) { $line = trim($line); if (!empty($line)) { $parts = explode(':', $line, 2); $key = trim($parts[0]); $value = isset($parts[1]) ? trim($parts[1]) : ''; $headers[$key] = $value; } } return $headers; } /** * Name:解密参数 * User: zcw * Date: 2023/7/14 * Time: 10:31 * @param $ts * @param $body * @return mixed|null * @throws ErrorException */ private function jM($ts, $body) { $url = $this->url; $appId = $this->appId; $sn = $this->aes['sn']; $aad = $url . '|' . $appId . '|' . $ts . '|' . $sn; $key = $this->aes['key']; $key = base64_decode($key); $iv = base64_decode($body['iv']); $data = base64_decode($body['data']); $authTag = base64_decode($body['authtag']); $result = openssl_decrypt($data, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad); if (!$result) { throw new ErrorException(); } $result = json_decode($result, true); return $result; } /** * Name:验证响应值 * User: zcw * Date: 2023/7/14 * Time: 11:16 * @param $data */ private function vertifyResponse($data) { $headers = $this->httpParseHeaders($data['header']); $nowTime = time(); $reTime = $headers['Wechatmp-TimeStamp']; $appId = $this->appId; $cert = $this->cert; $sn = $cert['sn']; $key = $cert['cert-key']; $url = $this->url; if ($appId != $headers['Wechatmp-Appid'] || $nowTime - $reTime > 300){ throw new \ErrorException('返回值安全字段校验失败'); } if ($sn == $headers['Wechatmp-Serial']) { $signature = $headers['Wechatmp-Signature']; } elseif ($sn == $headers['Wechatmp-Serial-Deprecated']) { $signature = $headers['Wechatmp-Signature-Deprecated']; } else { throw new \ErrorException('返回值sn不匹配'); } $reData = $data['body']; $payload = "$url\n$appId\n$reTime\n$reData"; $payload = utf8_encode($payload); $signature = base64_decode($signature); $rsa = new RSA(); $rsa->loadKey($key); $rsa->setHash("sha256"); $rsa->setMGFHash("sha256"); return $rsa->verify($payload, $signature); } }