• 微信支付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 }
    ————勇敢的少年啊 快去创造奇迹————
  • 相关阅读:
    洛谷P6218 [USACO06NOV] Round Numbers S 题解 数位DP
    Duilib的双缓冲实现,附带GDI、WTL的双缓冲实现
    关于热键HotKey与WM_KEYDOWN的一点心得
    源码不匹配,找到了xxx的副本。
    SetForegroundWindow的失效问题: 跨进程的窗口前置。
    2021年4月的一份总结
    制作msi文件,exe转msi文件。
    内存限制
    ISPRS Vaihingen 数据集解析
    Linux中sudo、su和su -命令的区别小结
  • 原文地址:https://www.cnblogs.com/masterccc/p/15760570.html
Copyright © 2020-2023  润新知