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 }