Files
oil/server-tp6/app/common/crypto/Qyt.php
T

183 lines
7.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}