微信支付v3 php回调函数 TP5 签名/验签/下载证书

  1 <?php
  2 
  3 namespace app\api\controller;
  4 
  5 use think\Controller;
  6 use think\Db;
  7 use think\Request;
  8 use think\Url;
  9 use think\Cache;
 10 use think\Log;
 11 
 12 class WeChatPayNotifyV3 extends Controller
 13 {
 14     private $appid = 'wx33682xxxxxxxxxx';
 15     private $appsecret = '3d502b31fxxxxxxxxxxx';
 16     private $merchantid = '1611xxxxxxxxxx';
 17     private $merchantSerialNumber = '2616F66DE286CBxxxxxxxxx';
 18     private $apiV3key = 'jwCq2VaVMdiRE9Oxxxxxxxxxxx';
 19 
 20     private $pingtai_public_key_path = ROOT_PATH . 'runtime' . DS . 'wechat' . DS . 'wechatpay' . DS . 'cert.pem';//平台证书,不是商户的证书,
 21     private $apiclient_key = ROOT_PATH . 'runtime' . DS . 'wechat' . DS . 'mch' . DS . 'private' . DS . 'apiclient_key.pem';//商户api私钥
 22 
 23     const KEY_LENGTH_BYTE = 32;
 24     const AUTH_TAG_LENGTH_BYTE = 16;
 25 
 26     //回调地址
 27     public function notifyUrl()
 28     {
 29 
 30         
 31 
 32         try {
 33             //code...
 34 
 35             $header                 = $this->getHeaders(); //读取http头信息  见下文
 36             $body                   = file_get_contents('php://input'); //读取微信传过来的信息,是一个json字符串 
 37 
 38             if (empty($header) || empty($body)) {
 39                 throw new \Exception('通知参数为空', 2001);
 40             }
 41 
 42             $timestamp              = $header['WECHATPAY-TIMESTAMP'];
 43             $nonce                  = $header['WECHATPAY-NONCE'];
 44             $signature              = $header['WECHATPAY-SIGNATURE'];
 45             $serialNo               = $header['WECHATPAY-SERIAL'];
 46             if (empty($timestamp) || empty($nonce) || empty($signature) || empty($serialNo)) {
 47                 throw new \Exception('通知头参数为空', 2002);
 48             }
 49             $cert  = $this->getzhengshuDb(1);
 50 
 51             if ($cert != $serialNo) {
 52                 throw new \Exception('验签失败', 2005);
 53             }
 54             $message                = "$timestamp\n$nonce\n$body\n";
 55 
 56             //校验签名 
 57             if (!$this->verify($message, $signature, $this->pingtai_public_key_path)) { //$this->pingtai_public_key_path是获取平台证书序列号$this->getzhengshuDb()时保存下来的平台公钥文件
 58                 throw new \Exception('验签失败', 2005);
 59             }
 60 
 61             $decodeBody             = json_decode($body, true);
 62             if (empty($decodeBody) || !isset($decodeBody['resource'])) {
 63                 throw new \Exception('通知参数内容为空', 2003);
 64             }
 65             $decodeBodyResource     = $decodeBody['resource'];
 66             $decodeData_res         = $this->decryptToString($decodeBodyResource['associated_data'], $decodeBodyResource['nonce'], $decodeBodyResource['ciphertext'], ''); //解密resource
 67             $decodeData             = json_decode($decodeData_res, true);
 68             Log::write($decodeData);
 69 
 70             //返回结果格式
 71             //array (
 72             //   'mchid' => 'xxx',
 73             //   'appid' => 'xxxxxxx',
 74             //   'out_trade_no' => '1217752501201407033233368026',
 75             //   'transaction_id' => '4200001336202201037507057791',
 76             //   'trade_type' => 'NATIVE',
 77             //   'trade_state' => 'SUCCESS',
 78             //   'trade_state_desc' => '支付成功',
 79             //   'bank_type' => 'OTHERS',
 80             //   'attach' => '',
 81             //   'success_time' => '2022-01-03T19:43:05+08:00',
 82             //   'payer' => 
 83             //   array (
 84             //     'openid' => 'ovs326bgwfA4o8jlFQXMEma2JZek',
 85             //   ),
 86             //   'amount' => 
 87             //   array (
 88             //     'total' => 1,
 89             //     'payer_total' => 1,
 90             //     'currency' => 'CNY',
 91             //     'payer_currency' => 'CNY',
 92             //   ),
 93             // )
 94             //执行自己的代码start
 95 
 96             //执行自己的代码end
 97 
 98             $arr = array("code" => "SUCCESS", "message" => "");
 99             echo json_encode($arr);
100 
101         } catch (\Exception $e) {
102             Log::error($e->getMessage());
103             $arr = array("code" => "ERROR", "message" => $e->getMessage());
104             echo json_encode($arr);
105         }
106     }
107     //获取微信回调http头信息
108     public function getHeaders()
109     {
110         $headers                = array();
111         foreach ($_SERVER as $key => $value) {
112             if ('HTTP_' == substr($key, 0, 5)) {
113                 $headers[str_replace('_', '-', substr($key, 5))] = $value;
114             }
115             if (isset($_SERVER['PHP_AUTH_DIGEST'])) {
116                 $header['AUTHORIZATION'] = $_SERVER['PHP_AUTH_DIGEST'];
117             } elseif (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
118                 $header['AUTHORIZATION'] = base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']);
119             }
120             if (isset($_SERVER['CONTENT_LENGTH'])) {
121                 $header['CONTENT-LENGTH'] = $_SERVER['CONTENT_LENGTH'];
122             }
123             if (isset($_SERVER['CONTENT_TYPE'])) {
124                 $header['CONTENT-TYPE'] = $_SERVER['CONTENT_TYPE'];
125             }
126         }
127         return $headers;
128     }
129     //获取平台证书序列号
130     public function getzhengshuDb($getNew = 0)
131     {
132         if ($getNew !== 1) {
133             dump(file_get_contents($this->pingtai_public_key_path));
134         }
135         $url = "https://api.mch.weixin.qq.com/v3/certificates";
136         $timestamp = time(); //时间戳
137         $nonce = $this->nonce_str(); //获取一个随机数
138         $body = "";
139         $mch_private_key = $this->getPrivateKey(); //读取商户api证书私钥
140         $merchant_id = $this->merchantid; //服务商商户号
141         $serial_no = $this->merchantSerialNumber; //在API安全中获取
142         $sign = $this->sign($url, 'GET', $timestamp, $nonce, $body, $mch_private_key, $merchant_id, $serial_no); //签名
143 
144         $header = [
145             'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $sign,
146             'Accept:application/json',
147             'User-Agent:' . $merchant_id
148         ];
149         $result = $this->curl($url, '', $header, 'GET');
150         $result = json_decode($result, true);
151         $serial_no = $result['data'][0]['serial_no'];
152         file_put_contents(ROOT_PATH . 'runtime' . DS . 'wechat' . DS . 'wechatpay' . DS . 'serial_no.text', $serial_no);
153    
154         $encrypt_certificate = $result['data'][0]['encrypt_certificate'];
155         $sign_key = $this->apiV3key; //在API安全中设置     
156         $result = $this->decryptToString($encrypt_certificate['associated_data'], $encrypt_certificate['nonce'], $encrypt_certificate['ciphertext'], $sign_key); //解密
157 
158         file_put_contents($this->pingtai_public_key_path, $result);
159   
160         return $serial_no;
161     }
162     //生成随机字符串
163     public function nonce_str($length = 32)
164     {
165         $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
166         $str = "";
167         for ($i = 0; $i < $length; $i++) {
168             $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
169         }
170         return $str;
171     }
172     //读取商户api证书私钥
173     public function getPrivateKey()
174     {
175         return openssl_get_privatekey(file_get_contents($this->apiclient_key)); //微信商户平台中下载下来,保存到服务器直接读取
176 
177     }
178     //签名
179     public function sign($url, $http_method, $timestamp, $nonce, $body, $mch_private_key, $merchant_id, $serial_no)
180     {
181         $url_parts = parse_url($url);
182         $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
183         $message =
184             $http_method . "\n" .
185             $canonical_url . "\n" .
186             $timestamp . "\n" .
187             $nonce . "\n" .
188             $body . "\n";
189         openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
190         $sign = base64_encode($raw_sign);
191         $schema = 'WECHATPAY2-SHA256-RSA2048';
192         $token = sprintf(
193             'mchid="%s",nonce_str="%s",signature="%s",timestamp="%d",serial_no="%s"',
194             $merchant_id,
195             $nonce,
196             $sign,
197             $timestamp,
198             $serial_no
199         );
200         return $token;
201     }
202     //curl提交
203     public function curl($url, $data = [], $header, $method = 'POST')
204     {
205         $curl = curl_init();
206         curl_setopt($curl, CURLOPT_URL, $url);
207         curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
208         curl_setopt($curl, CURLOPT_HEADER, false);
209         curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
210         curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
211         if ($method == "POST") {
212             curl_setopt($curl, CURLOPT_POST, TRUE);
213             curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
214         }
215         $result = curl_exec($curl);
216         curl_close($curl);
217         return $result;
218     }
219 
220     private function decryptToString($associatedData, $nonceStr, $ciphertext, $aesKey = '')
221     {
222         if (empty($aesKey)) {
223             $aesKey = $this->apiV3key; //微信商户平台 api安全中设置获取
224         }
225         $ciphertext = \base64_decode($ciphertext);
226         if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
227             return false;
228         }
229         // ext-sodium (default installed on >= PHP 7.2)
230         if (
231             function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()
232         ) {
233             return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
234         }
235 
236         // ext-libsodium (need install libsodium-php 1.x via pecl)
237         if (
238             function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()
239         ) {
240             return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
241         }
242 
243         // openssl (PHP >= 7.1 support AEAD)
244         if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
245             $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
246             $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
247 
248             return \openssl_decrypt(
249                 $ctext,
250                 'aes-256-gcm',
251                 $aesKey,
252                 \OPENSSL_RAW_DATA,
253                 $nonceStr,
254                 $authTag,
255                 $associatedData
256             );
257         }
258 
259         throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
260     }
261     //签名验证操作
262     private function verify($message, $signature, $merchantPublicKey)
263     {
264         if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
265             throw new \RuntimeException("当前PHP环境不支持SHA256withRSA");
266         }
267         $signature = base64_decode($signature);
268         $a = openssl_verify($message, $signature, $this->getWxPublicKey($merchantPublicKey), 'sha256WithRSAEncryption');
269         return $a;
270     }
271     //获取平台公钥  获取平台证书序列号时存起来的cert.pem文件
272     protected function getWxPublicKey($key)
273     {
274         $public_content = file_get_contents($key);
275         $a = openssl_get_publickey($public_content);
276         return $a;
277     }
278 }

 

posted @ 2022-01-03 21:22  MasterC  阅读(1683)  评论(1编辑  收藏  举报