【PHP】微信支付apiV3平台代金券和商家券接口api,使用原生php处理
微信支付平台代金券和商家券接口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!