Files
oil/server-tp6/app/common/crypto/Qyt.php
T
2026-09-18 20:55:21 +08:00

202 lines
8.3 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 ?: \app\common\service\QytConfig::get();
$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=iwxE****g==
//
// ⚠️ 两个坑,都是实测踩出来的,改动前务必看完:
//
// 1) 不能用 http_build_query:它会把 base64 签名里的 + / = 编码成 %2B %2F %3D。
// 参数挂在 # 之后的 fragment 里,浏览器不会自动解码,落地页若不做
// decodeURIComponent,拿到的签名串就和我们签的不一致 -> 验签失败。
// 文档示例里末尾是裸的 ==,对方期望的就是未编码的原始值。
// appId / mobile / timestamp / creditcode 都是字母数字,本来也无需编码。
// 若对接人日后确认页面会解码,把 h5_sign_urlencode 配成 true 即可切回。
//
// 2) 末尾【不能】再拼 '#/'。文档示例末尾是有 #/ 的,但实测:带上之后
// 桌面浏览器能打开,微信里打开 / 扫码一律提示验签失败 —— 落地页会把
// 这个 #/ 一起算进最后一个参数 sign 的值里。去掉后微信正常。
// ⚠️ 别照着文档示例把 '#/' 加回来。
$encode = !empty($this->conf['h5_sign_urlencode']);
$pairs = [];
foreach ($params as $k => $v) {
$pairs[] = $k . '=' . ($encode ? rawurlencode((string) $v) : (string) $v);
}
return $base . '/?' . implode('&', $pairs);
}
// ================================================================
// 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);
}
}