第一次更新,完成了初版调整

This commit is contained in:
2026-09-07 22:05:51 +08:00
parent b5a26a8848
commit 4d6a7e7aac
198 changed files with 6142 additions and 3856 deletions
+182
View File
@@ -0,0 +1,182 @@
<?php
// +----------------------------------------------------------------------
// | 企油通 · 签名 / 验签 / 请求信封
// +----------------------------------------------------------------------
// | 第二阶段 A-1 骨架。标 【TODO 待对接人确认】 处需按企油通实际规则微调。
// |
// | 三种签名场景(见《企油通接口对接文档.md》§2.1 / §2.4):
// | 1. 业务信封:外层 { appId, timestamp, sign, params(JSON字符串) }
// | 2. 7.15 H5 免登地址:ksort → http_build_query → urldecode → 大写MD5 → SHA1withRSA → base64
// | 3. 7.16 / 7.17:必须真实 RSA 验签调用(本质同 1,但企油通侧强校验)
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\common\crypto;
class Qyt
{
/** @var string 我方私钥资源 */
protected $privateKey;
/** @var string|false 企油通平台公钥资源 */
protected $platformPublicKey;
/** @var array config('qyt') */
protected $conf;
public function __construct(array $conf = null)
{
$this->conf = $conf ?: (array) config('qyt');
$this->privateKey = $this->loadPrivateKey($this->conf['private_key'] ?? '');
$this->platformPublicKey = $this->loadPublicKey($this->conf['platform_public_key'] ?? '');
}
// ================================================================
// 1. 业务信封
// ================================================================
/**
* 构建业务请求信封
* @param array $bizParams 业务参数(会被 json_encode 成字符串放进 params)
* @return array{appId:string,timestamp:int,sign:string,params:string}
*/
public function envelope(array $bizParams): array
{
$appId = (string) ($this->conf['app_id'] ?? '');
$timestamp = time();
// ⚠️ params 必须是 JSON 字符串(转义),不是对象
$paramsJson = json_encode($bizParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$sign = $this->bizSign([
'appId' => $appId,
'timestamp' => $timestamp,
'params' => $paramsJson,
]);
return [
'appId' => $appId,
'timestamp' => $timestamp,
'sign' => $sign,
'params' => $paramsJson,
];
}
/**
* 业务信封签名
* 【TODO 待对接人确认】业务接口的具体拼签字段与顺序,文档 §2.4 只给了 7.15 的算法,
* 业务接口写的是「密钥、公私钥与具体拼签字段请向对接人员索取」。
* 当前默认实现:对参与字段 ksort 后 http_build_query → urldecode → 大写MD5 → SHA1withRSA → base64
* (与 7.15 同族算法,落地时按实际规则替换本方法即可,调用方无需改动)
*
* @param array $fields 参与签名的字段(appId / timestamp / params)
*/
public function bizSign(array $fields): string
{
$mode = $this->conf['sign_mode'] ?? 'ksort_md5_sha1rsa';
switch ($mode) {
// 预留:'raw_concat_sha256rsa' 等其它模式
case 'ksort_md5_sha1rsa':
default:
return $this->md5Sha1RsaSign($fields);
}
}
// ================================================================
// 2. 7.15 H5 免登地址
// ================================================================
/**
* 生成司机免登加油 H5 跳转地址(7.15)
* @param string $mobile 司机手机号
* @param string $creditcode 企业营业执照号;传空则不带(不参与签名)
* @return string 完整可跳转 URL
*/
public function h5Url(string $mobile, string $creditcode = ''): string
{
$base = rtrim((string) ($this->conf['h5_base_url'] ?? ''), '/');
$appId = (string) ($this->conf['h5_app_id'] ?: $this->conf['app_id']);
$timestamp = date('YmdHis');
$params = [
'appId' => $appId,
'mobile' => $mobile,
'timestamp' => $timestamp,
];
if ($creditcode !== '') {
$params['creditcode'] = $creditcode; // 传了必须一并入签
}
$params['sign'] = $this->md5Sha1RsaSign($params);
// 文档示例形如 https://paytaskpage.worknoya.vip/#/?appId=..&mobile=..&timestamp=..&sign=..#/
return $base . '/?' . http_build_query($params) . '#/';
}
// ================================================================
// 3. 验签(企油通响应 / 回调)
// ================================================================
/**
* 验签企油通返回 / 回调报文
* @param array $data 业务数据(通用响应里的 data,或回调 params)
* @param string $sign 报文里的 sign
* 【TODO 待对接人确认】验签串的构造(对 data 的 json 形态 / 是否含外层字段)
*/
public function verify($data, string $sign): bool
{
if ($sign === '' || $this->platformPublicKey === false) {
return false;
}
$payload = is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$ok = openssl_verify($payload, base64_decode($sign), $this->platformPublicKey, OPENSSL_ALGO_SHA256);
return $ok === 1;
}
// ================================================================
// 内部:算法与密钥
// ================================================================
/**
* 同族签名算法:ksort → http_build_query → urldecode → 大写MD5 → SHA1withRSA → base64
*/
protected function md5Sha1RsaSign(array $params): string
{
ksort($params);
$raw = urldecode(http_build_query($params));
$md5Upper = strtoupper(md5($raw));
$signature = '';
openssl_sign($md5Upper, $signature, $this->privateKey, OPENSSL_ALGO_SHA1);
return base64_encode($signature);
}
/**
* 载入私钥:支持 PEM 全文 / 去头尾的单行 base64
* @return \OpenSSLAsymmetricKey|resource|string
*/
protected function loadPrivateKey(string $key)
{
if ($key === '') {
return '';
}
if (strpos($key, '-----BEGIN') === false) {
$key = "-----BEGIN PRIVATE KEY-----\n" . chunk_split(trim($key), 64, "\n") . "-----END PRIVATE KEY-----\n";
}
$res = openssl_pkey_get_private($key);
return $res === false ? '' : $res;
}
/**
* 载入公钥:支持 PEM 全文 / 去头尾的单行 base64
* @return \OpenSSLAsymmetricKey|resource|false
*/
protected function loadPublicKey(string $key)
{
if ($key === '') {
return false;
}
if (strpos($key, '-----BEGIN') === false) {
$key = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(trim($key), 64, "\n") . "-----END PUBLIC KEY-----\n";
}
return openssl_pkey_get_public($key);
}
}