微信支付平台代金券和商家券接口api,使用原生php處理
當然,能使用composer加載的,直接使用composer加載微信官方包就完事兒了,別舍近求遠得不償失。
微信官方鏈接:
指導文檔&SDK : https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
相關V3-api : https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter9_1_10.shtml
1、 背景:最近公司使用比較老的php框架需要實現微信支付apiV3版本的代金券和商家券功能。
本來微信官方有比較成熟的GuzzleHttp包實現composer加載開箱即用,可惜公司php框架太老了,不支持namespace和composer,沒辦法只能手擼
2、接口這塊分2大塊,一是普通接口api的簽名、驗簽,加密和解密;二是圖片上傳接口的簽名、驗簽、加密、解密;
3、參考過的鏈接,非常感謝:
1)圖片上傳:https://blog.csdn.net/qq_16469571/article/details/105436553
2)普通接口簽名和驗簽:簽名+加密 https://www.cnblogs.com/inkwhite/p/11686115.html
驗簽+解密 https://blog.csdn.net/qq_27987023/article/details/88987835,
(tips:可以使用微信提供的postman的JSON文件示例包,測試相關證書、序列號是不是對的,鏈接https://github.com/wechatpay-apiv3/wechatpay-postman-script)
4、不廢話,上代碼:
設置回調驗簽+解密
// 設置核銷券通知地址 public function setConsumeNotifyUrl( ) { $url='https://api.mch.weixin.qq.com/v3/marketing/favor/callbacks'; $notifyurl = C('wx_node_voucher.consume_notify'); //echo $notifyurl,$url; $wxnodevoucher = D('WxNodeVoucher', 'Service'); $data = [ 'mchid'=>$wxnodevoucher->mchId, 'notify_url'=>$notifyurl, ]; /*//生成V3請求 header認證信息 $header = $wxnodevoucher->createAuthorization($url, 'POST', $data); //var_dump($header); $result = $wxnodevoucher->curlHttp( $url, 'POST', $data , $header );*/ $result = $wxnodevoucher->send($url, 'POST', $data); exit(json_encode($result, JSON_UNESCAPED_UNICODE)); } // 回調通知,需要驗簽 public function notifyVoucher( ) { $log = function($info){ log_write("回調信息:{$info}", 'wx_voucher_notify'); }; $headers = $_SERVER; $log(json_encode($headers, JSON_UNESCAPED_UNICODE)); $sign = $headers['Wechatpay-Signature']; $nonce = $headers['Wechatpay-Nonce']; $timestamp = $headers['Wechatpay-Timestamp']; $body = file_get_contents('php://input'); $message = $timestamp."\n". $nonce."\n". $body."\n"; $wxnodevoucher = D('WxNodeVoucher', 'Service'); if( true === $wxnodevoucher->checkAuthorization($sign, $message) ){ $log('回調通知驗簽通過'); $data = json_decode($body, true); /** * ApiV3 key 執行AEAD_AES_256_GCM解密 -- https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter9_1_15.shtml */ $remsg = $wxnodevoucher->decryptToString($wxnodevoucher->apiv3key ,$data['resource']['associated_data'], $data['resource']['nonce'], $data['resource']['ciphertext']); if(false === $remsg) { $log('解密失敗,body=' . $body . ', apiv3key=' . $wxnodevoucher->apiv3key); // TODO 發送郵件 return false; } $sourceArr = json_decode($remsg, 1); if (json_last_error() || !is_array($sourceArr)) { // TODO 解析json串異常 $log('解析json串異常,,remsg=' . $remsg); } switch ($data['event_type']) { case 'COUPON.USE': // TODO 卡券核銷 $this->consumeVoucher($data, $sourceArr); break; case '': default: // TODO 證書下載 break; } exit(json_encode(['code'=>'SUCCESS', 'message'=>'成功'])); }else{ $log('回調通知驗簽失敗'); } }
發送普通api簽名+加密
public function initAutoload() { if (!$this->initAutoload) { // 商戶配置 $this->appid = C('wx_node_voucher.miniproject_appid'); $this->mchId = C('wx_node_voucher.mch_id'); $this->mch_serial_no = C('wx_node_voucher.mch_serial_no'); $this->mch_private_key = C('wx_node_voucher.apiclient_key'); $this->mch_cert_pem = C('wx_node_voucher.apiclient_cert'); $this->apiv3key = C('wx_node_voucher.apiv3_key'); $this->platform_cert_pem = C('wx_node_voucher.weixin_cert'); } } //生成v3 Authorization public function createAuthorization( $url , $method = 'GET', array $data=[] ){ if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { throw new \RuntimeException("當前PHP環境不支持SHA256withRSA"); } $url_parts = parse_url($url); $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")); //私鑰地址 -- path/to/privateKey $mch_private_key = $this->mch_private_key; // return $mch_private_key; //商戶號 $merchant_id = $this->mchId; //當前時間戳 $timestamp = time(); //隨機字符串 $nonce = $this->createNoncestr(); //POST請求時 需要 轉JSON字符串 $this->body = !empty($data) ? json_encode($data) : '' ; $method = strtoupper($method); $message = $method."\n". $canonical_url."\n". $timestamp."\n". $nonce."\n". $this->body."\n"; //生成簽名 openssl_sign($message, $raw_sign, openssl_get_privatekey(file_get_contents($mch_private_key)), 'sha256WithRSAEncryption'); $sign = base64_encode($raw_sign); // return $sign; //Authorization 類型 $schema = 'WECHATPAY2-SHA256-RSA2048'; //生成token $token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"', $merchant_id,$this->mch_serial_no, $nonce, $timestamp, $sign); //'User-Agent:*/*', $header = [ 'Content-Type:application/json', 'Accept:application/json', 'User-Agent : https://zh.wikipedia.org/wiki/User_agent', 'Authorization: '. $schema . ' ' . $token ]; return $header; } /** * @Notes: 驗簽v3 Authorization * * @param: $sign 返回的簽名串 * @param: $data_str 構造的驗簽串 * @param: $pub_key_file_path -- 微信支付平台證書公鑰路徑 * @return: bool * @author: Xuzhz 2021/5/28 11:31 */ public function checkAuthorization( $sign, $data_str, $pub_key_file_path='' ){ if (!$pub_key_file_path ) { $pub_key_file_path = $this->platform_cert_pem; } $public_key = openssl_get_publickey( file_get_contents($pub_key_file_path) ); if(empty($public_key)){ return false; } $sign = base64_decode($sign); $ok = openssl_verify( $data_str, $sign, $public_key, OPENSSL_ALGO_SHA256 ); //SHA256 openssl_free_key( $public_key ); if ($ok == 1) { $result = true; } elseif ($ok == 0) { $result = false; } else { log_write('111DEBUG'. __CLASS__.' ' . __FUNCTION__ . ' 0 openssl_error_str '.json_encode(openssl_error_string())); } return $result; } /** * 作用:產生隨機字符串,不長於32位 */ public function createNoncestr( $length = 32 ) { $chars = "abcdefghijklmnopqrstuvwxyz0123456789"; $str =""; for ( $i = 0; $i < $length; $i++ ) { $str.= substr($chars, mt_rand(0, strlen($chars)-1), 1); } return $str; } /** * Decrypt AEAD_AES_256_GCM ciphertext V3 -- 證書下載、回調報文解密 * link: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml * @param stingr $aesKey ApiV3_Key //商戶需先在【商戶平台】->【API安全】的頁面設置該密鑰 * @param string $associatedData AES GCM additional authentication data * @param string $nonceStr AES GCM nonce * @param string $ciphertext AES GCM cipher text * * @return string|bool Decrypted string on success or FALSE on failure */ public function decryptToString($aesKey ,$associatedData, $nonceStr, $ciphertext) { if (strlen($aesKey) != 32 ) { throw new InvalidArgumentException('無效的ApiV3Key,長度應為32個字節'); } $ciphertext = \base64_decode($ciphertext , true); if (strlen($ciphertext) <= 16) { return false; } // ext-sodium (default installed on >= PHP 7.2) if(function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available() ){ return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); } // ext-libsodium (need install libsodium-php 1.x via pecl) if(function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()){ return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); } // PHP >= 7.1 if(PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods()) ){ $ctext = substr($ciphertext, 0, -16); $authTag = substr($ciphertext, -16); return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr,$authTag, $associatedData); } throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安裝libsodium-php'); } public function curlHttp( $url, $method='GET', $data=[], $headers=['Content-Type: application/json;charset=UTF-8'] ) { $log = function ($info) { log_write($info, 'to_erp'); }; $json = json_encode($data); $log('request-url:' . $url); $log('request-data:' . $json); //棄用S::curl,會出現異常; $ch = curl_init(); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); if( strtoupper($method) == 'POST' ) { curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $json); } curl_setopt($ch, CURLOPT_HEADER, 1); //返回response頭部信息 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers/*[ 'Content-Type: application/json;charset=UTF-8' ]*/); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible;)' ); curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); $beginMicroTime = microtime(); $result = curl_exec($ch); // print_r($result); //請求日志記錄 $endMicroTime = microtime(); /*ApiRecord::dispatch( $url, curl_getinfo($ch, CURLINFO_HTTP_CODE), curl_error($ch), $json, $result, $beginMicroTime, $endMicroTime )->onQueue(ApiRecord::QUEUE_NAME);*/ $log('response-data:' . $result); if( substr(curl_getinfo( $ch, CURLINFO_HTTP_CODE ), 0, 1) != '2' ){ $errno = curl_errno( $ch ); $log('response-code:' . $errno); curl_close( $ch ); echo curl_getinfo( $ch, CURLINFO_HTTP_CODE ).PHP_EOL; return false; } $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $re_header = substr($result, 0, $headerSize); $log("返回的header頭信息msg={$re_header}, url={$url}, data={$json}"); $body = substr($result, $headerSize); //驗簽 if(! $this->comboCheckAuth($re_header, $body)) { $log('驗證失敗'); return false; } $log('驗簽成功'); // echo 'comboCheckAuth驗簽成功'; curl_close( $ch ); if (!$result) { return false; } // $body = json_decode($result, 1); $body = $body ?? $result; if (is_string($body)) { $body = json_decode($body, 1); if (json_last_error() || !is_array($body)) { return false; } else { return $body; } } else if (is_array($body)) { return $body; } else { return false; } } // 回調通知,需要驗簽 public function comboCheckAuth( $headers, $body ) { $sign = $nonce = $timestamp = ''; // 處理請求頭 $headArr = explode("\r\n", $headers); foreach ($headArr as $loop) { if (strpos($loop, "Wechatpay-Signature") !== false) { $sign = trim(substr($loop, strlen('Wechatpay-Signature')+2)); } if (strpos($loop, "Wechatpay-Nonce") !== false) { $nonce = trim(substr($loop, strlen('Wechatpay-Nonce')+2)); } if (strpos($loop, "Wechatpay-Timestamp") !== false) { $timestamp = trim(substr($loop, strlen('Wechatpay-Timestamp')+2)); } } $message = $timestamp."\n". $nonce."\n". $body."\n"; return $a = $this->checkAuthorization($sign, $message); } /** * 獲取微信支付平台證書(微信支付負責申請), 與商戶API證書(商戶自行申請)不是一個內容 * link: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_1.shtml * * @return 返回需要$this->decryptToString()解密 * @return httpCode:200 * @return { "data": [ { "serial_no": "5157F09EFDC096DE15EBE81A47057A7232F1B8E1", "effective_time ": "2018-06-08T10:34:56+08:00", "expire_time ": "2018-12-08T10:34:56+08:00", "encrypt_certificate": { "algorithm": "AEAD_AES_256_GCM", "nonce": "61f9c719728a", "associated_data": "certificate", "ciphertext": "sRvt… " } }, {...} ] } */ public function getcertificates() { $url="https://api.mch.weixin.qq.com/v3/certificates"; //生成V3請求 header認證信息 $header = $this->createAuthorization( $url ); $data = $this->curlHttp( $url, 'GET', $this->data , $header ); //證書報文解密 //$this->decryptToString(); return $data; } /** * V3 -- 敏感信息(身份證、銀行卡、手機號.etc)加密,微信支付平台證書中的(RSA)公鑰加密 * link: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_3.shtml */ public function getEncrypt($str){ //$str是待加密字符串 $public_key_path = '證書地址'; //看情況使用證書, 個別接口證書 使用的是 平台證書而不是 api證書 $public_key = file_get_contents($public_key_path); $encrypted = ''; if (openssl_public_encrypt($str,$encrypted,$public_key,OPENSSL_PKCS1_OAEP_PADDING)) { //base64編碼 $sign = base64_encode($encrypted); } else { throw new Exception('encrypt failed'); } return $sign; } /** * V3 -- 敏感信息(身份證、銀行卡、手機號.etc)解密,微信商戶私鑰(RSA)解密 * link: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_3.shtml */ private function getDecrypt($str){ //$str是待加密字符串 $private_key_path = '平台證書路徑'; $private_key = file_get_contents($private_key_path); $dncrypted = ''; if (openssl_private_decrypt(base64_decode($str),$dncrypted,$private_key,OPENSSL_PKCS1_OAEP_PADDING)) { # TODO } else { throw new Exception('dncrypt failed'); } return $dncrypted; } //返回array | false protected function send($url, $method='GET', array $data=[]) { // 生成V3請求 header認證信息 $header = $this->createAuthorization($url, strtoupper($method), $data); // 發起curlHttp調用 & 驗簽 $result = $this->curlHttp($url, strtoupper($method), $data , $header); return $result; } //微信api-v3上傳圖片 public function wxPayUploadFile( ) { header("Content-type:text/html;charset=utf-8"); $url = 'https://api.mch.weixin.qq.com/v3/marketing/favor/media/image-upload'; $filePath = APP_PATH . 'Public/Image/Wxzf/star-group.png';//'你需要上傳的圖片'; $mime = mime_content_type($filePath); $filename = pathinfo($filePath, PATHINFO_BASENAME); //圖片校驗 if(!$this->checkImgFile($filePath)){ log_write('微信商戶上傳圖片失敗:'.$this->error); return false; } //私鑰地址 -- path/to/privateKey $keyPath = $this->mch_private_key; //商戶號 $merchant_id = $this->mchId; $mess = $this->binaryEncodeImage($filePath); $filestr = json_encode(array('filename' => $filename, 'sha256' => hash_file("sha256", $filePath))); #准備參與簽名數據 $time = time(); $nonce_str = $this->createNoncestr(); $pkid = file_get_contents($keyPath); $token = $this->sign($url, "POST", $time, $nonce_str, $filestr, $pkid, $merchant_id, $this->mch_serial_no); //Authorization 類型 $schema = 'WECHATPAY2-SHA256-RSA2048'; #設置頭部信息 $boundary = '7derenufded'; $headers = [ "Authorization: " . $schema . ' ' . $token, "User-Agent:https://zh.wikipedia.org/wiki/User_agent", "Accept:application/json", "Content-Type:multipart/form-data;boundary=" . $boundary//切記boundary=后面這里切記這里不要加-- 和 “” ]; #這里是構造請求body $boundarystr = "--{$boundary}\r\n"; $out = $boundarystr; $out .= 'Content-Disposition:form-data;name="meta"' . "\r\n";#name必須是meta $out .= 'Content-Type: application/json; charset=UTF-8' . "\r\n"; $out .= "\r\n"; $out .= "" . $filestr . "\r\n"; $out .= $boundarystr; $out .= 'Content-Disposition:form-data;name="file";filename="' . $filename . '"' . "\r\n";#name必須是file $out .= "Content-Type: {$mime}\r\n"; $out .= "\r\n"; $out .= $mess . "\r\n"; $out .= "--{$boundary}--"; // 發起curlMediaHttp調用 & 驗簽 return $this->curlMediaHttp($url, 'POST', $out, $headers); } public function curlMediaHttp( $url, $method='GET', $out='', $headers=['Content-Type: application/json;charset=UTF-8'] ) { $log = function ($info) { log_write($info, 'to_erp'); }; $json = $out; $log('request-url:' . $url); $log('request-data:' . $json); //棄用S::curl,會出現異常; $ch = curl_init(); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); // if( strtoupper($method) == 'POST' ) { curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $json); // } curl_setopt($ch, CURLOPT_HEADER, 1); //返回response頭部信息 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers/*[ 'Content-Type: application/json;charset=UTF-8' ]*/); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible;)' ); curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); $result = curl_exec($ch); // var_dump($result); $log('response-data:' . $result); if( substr(curl_getinfo( $ch, CURLINFO_HTTP_CODE ), 0, 1) != '2' ){ $errno = curl_errno( $ch ); $log('response-code:' . $errno); curl_close( $ch ); //echo curl_getinfo( $ch, CURLINFO_HTTP_CODE ).PHP_EOL; return false; } $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $re_header = substr($result, 0, $headerSize); $log("返回的header頭信息msg={$re_header}, url={$url}, data={$json}"); $body = substr($result, $headerSize); //驗簽 if(! $this->comboCheckAuth($re_header, $body)) { $log('驗證失敗'); return false; } $log('驗簽成功'); // echo 'comboCheckAuth驗簽成功'; curl_close( $ch ); if (!$result) { return false; } // $body = json_decode($result, 1); $body = $body ?? $result; if (is_string($body)) { $body = json_decode($body, 1); if (json_last_error() || !is_array($body)) { return false; } else { return $body; } } else if (is_array($body)) { return $body; } else { return false; } } //簽名 private function sign($url,$http_method,$timestamp,$nonce,$body,$mch_private_key,$merchant_id,$serial_no){ $url_parts = parse_url($url); $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")); $message = $http_method."\n". $canonical_url."\n". $timestamp."\n". $nonce."\n". $body. "\n"; openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption'); $sign = base64_encode($raw_sign); $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $merchant_id, $nonce, $timestamp, $serial_no, $sign); return $token; } /** * 圖片轉化為二進制數據流 * @desc 圖片轉化為二進制數據流 * return string */ public function binaryEncodeImage($img_file){ header("Content-type:text/html;charset=utf-8"); $p_size = filesize($img_file); $img_binary = fread(fopen($img_file, "rb"), $p_size); return $img_binary; } //判斷圖片類型 public function checkImgFile($file) { if(!is_file($file)){ $this->error='無效文件'; return false; } $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION)); if (function_exists('exif_imagetype')) { $imgType=exif_imagetype($file); }else{ $res = getimagesize($file); $imgType=$res?$res[2]:false; } if (!in_array($extension, ['jpg', 'jpeg', 'bmp', 'png']) || !in_array($imgType, [ 2, 3, 6])) { $this->error='無效圖片文件'; return false; } if(filesize($file) > 2097152){ $this->error='圖片文件大小超過 2M'; return false; } return true; }
5、over!