You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
12 KiB
330 lines
12 KiB
<?php
|
|
|
|
namespace OpenAPI\Client;
|
|
|
|
use Exception;
|
|
use OpenAPI\Client\Model\AsyncNotifyRequest;
|
|
use OpenAPI\Client\Model\AsyncNotifyResponse;
|
|
|
|
class ApiClient
|
|
{
|
|
/**
|
|
* @var Configuration 基础配置
|
|
*/
|
|
private Configuration $config;
|
|
|
|
/**
|
|
* @var array 通知回调处理路由表
|
|
*/
|
|
private array $notifyRouter;
|
|
|
|
public function __construct(Configuration $config)
|
|
{
|
|
// 初始化配置
|
|
$this->config = $config;
|
|
// 初始化路由
|
|
}
|
|
|
|
/**
|
|
* 执行默认数据类型的通知方法
|
|
* @param string $raw 源数据
|
|
* @param NotifyHandler $handler 通知处理器
|
|
* @return string 处理结果
|
|
* @throws ApiException
|
|
*/
|
|
public function notifyRunDefault(string $raw, NotifyHandler $handler): string
|
|
{
|
|
return $this->notifyRun($raw, "application/json", "application/json", $handler);
|
|
}
|
|
|
|
/**
|
|
* 执行通知方法
|
|
* @param string $raw 源数据
|
|
* @param string $contentType 服务端发送的数据类型
|
|
* @param string $accept 服务端需要接收的数据类型
|
|
* @param NotifyHandler $handler 通知处理器
|
|
* @return string 处理结果
|
|
* @throws ApiException
|
|
*/
|
|
public function notifyRun(string $raw, string $contentType, string $accept, NotifyHandler $handler): string
|
|
{
|
|
if (empty(trim($raw))) {
|
|
throw new ApiException("内容为空");
|
|
}
|
|
// 解析请求数据
|
|
$notifyRequest = null;
|
|
if ($contentType === 'application/json') {
|
|
$content = json_decode($raw);
|
|
$notifyRequest = ObjectSerializer::deserialize($content, '\OpenAPI\Client\Model\AsyncNotifyRequest', []);
|
|
} elseif ($contentType === 'application/xml') {
|
|
// XML格式暂不支持
|
|
throw new ApiException("暂不支持的格式: " . $contentType);
|
|
} else {
|
|
throw new ApiException("不支持的数据格式: " . $contentType);
|
|
}
|
|
if ($notifyRequest instanceof AsyncNotifyRequest) {
|
|
// 检查签名类型
|
|
if ($notifyRequest->getSignType() !== 'RSA') {
|
|
throw new ApiException("不支持的签名类型: " . $notifyRequest->getSignType());
|
|
}
|
|
// 验证通知
|
|
if (!$this->verifyNotify($notifyRequest)) {
|
|
throw new ApiException("签名错误: " . json_encode($notifyRequest));
|
|
}
|
|
$resp = new AsyncNotifyResponse();
|
|
// 调用对应通知处理方法
|
|
$module = $notifyRequest->getModule();
|
|
// 检查路径是否在路由表中
|
|
if (array_key_exists($module, $this->notifyRouter)) {
|
|
// 调用对应的函数
|
|
$function = $this->notifyRouter[$module];
|
|
try {
|
|
$answer = $function($handler, $notifyRequest->getPayload());
|
|
$resp->setPayload($answer);
|
|
$resp->setResult("ok");
|
|
} catch (Exception $e) {
|
|
echo sprintf("调用通知回调处理方法失败:在 %s 的第 %d 行: [%d] %s", $e->getFile(), $e->getLine(), $e->getCode(), $e->getMessage());
|
|
$resp->setResult("fail");
|
|
}
|
|
$resp->setAnswerId($notifyRequest->getNotifyId());
|
|
$resp->setAppId($this->config->getAppid());
|
|
$resp->setTime(date($this->config->getDataFormat(), time()));
|
|
$resp->setSignType($this->config->getSignType());
|
|
$resp->setCharset($this->config->getCharset());
|
|
$resp->setSign($this->answerSign($resp));
|
|
|
|
//处理完成
|
|
if ($accept === "application/json") {
|
|
return json_encode($resp);
|
|
} else if ($accept === "application/xml") {
|
|
//返回XML格式
|
|
throw new ApiException("暂不支持的数据接收格式:" . $accept);
|
|
} else {
|
|
//未知的格式
|
|
throw new ApiException("未知的数据接收格式:" . $accept);
|
|
}
|
|
} else {
|
|
// 如果路由不存在,抛出异常或进行相应的错误处理
|
|
throw new ApiException("路由未定义: " . $module);
|
|
}
|
|
} else {
|
|
throw new ApiException("对象格式错误");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws ApiException
|
|
*/
|
|
function requestSign($params): string
|
|
{
|
|
// 获取payload和其他参数
|
|
$payload = $params['payload'];
|
|
$content = sprintf(
|
|
"%s&%s&%s&%s&%s&%s&%s",
|
|
$params['path'],
|
|
$params['app_id'],
|
|
$params['access_token'],
|
|
$params['sign_type'],
|
|
$params['charset'],
|
|
$params['time'],
|
|
$this->object2LinkStr($payload)
|
|
);
|
|
|
|
// 计算SHA-256摘要
|
|
$bytesContent = mb_convert_encoding($content, $params['charset']);
|
|
$digestData = hash('sha256', $bytesContent, true);
|
|
|
|
// 加载私钥
|
|
$privateKeyContent = $this->config->getPrivateKey();
|
|
$privateKey = openssl_pkey_get_private($privateKeyContent);
|
|
if ($privateKey === false) {
|
|
while ($errMsg = openssl_error_string()) {
|
|
error_log($errMsg);
|
|
}
|
|
throw new ApiException('Invalid private key');
|
|
}
|
|
|
|
// 使用SHA256withRSA签名
|
|
$signature = '';
|
|
$result = openssl_sign($digestData, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
|
if ($result === false) {
|
|
throw new ApiException('Failed to sign data');
|
|
}
|
|
|
|
// 检查PHP版本,只有在PHP 7.x版本中才释放私钥资源
|
|
if (PHP_VERSION_ID < 80000) {
|
|
openssl_pkey_free($privateKey);
|
|
}
|
|
|
|
// 返回十六进制编码的签名
|
|
return bin2hex($signature);
|
|
}
|
|
|
|
|
|
/**
|
|
* @throws ApiException
|
|
*/
|
|
private function answerSign(AsyncNotifyResponse $resp): string
|
|
{
|
|
// 获取payload和其他参数
|
|
$content = sprintf(
|
|
"%s&%s&%s&%s&%s&%s&%s",
|
|
$resp->getAnswerId(),
|
|
$resp->getAppId(),
|
|
$resp->getResult(),
|
|
$resp->getSignType(),
|
|
$resp->getCharset(),
|
|
$resp->getTime(),
|
|
$this->object2LinkStr($resp->getPayload())
|
|
);
|
|
// 计算SHA-256摘要
|
|
$bytesContent = mb_convert_encoding($content, $resp->getCharset());
|
|
$digestData = hash('sha256', $bytesContent, true);
|
|
// 加载私钥
|
|
$privateKeyContent = $this->config->getPrivateKey();
|
|
$privateKey = openssl_pkey_get_private($privateKeyContent);
|
|
if ($privateKey === false) {
|
|
while ($errMsg = openssl_error_string()) {
|
|
error_log($errMsg);
|
|
}
|
|
throw new ApiException('Invalid private key');
|
|
}
|
|
// 使用SHA256withRSA签名
|
|
$signature = '';
|
|
$result = openssl_sign($digestData, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
|
if ($result === false) {
|
|
throw new ApiException('Failed to sign data');
|
|
}
|
|
// 检查PHP版本,只有在PHP 7.x版本中才释放私钥资源
|
|
if (PHP_VERSION_ID < 80000) {
|
|
openssl_pkey_free($privateKey);
|
|
}
|
|
// 返回十六进制编码的签名
|
|
return bin2hex($signature);
|
|
}
|
|
|
|
function object2LinkStr($o): string
|
|
{
|
|
if ($o === null) {
|
|
return "";
|
|
}
|
|
// 处理简单值类型
|
|
if (is_numeric($o) || is_string($o)) {
|
|
return strval($o);
|
|
}
|
|
if (is_bool($o)) {
|
|
return $o ? 'true' : 'false';
|
|
}
|
|
// 处理数组
|
|
if (is_array($o)) {
|
|
// 检查数组是否是关联数组
|
|
if (array_keys($o) !== range(0, count($o) - 1)) {
|
|
// 对关联数组按键进行字典排序
|
|
ksort($o);
|
|
$result = [];
|
|
foreach ($o as $key => $value) {
|
|
// 递归处理数组内部的每个元素
|
|
$result[] = $key . "=" . $this->object2LinkStr($value);
|
|
}
|
|
return implode("&", $result);
|
|
} else {
|
|
// 非关联数组,按元素顺序处理
|
|
return implode(",", array_map([$this, 'object2LinkStr'], $o));
|
|
}
|
|
}
|
|
// 其他类型数据处理
|
|
return strval($o);
|
|
}
|
|
|
|
/**
|
|
* @throws ApiException
|
|
*/
|
|
function verifySign($data): bool
|
|
{
|
|
// 检查签名类型
|
|
$signType = $data['sign_type'] ?? null;
|
|
if ($signType !== 'RSA') {
|
|
throw new ApiException("不支持的签名类型: {$signType}");
|
|
}
|
|
|
|
$waitStr = $this->object2LinkStr($data['payload']);
|
|
$formatStr = sprintf(
|
|
"%s&%s&%s&%s&%s&%s&%s&%s&%s&%s&%s",
|
|
$data['response_id'] ?? '',
|
|
$data['err_code'] ?? '',
|
|
$data['err_msg'] ?? '',
|
|
$data['sub_err'] ?? '',
|
|
$data['sub_msg'] ?? '',
|
|
$data['time'] ?? '',
|
|
$data['open_id'] ?? '',
|
|
$data['sign_type'] ?? '',
|
|
$data['charset'] ?? 'UTF-8',
|
|
$data['description'] ?? '',
|
|
$waitStr
|
|
);
|
|
$bodyCharset = $data['charset'] ?? 'UTF-8';
|
|
$bytesContent = mb_convert_encoding($formatStr, $bodyCharset);
|
|
$digestData = hash('sha256', $bytesContent, true);
|
|
$pubKey = openssl_pkey_get_public($this->config->getApiPublicKey());
|
|
if ($pubKey === false) {
|
|
while ($errMsg = openssl_error_string()) {
|
|
error_log($errMsg);
|
|
}
|
|
return false;
|
|
}
|
|
$signature = openssl_verify($digestData, hex2bin($data['sign']), $pubKey, OPENSSL_ALGO_SHA256);
|
|
// 检查PHP版本,只有在PHP 7.x版本中才释放公钥资源
|
|
if (PHP_VERSION_ID < 80000) {
|
|
openssl_free_key($pubKey);
|
|
}
|
|
return $signature === 1;
|
|
}
|
|
|
|
/**
|
|
* @throws ApiException
|
|
*/
|
|
function verifyNotify(AsyncNotifyRequest $request): bool
|
|
{
|
|
// 检查签名类型
|
|
$signType = $request->getSignType() ?? null;
|
|
if ($signType !== 'RSA') {
|
|
throw new ApiException("不支持的签名类型: {$signType}");
|
|
}
|
|
|
|
// 检查payload
|
|
$waitStr = $this->object2LinkStr($request->getPayload());
|
|
$formatStr = sprintf(
|
|
"%s&%s&%s&%s&%s&%s&%s&%s&%s&%s&%s&%s&%s",
|
|
$request->getNotifyId() ?? '',
|
|
$request->getSourceId() ?? '',
|
|
$request->getAppId() ?? '',
|
|
$request->getErrCode() ?? '',
|
|
$request->getErrMsg() ?? '',
|
|
$request->getSubErr() ?? '',
|
|
$request->getSubMsg() ?? '',
|
|
$request->getTime() ?? '',
|
|
$request->getOpenId() ?? '',
|
|
$request->getSignType() ?? '',
|
|
$request->getCharset() ?? 'UTF-8',
|
|
$request->getDescription() ?? '',
|
|
$waitStr
|
|
);
|
|
$bodyCharset = $request->getCharset() ?? 'UTF-8';
|
|
$bytesContent = mb_convert_encoding($formatStr, $bodyCharset);
|
|
$digestData = hash('sha256', $bytesContent, true);
|
|
$pubKey = openssl_pkey_get_public($this->config->getApiPublicKey());
|
|
if ($pubKey === false) {
|
|
while ($errMsg = openssl_error_string()) {
|
|
error_log($errMsg);
|
|
}
|
|
return false;
|
|
}
|
|
$signature = openssl_verify($digestData, hex2bin($request->getSign()), $pubKey, OPENSSL_ALGO_SHA256);
|
|
// 检查PHP版本,只有在PHP 7.x版本中才释放公钥资源
|
|
if (PHP_VERSION_ID < 80000) {
|
|
openssl_free_key($pubKey);
|
|
}
|
|
return $signature === 1;
|
|
}
|
|
|
|
} |