114 lines
4.5 KiB
PHP
114 lines
4.5 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | 7.15 免登链接 · 编码方式对照工具(仅联调用,CLI)
|
||
// +----------------------------------------------------------------------
|
||
// | 背景:同一条链接在桌面浏览器能打开,微信里打开 / 扫码却提示验签失败。
|
||
// | base64 签名里的 + / = 在不同 URL 解析实现下会被区别对待,
|
||
// | 本工具用同一个签名生成多种编码变体,逐个在微信里试,定位是哪一种。
|
||
// |
|
||
// | 用法(在站点根目录执行):
|
||
// | php deploy/h5_debug.php 13800000000
|
||
// | php deploy/h5_debug.php 13800000000 2 # 指定企业ID,不传用默认企业
|
||
// |
|
||
// | 测法:把打印出来的每条链接发到微信「文件传输助手」,逐条点开,
|
||
// | 记下哪几条能进、哪几条报验签失败,再据此定方案。
|
||
// |
|
||
// | ⚠️ 会生成真实可登录的免登链接,联调结束请删除本文件。
|
||
// +----------------------------------------------------------------------
|
||
declare (strict_types=1);
|
||
|
||
if (PHP_SAPI !== 'cli') {
|
||
http_response_code(403);
|
||
exit('仅限命令行执行');
|
||
}
|
||
|
||
$root = dirname(__DIR__);
|
||
require $root . '/vendor/autoload.php';
|
||
|
||
$app = new \think\App($root . '/');
|
||
$app->initialize();
|
||
|
||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||
use app\common\service\QytConfig;
|
||
|
||
$mobile = $argv[1] ?? '';
|
||
$enterpriseId = (int) ($argv[2] ?? 0);
|
||
|
||
if (!preg_match('/^1[3-9]\d{9}$/', $mobile)) {
|
||
exit("用法:php deploy/h5_debug.php <司机手机号> [企业ID]\n");
|
||
}
|
||
|
||
$enterprise = EnterpriseModel::resolve($enterpriseId);
|
||
if (!$enterprise) {
|
||
exit("未找到可用企业,请先在后台「设置 → 企业管理」建企业并设为默认\n");
|
||
}
|
||
|
||
$conf = QytConfig::forEnterprise($enterprise);
|
||
$base = rtrim((string) $conf['h5_base_url'], '/');
|
||
$appId = (string) ($conf['h5_app_id'] ?: $conf['app_id']);
|
||
|
||
// ---------- 按 7.15 规则签名(ksort -> http_build_query -> urldecode -> 大写MD5 -> SHA1withRSA -> base64) ----------
|
||
$params = [
|
||
'appId' => $appId,
|
||
'mobile' => $mobile,
|
||
'timestamp' => date('YmdHis'),
|
||
];
|
||
!empty($conf['h5_with_creditcode']) && $params['creditcode'] = (string) $conf['business_license'];
|
||
|
||
$forSign = $params;
|
||
ksort($forSign);
|
||
$raw = urldecode(http_build_query($forSign));
|
||
$md5 = strtoupper(md5($raw));
|
||
|
||
$pem = trim((string) $conf['private_key']);
|
||
if (strpos($pem, '-----BEGIN') === false) {
|
||
$pem = "-----BEGIN PRIVATE KEY-----\n" . chunk_split($pem, 64, "\n") . "-----END PRIVATE KEY-----\n";
|
||
}
|
||
$pkey = openssl_pkey_get_private($pem);
|
||
if ($pkey === false) {
|
||
exit("私钥无效:" . openssl_error_string() . "\n");
|
||
}
|
||
$sig = '';
|
||
openssl_sign($md5, $sig, $pkey, OPENSSL_ALGO_SHA1);
|
||
$sign = base64_encode($sig);
|
||
|
||
// ---------- 输出 ----------
|
||
$build = function (array $p, string $sign, string $tail) {
|
||
$pairs = [];
|
||
foreach ($p as $k => $v) {
|
||
$pairs[] = $k . '=' . $v;
|
||
}
|
||
$pairs[] = 'sign=' . $sign;
|
||
return '/?' . implode('&', $pairs) . $tail;
|
||
};
|
||
|
||
echo "企业:{$enterprise['company_name']}({$enterprise['business_license']},状态 {$enterprise['status']})\n";
|
||
echo "通道:{$conf['passageway_name']} appId:{$appId}\n";
|
||
echo "待签串:{$raw}\n";
|
||
echo "大写MD5:{$md5}\n";
|
||
echo "签名含 + 的个数:" . substr_count($sign, '+') . ",含 / 的个数:" . substr_count($sign, '/') . "\n";
|
||
echo str_repeat('=', 70) . "\n\n";
|
||
|
||
$variants = [
|
||
'A. 原样(当前线上行为)' => [$sign, '#/'],
|
||
'B. 只把 + 编码为 %2B' => [str_replace('+', '%2B', $sign), '#/'],
|
||
'C. 把 + 和 / 都编码' => [str_replace(['+', '/'], ['%2B', '%2F'], $sign), '#/'],
|
||
'D. 全部编码(改之前的老行为)' => [rawurlencode($sign), '#/'],
|
||
'E. 原样,但去掉末尾的 #/' => [$sign, ''],
|
||
];
|
||
|
||
$i = 1;
|
||
foreach ($variants as $label => [$s, $tail]) {
|
||
echo "【{$i}】{$label}\n";
|
||
echo $base . $build($params, $s, $tail) . "\n\n";
|
||
$i++;
|
||
}
|
||
|
||
echo str_repeat('-', 70) . "\n";
|
||
echo "把上面 5 条分别发到微信里点开,记录结果:\n";
|
||
echo " 只有 A 失败、B 成功 -> 问题是 +,对方页面会解码,我们编码 + 即可\n";
|
||
echo " A、B 都失败、C 成功 -> + 和 / 都要编码\n";
|
||
echo " A 成功、其余失败 -> 不是编码问题,重点查 E(末尾 #/)与微信追加参数\n";
|
||
echo " 只有 E 成功 -> 末尾的 #/ 被算进了 sign\n";
|
||
echo " 全都失败 -> 不是 URL 层面的问题,查公钥是否配对、企业是否已审核通过\n";
|