微信支付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 }
————勇敢的少年啊 快去创造奇迹————