⽀付v3php回调函数TP5签名验签下载证书
1 <?php
2
3 namespace app\api\controller;
4
5use think\Controller;
6use think\Db;
7use think\Request;
8use think\Url;
9use think\Cache;
10use think\Log;
11
12class WeChatPayNotifyV3 extends Controller
13 {
14private$appid = 'wx33682xxxxxxxxxx';
15private$appsecret = '3d502b31fxxxxxxxxxxx';
16private$merchantid = '1611xxxxxxxxxx';
17private$merchantSerialNumber = '2616F66DE286CBxxxxxxxxx';
18private$apiV3key = 'jwCq2VaVMdiRE9Oxxxxxxxxxxx';
19
20private$pingtai_public_key_path = ROOT_PATH . 'runtime' . DS . 'wechat' . DS . 'wechatpay' . DS . 'cert.pem';//平台证书,不是商户的证书,
21private$apiclient_key = ROOT_PATH . 'runtime' . DS . 'wechat' . DS . 'mch' . DS . 'private' . DS . 'apiclient_key.pem';//商户api私钥
22
23const KEY_LENGTH_BYTE = 32;
24const AUTH_TAG_LENGTH_BYTE = 16;
25
26//回调地址
27public function notifyUrl()
28 {
29
30
31
32try {
33//
34
35$header = $this->getHeaders(); //读取http头信息见下⽂
36$body = file_get_contents('php://input'); //读取传过来的信息,是⼀个json字符串
37
38if (empty($header) || empty($body)) {
39throw 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'];
46if (empty($timestamp) || empty($nonce) || empty($signature) || empty($serialNo)) {
47throw new \Exception('通知头参数为空', 2002);
48 }
49$cert = $this->getzhengshuDb(1);
50
51if ($cert != $serialNo) {
52throw new \Exception('验签失败', 2005);
53 }
54$message = "$timestamp\n$nonce\n$body\n";
55
56//校验签名
57if (!$this->verify($message, $signature, $this->pingtai_public_key_path)) { //$this->pingtai_public_key_path是获取平台证书序列号$this->getzhengshuDb()时保存下来的平台公钥⽂件 58throw new \Exception('验签失败', 2005);
59 }
60
61$decodeBody = json_decode($body, true);
php实例代码解密62if (empty($decodeBody) || !isset($decodeBody['resource'])) {
63throw 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);
68Log::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" => "");
99echo json_encode($arr);
100
101 } catch (\Exception$e) {
102Log::error($e->getMessage());
103$arr = array("code" => "ERROR", "message" => $e->getMessage());
104echo json_encode($arr);
105 }
106 }
107//获取回调http头信息
108public function getHeaders()
109 {
110$headers = array();
111foreach ($_SERVER as$key => $value) {
112if ('HTTP_' == substr($key, 0, 5)) {
113$headers[str_replace('_', '-', substr($key, 5))] = $value;
114 }
115if (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 }
120if (isset($_SERVER['CONTENT_LENGTH'])) {
121$header['CONTENT-LENGTH'] = $_SERVER['CONTENT_LENGTH'];
122 }
123if (isset($_SERVER['CONTENT_TYPE'])) {
124$header['CONTENT-TYPE'] = $_SERVER['CONTENT_TYPE'];
125 }
126 }
127return$headers;
128 }
129//获取平台证书序列号
130public function getzhengshuDb($getNew = 0)
131 {
132if ($getNew !== 1) {
133 dump(file_get_contents($this->pingtai_public_key_path));
134 }
135$url = "h.weixin.qq/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'];
152file_put_contents(ROOT_PATH . 'runtime' . DS . 'wechat' . DS . 'wechatpay' . DS . '', $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
158file_put_contents($this->pingtai_public_key_path, $result);
159
160return$serial_no;
161 }
162//⽣成随机字符串
163public function nonce_str($length = 32)
164 {
165$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
166$str = "";
167for ($i = 0; $i < $length; $i++) {
168$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
169 }
170return$str;
171 }
172//读取商户api证书私钥
173public function getPrivateKey()
174 {
175return openssl_get_privatekey(file_get_contents($this->apiclient_key)); //商户平台中下载下来,保存到服务器直接读取
176
177 }
178//签名
179public 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 );
200return$token;
201 }
202//curl提交
203public 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);
211if ($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);
217return$result;
218 }
219
220private function decryptToString($associatedData, $nonceStr, $ciphertext, $aesKey = '')
221 {
222if (empty($aesKey)) {
223$aesKey = $this->apiV3key; //商户平台 api安全中设置获取
224 }
225$ciphertext = \base64_decode($ciphertext);
226if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
227return false;
228 }
229// ext-sodium (default installed on >= PHP 7.2)
230if (
231function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available() 232 ) {
233return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
234 }
235
236// ext-libsodium (need install libsodium-php 1.x via pecl)
237if (
238function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes25
6gcm_is_available() 239 ) {
240return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
241 }
242
243// openssl (PHP >= 7.1 support AEAD)
244if (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
248return \openssl_decrypt(
249$ctext,
250 'aes-256-gcm',
251$aesKey,
252 \OPENSSL_RAW_DATA,
253$nonceStr,
254$authTag,
255$associatedData
256 );
257 }
258
259throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
260 }
261//签名验证操作
262private function verify($message, $signature, $merchantPublicKey)
263 {
264if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
265throw new \RuntimeException("当前PHP环境不⽀持SHA256withRSA");
266 }
267$signature = base64_decode($signature);
268$a = openssl_verify($message, $signature, $this->getWxPublicKey($merchantPublicKey), 'sha256WithRSAEncryption'); 269return$a;
270 }
271//获取平台公钥获取平台证书序列号时存起来的cert.pem⽂件
272protected function getWxPublicKey($key)
273 {
274$public_content = file_get_contents($key);
275$a = openssl_get_publickey($public_content);
276return$a;
277 }
278 }
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论