| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- <?php
- /**
- * 封装微信api签名安全类
- */
- namespace common\components\wxapi;
- use phpseclib\Crypt\RSA;
- use yii\base\ErrorException;
- class WxApi
- {
- private $appId;
- private $aes;
- private $rsa;
- private $cert;
- private $url;
- private $accessToken;
- public function __construct($appId, $accessToken)
- {
- $this->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);
- }
- }
|