class Formatter
{
/**
* Generate a random BASE62 string aka `nonce`, similar as `random_bytes`.
*
* @param int $size - Nonce string length, default is 32.
*
* @return string - base62 random string.
*/
public static function nonce(int $size = 32): string
{
if ($size < 1) {
throw new Exception('Size must be a positive integer.');
}
return implode('', array_map(static function(string $c): string {
return '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'[ord($c) % 62];
}, str_split(random_bytes($size))));
}
/**
* Retrieve the current `Unix` timestamp.
*
* @return int - Epoch timestamp.
*/
public static function timestamp(): int
{
return time();
}
/**
* Formatting for the heading `Authorization` value.
*
* @param string $mchid - The merchant ID.
* @param string $nonce - The Nonce string.
* @param string $signature - The base64-encoded `Rsa::sign` ciphertext.
* @param string $timestamp - The `Unix` timestamp.
* @param string $serial - The serial number of the merchant public certification.
*
* @return string - The APIv3 Authorization `header` value
*/
public static function authorization(string $mchid, string $nonce, string $signature, string $timestamp, string $serial): string
{
return sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",serial_no="%s",timestamp="%s",nonce_str="%s",signature="%s"',
$mchid, $serial, $timestamp, $nonce, $signature
);
}
/**
* Formatting this `HTTP::request` for `Rsa::sign` input.
*
* @param string $method - The HTTP verb, must be the uppercase sting.
* @param string $uri - Combined string with `URL::pathname` and `URL::search`.
* @param string $timestamp - The `Unix` timestamp, should be the one used in `authorization`.
* @param string $nonce - The `Nonce` string, should be the one used in `authorization`.
* @param string $body - The playload string, HTTP `GET` should be an empty string.
*
* @return string - The content for `Rsa::sign`
*/
public static function request(string $method, string $uri, string $timestamp, string $nonce, string $body = ''): string
{
return static::joinedByLineFeed($method, $uri, $timestamp, $nonce, $body);
}
/**
* Formatting this `HTTP::response` for `Rsa::verify` input.
*
* @param string $timestamp - The `Unix` timestamp, should be the one from `response::headers[Wechatpay-Timestamp]`.
* @param string $nonce - The `Nonce` string, should be the one from `response::headers[Wechatpay-Nonce]`.
* @param string $body - The response payload string, HTTP status(`201`, `204`) should be an empty string.
*
* @return string - The content for `Rsa::verify`
*/
public static function response(string $timestamp, string $nonce, string $body = ''): string
{
return static::joinedByLineFeed($timestamp, $nonce, $body);
}
/**
* Joined this inputs by for `Line Feed`(LF) char.
*
* @param string|float|int|bool $pieces - The scalar variable(s).
*
* @return string - The joined string.
*/
public static function joinedByLineFeed(...$pieces): string
{
return implode("\n", array_merge($pieces, ['']));
}
/**
* Sort an array by key with `SORT_STRING` flag.
*
* @param array $thing - The input array.
*
* @return array - The sorted array.
*/
public static function ksort(array $thing = []): array
{
ksort($thing, SORT_STRING);
return $thing;
}
/**
* Like `queryString` does but without the `sign` and `empty value` entities.
*
* @param array $thing - The input array.
*
* @return string - The `key=value` pair string whose joined by `&` char.
*/
public static function queryStringLike(array $thing = []): string
{
$data = [];
foreach ($thing as $key => $value) {
if ($key === 'sign' || is_null($value) || $value === '') {
continue;
}
$data[] = implode('=', [$key, $value]);
}
return implode('&', $data);
}
public static function curlPostWithWx($params,$authorization,$url)
{
$paramsString = json_encode($params);
// 初始化curl
$ch = curl_init();
// 设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
// post数据
curl_setopt($ch, CURLOPT_POST, 1);
// post的变量
curl_setopt($ch, CURLOPT_POSTFIELDS, $paramsString);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json; charset=utf-8',
'Content-Length: ' . strlen($paramsString),
'Authorization: ' . "WECHATPAY2-SHA256-RSA2048 " . $authorization,
'Accept: application/json',
'User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'
)
);
// 运行curl,结果以jason形式返回
$res = curl_exec($ch);
curl_close($ch);
// 取出数据
$data = json_decode($res, true);
return $data;
}
/**
* get请求
* @param $url
* @param $authorization
* @return mixed
*/
public static function curlGetWithWx($url, $authorization,$params=[])
{
$paramsString = json_encode($params);
// 初始化curl
$ch = curl_init();
// 设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json; charset=utf-8',
'Content-Length: ' . strlen($paramsString),
'Authorization: ' . "WECHATPAY2-SHA256-RSA2048 " . $authorization,
'Accept: application/json',
'User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'
)
);
// 运行curl,结果以jason形式返回
$res = curl_exec($ch);
curl_close($ch);
// 取出数据
$data = json_decode($res, true);
//返回
return $data;
}
public static function getAuthorization($url, $body,$mch_id, $priKey, $serial, $http_method = "POST")
{
// Authorization:
$url_parts = parse_url($url);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
$timestamp = static::timestamp();
$nonce = static::nonce();
$message = static::request($http_method, $canonical_url, $timestamp, $nonce, json_encode($body));
openssl_sign($message, $raw_sign, $priKey, 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
return static::authorization($mch_id, $nonce, $sign, $timestamp, $serial);
}
}
$mch_id = '1630****';//商户号
$priKey = '';//私钥文本
$serial = '';//证书序号
$queryUrl = 'https://api.mch.weixin.qq.com/v3/bill/tradebill';//账单接口
$param = ['bill_date'=>'2022-09-01','sub_mchid'=>$mch_id,'bill_type'=>'ALL','tar_type'=>'GZIP'];
$authorization = Formatter::getAuthorization($queryUrl, $param, $mch_id, $priKey, $serial);//生成签名
$result = Formatter::curlGetWithWx($param, $authorization, $queryUrl);//接口请求
var_dump($result);
//通知
header('Content-type:text/html; Charset=utf-8');
/** 请填写以下配置信息 **/
$publicKeyPath = getcwd() . '/cert/public_key.pem'; //微信支付公钥证书文件路径,可以到 https://www.dedemao.com/wx/wx_publickey_download.php 生成
$apiKey = 'xxxxx'; //https://pay.weixin.qq.com 帐户中心-安全中心-API安全-APIv3密钥-设置密钥
/** 配置结束 **/
$wxPay = new WxpayService($apiKey, $publicKeyPath);
$result = $wxPay->validate();
if($result===false){
//验证签名失败
exit('sign error');
}
$result = $wxPay->notify();
if ($result === false) {
exit('pay error');
}
if ($result['trade_state'] == 'SUCCESS') {
//支付成功,完成你的逻辑
//例如连接数据库,获取付款金额$result['amount']['total'],获取订单号$result['out_trade_no']修改数据库中的订单状态等;
//订单总金额,单位为分:$result['amount']['total']
//用户支付金额,单位为分:$result['amount']['payer_total']
//商户订单号:$result['out_trade_no']
//微信支付订单号:$result['transaction_id']
//银行类型:$result['bank_type']
//支付完成时间:$result['success_time'] 格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
//用户标识:$result['payer']['openid']
//交易状态:$result['trade_state']
//具体详细请看微信文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transactions/chapter3_11.shtml
echo 'success';
}
class WxpayService
{
protected $apiKey;
protected $publicKeyPath;
protected $publicKey;
public function __construct($apikey, $publicKeyPath)
{
$this->apiKey = $apikey;
$this->publicKeyPath = $publicKeyPath;
}
public function getHeader($key = '')
{
$headers = getallheaders();
if ($key) {
return $headers[$key];
}
return $headers;
}
public function validate()
{
$serialNo = $this->getHeader('Wechatpay-Serial');
$sign = $this->getHeader('Wechatpay-Signature');
$timestamp = $this->getHeader('Wechatpay-Timestamp');
$nonce = $this->getHeader('Wechatpay-Nonce');
if (!isset($serialNo, $sign, $timestamp, $nonce)) {
return false;
}
// if (!$this->checkTimestamp($timestamp)) {
// return false;
// }
$body = file_get_contents('php://input');
$message = "$timestamp\n$nonce\n$body\n";
$certificate = openssl_x509_read(file_get_contents($this->publicKeyPath));
$_serialNo = $this->parseSerialNo($certificate);
if ($serialNo !== $_serialNo) return false;
$this->publicKey = openssl_get_publickey($certificate);
return $this->verify($message, $sign);
}
private function verify($message, $signature)
{
if (!$this->publicKey) {
return false;
}
if (!in_array('sha256WithRSAEncryption', openssl_get_md_methods(true))) {
exit("当前PHP环境不支持SHA256withRSA");
}
$signature = base64_decode($signature);
return (bool)openssl_verify($message, $signature, $this->publicKey, 'sha256WithRSAEncryption');
}
private function parseSerialNo($certificate)
{
$info = openssl_x509_parse($certificate);
if (!isset($info['serialNumber']) && !isset($info['serialNumberHex'])) {
exit('证书格式错误');
}
$serialNo = '';
if (isset($info['serialNumberHex'])) {
$serialNo = $info['serialNumberHex'];
} else {
if (strtolower(substr($info['serialNumber'], 0, 2)) == '0x') { // HEX format
$serialNo = substr($info['serialNumber'], 2);
} else { // DEC format
$value = $info['serialNumber'];
$hexvalues = ['0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
while ($value != '0') {
$serialNo = $hexvalues[bcmod($value, '16')] . $serialNo;
$value = bcdiv($value, '16', 0);
}
}
}
return strtoupper($serialNo);
}
protected function checkTimestamp($timestamp)
{
return abs((int)$timestamp - time()) <= 120;
}
public function notify()
{
$postStr = file_get_contents('php://input');
$postData = json_decode($postStr, true);
if ($postData['resource']) {
$data = $this->decryptToString($postData['resource']['associated_data'], $postData['resource']['nonce'], $postData['resource']['ciphertext']);
$data = json_decode($data, true);
return is_array($data) ? $data : false;
}
return false;
}
public function decryptToString($associatedData, $nonceStr, $ciphertext)
{
$ciphertext = base64_decode($ciphertext);
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, $this->apiKey);
}
// 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, $this->apiKey);
}
// openssl (PHP >= 7.1 support AEAD)
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', $this->apiKey, OPENSSL_RAW_DATA, $nonceStr,
$authTag, $associatedData);
}
exit('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
}
}
无限星辰 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权 , 转载请注明微信支付V3api demo!