<?php
namespace app\common\libs\wechat;
use app\common\libs\wechat\exception\WechatPayV3Exception; use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Middleware; use GuzzleHttp\HandlerStack; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\MessageInterface; use think\Cache;
const MAXIMUM_CLOCK_OFFSET = 300;
const WechatpayNonce = 'Wechatpay-Nonce'; const WechatpaySerial = 'Wechatpay-Serial'; const WechatpaySignature = 'Wechatpay-Signature'; const WechatpayTimestamp = 'Wechatpay-Timestamp'; const ALGO_AES_256_GCM = 'aes-256-gcm'; const AUTH_TAG_LENGTH_BYTE = 16;
class WeChatPayV3 {
protected static $defaults = [ 'base_uri' => 'https://api.mch.weixin.qq.com/', 'headers' => [ 'Accept' => 'application/json, text/plain, application/x-gzip', 'Content-Type' => 'application/json; charset=utf-8', ], ];
protected $platform_serial_no;
public function __construct($config) { $this->config = [ 'app_id' => $config['app_id'], 'ssl_key_path' => $config['ssl_key_path'], 'mch_id' => $config['mch_id'], 'serial_no' => $config['serial_no'], 'pay_sign_key' => $config['pay_sign_key'],
]; }
public function transfer(array $param) { $uri = 'v3/transfer/batches';
if (!isset($param['out_detail_no']) || !isset($param['transfer_amount']) || !isset($param['openid'])) { throw new WechatPayV3Exception('Missing required parameters'); }
$detail = [ 'out_detail_no' => $param['out_detail_no'], 'transfer_amount' => $param['transfer_amount'], 'transfer_remark' => $param['transfer_remark'] ?? '转账', 'openid' => $param['openid'], ];
if (!empty($param['user_name'])) { $detail['user_name'] = $this->encryptedData($param['user_name']); }
$params = [ 'appid' => $this->config['app_id'], 'out_batch_no' => $param['out_detail_no'], 'batch_name' => '转账', 'batch_remark' => $param['transfer_remark'] ?? '转账', 'total_amount' => $param['transfer_amount'], 'total_num' => 1, 'transfer_detail_list' => [$detail], ];
try { return $this->request('POST', $uri, ['json' => $params]); } catch (RequestException $e) { $err = $this->toArray($e->getResponse()); throw new WechatPayV3Exception(($err['message'] ?? $e->getMessage()) . ($err['code']), $e->getCode()); }
}
public function request(string $method, string $uri, array $options = [], array $config = []) { $method = strtolower($method); $response = $this->jsonBased($config)->{$method}($uri, $options); return $this->toArray($response); }
protected function jsonBased(array $config = []): Client { $privateKey = file_get_contents($this->config['ssl_key_path']); $mchid = $this->config['mch_id']; $serial = $this->config['serial_no'];
$handler = isset($config['handler']) && ($config['handler'] instanceof HandlerStack) ? (clone $config['handler']) : HandlerStack::create(); $handler->unshift(Middleware::mapRequest($this->signer((string)$mchid, $serial, $privateKey)), 'signer');
$config['handler'] = $handler;
return (new Client($this->withDefaults($config))); }
protected function toArray(ResponseInterface $response) { $contentType = $response->getHeaderLine('Content-Type'); $contents = $response->getBody()->getContents();
$this->headers = $response->getHeaders();
if (false !== stripos($contentType, 'json') || stripos($contentType, 'javascript')) { return json_decode($contents, true); } elseif (false !== stripos($contentType, 'xml')) { return json_decode(json_encode(simplexml_load_string($contents)), true); }
return $contents; }
protected function signer(string $mchid, string $serial, $privateKey): callable { return function (RequestInterface $request) use ($privateKey, $mchid, $serial) { if ($this->platform_serial_no) { $request = $request->withHeader(WechatpaySerial, $this->platform_serial_no); }
$timestamp = time(); $nonce = substr(md5(uniqid((string)rand(), true)), mt_rand(1, 16), 16);
$body = ''; $bodyStream = $request->getBody(); if ($bodyStream->isSeekable()) { $body = (string)$bodyStream; $bodyStream->rewind(); } $sign_data = implode("\n", array_merge([$request->getMethod(), $request->getRequestTarget(), $timestamp, $nonce, $body], [''])); openssl_sign($sign_data, $signature, $privateKey, 'sha256WithRSAEncryption');
return $request->withHeader('Authorization', sprintf( 'WECHATPAY2-SHA256-RSA2048 mchid="%s",serial_no="%s",timestamp="%s",nonce_str="%s",signature="%s"', $mchid, $serial, $timestamp, $nonce, base64_encode($signature) )); }; }
protected function verifier(array &$certs): callable { return function (ResponseInterface $response) use (&$certs): ResponseInterface { if (!($response->hasHeader(WechatpayNonce) && $response->hasHeader(WechatpaySerial) && $response->hasHeader(WechatpaySignature) && $response->hasHeader(WechatpayTimestamp))) { throw new WechatPayV3Exception('Invalid response headers'); }
list($nonce) = $response->getHeader(WechatpayNonce); list($serial) = $response->getHeader(WechatpaySerial); list($signature) = $response->getHeader(WechatpaySignature); list($timestamp) = $response->getHeader(WechatpayTimestamp);
$localTimestamp = time();
if (abs($localTimestamp - intval($timestamp)) > MAXIMUM_CLOCK_OFFSET) { throw new WechatPayV3Exception('超出服务器时间误差范围'); }
if (!array_key_exists($serial, $certs)) { throw new WechatPayV3Exception('微信返回的序列号不存在'); }
if (($result = openssl_verify($this->joinedByLineFeed($timestamp, $nonce, $this->body($response)), base64_decode($signature), $certs[$serial], 'sha256WithRSAEncryption')) === false) { throw new WechatPayV3Exception('验证签名失败'); }
return $response; }; }
protected function joinedByLineFeed(string $timestamp, string $nonce, string $body = ''): string { return implode("\n", [$timestamp, $nonce, $body]); }
protected function body(MessageInterface $message): string { $body = ''; $bodyStream = $message->getBody(); if ($bodyStream->isSeekable()) { $body = (string)$bodyStream; $bodyStream->rewind(); }
return $body; }
protected function withDefaults(array $config = []): array { return array_replace_recursive(static::$defaults, ['headers' => $this->userAgent()], $config); }
protected function userAgent(): array { $value = [''];
array_push($value, 'GuzzleHttp/' . ClientInterface::VERSION);
extension_loaded('curl') && function_exists('curl_version') && array_push($value, 'curl/' . ((array)curl_version())['version']);
array_push($value, sprintf('(%s/%s) PHP/%s', PHP_OS, php_uname('r'), PHP_VERSION));
return ['User-Agent' => implode(' ', $value)]; }
protected function encryptedData(string $param): string { $public_key = openssl_pkey_get_public($this->getPlatformCertificate());
if (openssl_public_encrypt($param, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)) { $encrypted = base64_encode($encrypted); }
openssl_free_key($public_key);
return $encrypted ?: ''; }
public static function decryptData(string $ciphertext, string $key, string $iv = '', string $aad = ''): string { $ciphertext = base64_decode($ciphertext); $authTag = substr($ciphertext, intval(-16)); $tagLength = strlen($authTag);
if ($tagLength > 16 || ($tagLength < 12 && $tagLength !== 8 && $tagLength !== 4)) { throw new WechatPayV3Exception('获取平台公钥失败,微信返回异常,请稍后再试'); }
if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) { try { $plaintext = \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $aad, $iv, $key); } catch (\Exception $e) { throw new WechatPayV3Exception('解密失败,请稍后再试'); } }
if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) { $plaintext = \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $aad, $iv, $key); }
if (PHP_VERSION_ID >= 70100 && in_array(ALGO_AES_256_GCM, \openssl_get_cipher_methods())) { $ctext = substr($ciphertext, 0, -AUTH_TAG_LENGTH_BYTE); $authTag = substr($ciphertext, -AUTH_TAG_LENGTH_BYTE);
$plaintext = \openssl_decrypt($ctext, ALGO_AES_256_GCM, $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad); }
if (!isset($plaintext)) { throw new WechatPayV3Exception('服务器异常,请稍后再试'); } elseif (empty($plaintext)) { throw new WechatPayV3Exception('解密失败,请检查apiV3的公钥是否正确'); } else { return $plaintext; } }
public function getPlatformCertificate() { $apiv3Key = $this->config['pay_sign_key']; $cache_key = 'wechatpay_apiv3_cate_' . $apiv3Key;
if ($platformCertificate = Cache::get($cache_key)) { $this->platform_serial_no = $platformCertificate['serial_no']; return $platformCertificate['cate']; }
$client = $this->jsonBased(); $handler = $client->getConfig('handler'); $handler->remove('verifier');
$response = $this->toArray($client->get('v3/certificates'));
if (\is_array($response) && isset($response['data']) && \is_array($response['data'])) { $row = end($response['data']);
$cert = $row['encrypt_certificate']; $cert = ['serial_no' => $row['serial_no'], 'cate' => static::decryptData($cert['ciphertext'], $apiv3Key, $cert['nonce'], $cert['associated_data'])]; $this->platform_serial_no = $row['serial_no'];
Cache::set($cache_key, $cert, 60 * 60 * 12);
return $cert['cate']; } else { throw new WechatPayV3Exception('获取平台公钥失败,微信返回异常,请稍后再试'); }
} }
|