代码优化

This commit is contained in:
Billy 2025-10-20 10:37:59 +08:00
parent c5185c9722
commit 4d47e50e96
7 changed files with 646 additions and 42 deletions

View File

@ -13,6 +13,13 @@
use think\Env;
// 读取 .env 文件以获取密钥配置
$envFile = __DIR__ . '/../../../.env';
$envVars = [];
if (file_exists($envFile)) {
$envVars = parse_ini_file($envFile);
}
return [
// API基础地址 (生产环境)
'api_base_url' => 'https://yunbusiness.ccb.com/tp_service/txCtrl/server',
@ -37,13 +44,8 @@ return [
'branch_id' => Env::get('ccb.branch_id', '340650000'),
// 密钥配置 (从.env读取BASE64格式不含PEM头尾)
// ⚠️ 注意密钥会在代码中自动添加PEM包装.env中只需要存储BASE64内容
'private_key' => Env::get('ccb.private_key', 'MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALrJmPmtQfP6mURtMxLEXqJHLldN3zYukoaRxG0lw2IdcC86H9C9brFz4YlJ+98z2mdELJaQWu8VWI4actSuPKgHTBr9MSpaii0QQpdINpwXJD9AglIrT7MxhMLYx3qAYDhjKUlC5hnWVYOg4sG32k/3dCebRHY8RDlrXUfHB2+VAgMBAAECgYArgn5R2pv8WymMmOtGudtZbb9LsuYF1v9mvVnGGv/SQQ060w1KMHYye83TjxpOueNsHqNMR0AHZS+Fmn+ZLyUNj9S77oQvUx5HQvY2/TDnsKbETzEMDybIWB+XdLsUkOrB3peVLTbk25i6oSNPOT2Fvd8TWbDqzBL9Ci27uJH72QJBAP/DfDLYoYx9OIRCykkxrDdQVFEkzhXj0wIkLa0Wnf8kP/JfBqvr0AGUPF8nEfh7fLVXYQlh5ab2FL5KvUifSL8CQQC69crW0fryyDHePp6OIVRUbw0T93h52vbGXnoQ6wdvKxZeL3MsfdNUvsJYeSxmtyY+LLgz1p3qOoEn6UpLvCirAkEA4N7qUvY+y3vJdhgXLNV8mkGJcLKQc5SUkJxogHeTQKGJi7ra7ctuXgUMM4jxduxz0CjcS1iEhxBzWn/x/mj1lwJBALgtv39VKLTXx1i7s5Ms/liXdfi/iC3zKbxOAk58WryHY+exMvMXmYMY0Xg7FySxNLl3cJeQy8ydifL5fbmSSTUCQQCj/YUbcTP8BQ6N0AgFdBwmXJyiNkB9zaDI5cEtpSCgq72m8lfn883GJ1MT7nKVXeX69/q5yDQUYiYPBXH4lCEC'),
'public_key' => Env::get('ccb.public_key', 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6yZj5rUHz+plEbTMSxF6iRy5XTd82LpKGkcRtJcNiHXAvOh/QvW6xc+GJSfvfM9pnRCyWkFrvFViOGnLUrjyoB0wa/TEqWootEEKXSDacFyQ/QIJSK0+zMYTC2Md6gGA4YylJQuYZ1lWDoOLBt9pP93Qnm0R2PEQ5a11HxwdvlQIDAQAB'),
// 建行平台公钥如果建行提供则配置否则使用merchant_public_key
'platform_public_key' => Env::get('ccb.platform_public_key', ''),
'private_key' => $envVars['private_key'] ?? '',
'public_key' => $envVars['public_key'] ?? '',
// HTTP请求配置
'http' => [
'timeout' => 30, // 超时时间(秒)

View File

@ -27,53 +27,83 @@ class Ccblife extends Common
protected $noNeedRight = ['*'];
/**
* 建行生活用户登录URL跳转方式
* 建行生活用户登录URL跳转方式或POST方式
* 建行App会携带加密参数跳转到此地址
*
* GET /addons/shopro/ccblife/login
* GET/POST /addons/shopro/ccblife/login
*/
public function login()
{
try {
// 获取URL参数
$ccbParamSJ = $this->request->get('ccbParamSJ', '');
$otherParams = $this->request->get();
// 获取参数(支持 GET 和 POST
$ccbParamSJ = $this->request->param('ccbParamSJ', '');
$otherParams = $this->request->param();
// 验证必要参数
if (empty($ccbParamSJ)) {
$this->error('缺少必要参数');
}
// 获取配置
$config = config('ccblife');
// 从插件配置文件直接加载避免config()缓存问题)
$configFile = __DIR__ . '/../config/ccblife.php';
if (!file_exists($configFile)) {
$this->error('建行配置文件不存在');
}
$config = include $configFile;
// 验证配置
if (empty($config['private_key'])) {
$this->error('配置错误private_key 为空');
}
Log::info('建行登录解密参数: ' . json_encode([
'ccbParamSJ_length' => strlen($ccbParamSJ),
'service_id' => $config['service_id'],
'private_key_length' => strlen($config['private_key'])
], JSON_UNESCAPED_UNICODE));
// 解密参数(使用服务方私钥)
$decryptedParams = CcbUrlDecrypt::decrypt($ccbParamSJ, $config['private_key']);
if (!$decryptedParams) {
$this->error('参数解密失败');
// 检查日志文件获取详细错误
$logFile = RUNTIME_PATH . 'log/' . date('Ym') . '/' . date('d') . '.log';
$errorDetail = '';
if (file_exists($logFile)) {
$logContent = file_get_contents($logFile);
// 提取最后几行日志
$lines = explode("\n", $logContent);
$errorDetail = implode("\n", array_slice($lines, -10));
}
$this->error('参数解密失败,请查看日志: ' . $errorDetail);
}
// 合并所有参数
$allParams = array_merge($otherParams, $decryptedParams);
// 验证解密结果
if (!is_array($decryptedParams)) {
$this->error('解密参数格式错误');
}
// 获取建行用户信息
$ccbUserId = $allParams['userid'] ?? '';
$mobile = $allParams['mobile'] ?? '';
$openId = $allParams['openid'] ?? '';
Log::info('解密参数成功: ' . json_encode($decryptedParams, JSON_UNESCAPED_UNICODE));
// 获取建行用户信息(直接从解密参数中获取)
$ccbUserId = $decryptedParams['userid'] ?? '';
$mobile = $decryptedParams['mobile'] ?? '';
$openId = $decryptedParams['openid'] ?? '';
if (empty($ccbUserId)) {
$this->error('用户信息获取失败');
}
// 处理用户登录/注册
$userInfo = $this->processUserLogin($ccbUserId, $mobile, $openId, $allParams);
$userInfo = $this->processUserLogin($ccbUserId, $mobile, $openId, $decryptedParams);
// 生成商城Token
$this->auth->direct($userInfo['user_id']);
$token = $this->auth->getToken();
// 构建跳转URL
$redirectUrl = $allParams['redirect_url'] ?? '/pages/index/index';
$redirectUrl = $decryptedParams['redirect_url'] ?? '/pages/index/index';
$this->success('登录成功', [
'token' => $token,
@ -83,6 +113,9 @@ class Ccblife extends Common
} catch (\Exception $e) {
Log::error('建行生活登录失败: ' . $e->getMessage());
Log::error('错误文件: ' . $e->getFile());
Log::error('错误行号: ' . $e->getLine());
Log::error('错误堆栈: ' . $e->getTraceAsString());
$this->error($e->getMessage());
}
}

View File

@ -29,8 +29,8 @@ class CcbUrlDecrypt
{
try {
// 调试日志
trace('开始解密建行参数双重BASE64 + RSA', 'info');
trace('ccbParamSJ 长度: ' . strlen($ccbParamSJ), 'info');
self::log('开始解密建行参数双重BASE64 + RSA', 'info');
self::log('ccbParamSJ 长度: ' . strlen($ccbParamSJ), 'info');
// 验证输入
if (empty($ccbParamSJ)) {
@ -43,21 +43,21 @@ class CcbUrlDecrypt
// 步骤1: URLDecode如果还没有解码
$urlDecoded = urldecode($ccbParamSJ);
trace('URLDecode后长度: ' . strlen($urlDecoded), 'info');
self::log('URLDecode后长度: ' . strlen($urlDecoded), 'info');
// 步骤2: 第一次 BASE64 解码
$firstDecode = base64_decode($urlDecoded);
if ($firstDecode === false || empty($firstDecode)) {
throw new \Exception('第一次 BASE64 解码失败');
}
trace('第一次BASE64解码成功长度: ' . strlen($firstDecode), 'info');
self::log('第一次BASE64解码成功长度: ' . strlen($firstDecode), 'info');
// 步骤3: 第二次 BASE64 解码建行特殊之处双重BASE64编码
$secondDecode = base64_decode($firstDecode);
if ($secondDecode === false || empty($secondDecode)) {
throw new \Exception('第二次 BASE64 解码失败');
}
trace('第二次BASE64解码成功长度: ' . strlen($secondDecode), 'info');
self::log('第二次BASE64解码成功长度: ' . strlen($secondDecode), 'info');
// 步骤4: RSA 私钥解密(直接处理二进制数据,不再用 CcbRSA::decrypt
$decrypted = self::rsaPrivateDecrypt($secondDecode, $privateKey);
@ -65,24 +65,30 @@ class CcbUrlDecrypt
throw new \Exception('RSA解密失败');
}
trace('RSA解密成功长度: ' . strlen($decrypted), 'info');
trace('解密内容: ' . $decrypted, 'info');
self::log('RSA解密成功长度: ' . strlen($decrypted), 'info');
self::log('解密内容: ' . $decrypted, 'info');
// 步骤5: 解析参数字符串为数组
$params = [];
parse_str($decrypted, $params);
if (empty($params)) {
throw new \Exception('解析参数失败,结果为空');
}
trace('参数解析成功: ' . json_encode($params, JSON_UNESCAPED_UNICODE), 'info');
// 验证解析结果是否为有效数组
if (!is_array($params)) {
throw new \Exception('解析参数失败,结果不是数组: ' . gettype($params));
}
self::log('参数解析成功: ' . json_encode($params, JSON_UNESCAPED_UNICODE), 'info');
return $params;
} catch (\Exception $e) {
// 记录错误日志
trace('建行URL参数解密失败: ' . $e->getMessage(), 'error');
trace('错误堆栈: ' . $e->getTraceAsString(), 'error');
self::log('建行URL参数解密失败: ' . $e->getMessage(), 'error');
self::log('错误堆栈: ' . $e->getTraceAsString(), 'error');
return false;
}
}
@ -97,6 +103,11 @@ class CcbUrlDecrypt
private static function rsaPrivateDecrypt($encryptedData, $privateKey)
{
try {
self::log('rsaPrivateDecrypt 开始', 'info');
self::log('加密数据长度: ' . strlen($encryptedData), 'info');
self::log('加密数据MD5: ' . md5($encryptedData), 'info');
self::log('私钥长度: ' . strlen($privateKey), 'info');
// 格式化私钥为 PEM 格式
$pemPrivateKey = $privateKey;
if (strpos($pemPrivateKey, '-----BEGIN') === false) {
@ -109,11 +120,23 @@ class CcbUrlDecrypt
// 加载私钥资源
$priKey = openssl_pkey_get_private($pemPrivateKey);
if (!$priKey) {
throw new \Exception('私钥格式错误: ' . openssl_error_string());
$openssl_error = openssl_error_string();
self::log('openssl_pkey_get_private 失败', 'error');
self::log('OpenSSL错误: ' . ($openssl_error ?: '无错误信息'), 'error');
self::log('PEM私钥前200字符: ' . substr($pemPrivateKey, 0, 200), 'error');
throw new \Exception('私钥格式错误: ' . ($openssl_error ?: '未知错误'));
}
// 获取密钥大小
$keyDetails = openssl_pkey_get_details($priKey);
if (!$keyDetails || !isset($keyDetails['bits'])) {
self::log('openssl_pkey_get_details 失败', 'error');
self::log('priKey 类型: ' . gettype($priKey), 'error');
throw new \Exception('无法获取密钥详情');
}
self::log('密钥加载成功,大小: ' . ($keyDetails['bits'] / 8) . ' 字节', 'info');
$keySize = $keyDetails['bits'] / 8; // 1024位 = 128字节
// 分块解密
@ -129,12 +152,15 @@ class CcbUrlDecrypt
$decrypted .= $decryptedBlock;
}
openssl_free_key($priKey);
// PHP 8+ openssl_free_key() 已废弃,资源会自动释放
if (PHP_VERSION_ID < 80000) {
openssl_free_key($priKey);
}
return $decrypted;
} catch (\Exception $e) {
trace('RSA私钥解密错误: ' . $e->getMessage(), 'error');
self::log('RSA私钥解密错误: ' . $e->getMessage(), 'error');
return false;
}
}
@ -153,7 +179,7 @@ class CcbUrlDecrypt
{
// 确保密钥长度为8字节
if (strlen($key) !== 8) {
trace('DES密钥长度必须为8字节当前: ' . strlen($key), 'error');
self::log('DES密钥长度必须为8字节当前: ' . strlen($key), 'error');
return false;
}
@ -169,12 +195,12 @@ class CcbUrlDecrypt
// 移除PKCS5填充
$result = self::removePKCS5Padding($decrypted);
if ($result !== false) {
trace('使用phpseclib解密成功', 'info');
self::log('使用phpseclib解密成功', 'info');
return $result;
}
}
} catch (\Exception $e) {
trace('phpseclib解密失败: ' . $e->getMessage(), 'error');
self::log('phpseclib解密失败: ' . $e->getMessage(), 'error');
}
}
@ -191,15 +217,15 @@ class CcbUrlDecrypt
// 移除PKCS5填充
$result = self::removePKCS5Padding($decrypted);
if ($result !== false) {
trace('使用OpenSSL解密成功', 'info');
self::log('使用OpenSSL解密成功', 'info');
return $result;
}
}
} catch (\Exception $e) {
trace('OpenSSL解密失败: ' . $e->getMessage(), 'error');
self::log('OpenSSL解密失败: ' . $e->getMessage(), 'error');
}
trace('所有DES解密方法都失败', 'error');
self::log('所有DES解密方法都失败', 'error');
return false;
}
@ -357,4 +383,31 @@ class CcbUrlDecrypt
$separator = strpos($baseUrl, '?') === false ? '?' : '&';
return $baseUrl . $separator . $queryString;
}
/**
* 日志记录辅助方法
* 兼容 trace() 函数和独立使用
*
* @param string $message 日志消息
* @param string $level 日志级别
* @return void
*/
private static function log($message, $level = 'info')
{
// 如果 trace() 函数存在,使用它
if (function_exists('trace')) {
trace($message, $level);
return;
}
// 否则输出到标准错误流CLI模式和 error_log
$prefix = strtoupper($level);
$logMessage = "[{$prefix}] {$message}";
if (PHP_SAPI === 'cli') {
fwrite(STDERR, $logMessage . PHP_EOL);
}
error_log($logMessage);
}
}

View File

@ -0,0 +1,152 @@
<?php
/**
* 诊断建行加密方式
* 尝试不同的解密方法
*/
require_once __DIR__ . '/../library/ccblife/CcbRSA.php';
use addons\shopro\library\ccblife\CcbRSA;
echo "========================================\n";
echo " 建行加密方式诊断\n";
echo "========================================\n\n";
// 从.env读取密钥
$envFile = __DIR__ . '/../../../.env';
$envLines = file($envFile, FILE_IGNORE_NEW_LINES);
$privateKey = '';
$publicKey = '';
foreach ($envLines as $line) {
if (stripos($line, 'private_key=') === 0) {
$privateKey = trim(substr($line, 12));
}
if (stripos($line, 'public_key=') === 0) {
$publicKey = trim(substr($line, 11));
}
}
echo "1. 密钥信息\n";
echo " 私钥长度: " . strlen($privateKey) . "\n";
echo " 公钥长度: " . strlen($publicKey) . "\n\n";
// 步骤1: 验证密钥对是否配对
echo "2. 验证密钥对是否配对\n";
try {
$testData = "test=123&name=hello";
$encrypted = CcbRSA::encrypt($testData, $publicKey);
$decrypted = CcbRSA::decrypt($encrypted, $privateKey);
if ($decrypted === $testData) {
echo " ✅ 密钥对配对正确\n\n";
} else {
echo " ❌ 密钥对不匹配\n\n";
exit;
}
} catch (\Exception $e) {
echo " ❌ 密钥验证失败: " . $e->getMessage() . "\n\n";
exit;
}
// 建行的加密参数
$ccbParamSJ = 'cURYdUJxQ2pJTlVWQUltSW9DQWN1T2xJeFI4WjR2ZVY0NENTVmxPQW9RUVJ1bnJ2Ync5MkwzS2dXdDF1TTVSd3NvUFlsZWFURlYvK243aFNGaWRNeTJsZXRxM1VJR2k0VFdqNnU0Z2JXRnVxMnVIcjhzSVcrRW4xOGxaWDA4Snk0VW1hcFllZ05pekxmc2h6dGFCOTloTUtnVXh2ekJNbDFXZzdwT3lOcDBHZGx5N0FidW1FY1NudmlhNGcxZFAwM09BRk5sUE9USkM4QlJNNWtGTzRKVjFqTWx6RlhrblNwWG0rYmxpS2kyOFpYLy91OGZCR1NPNGp2dHN6b3N1TGxNZnUzd2JNeVUyOVpFTzYxWDRyNm0yb2c0ekZCQWpjc1VLdU9Kcmh1VjVwVDdQZlBlQ0MwRWhLQXFWWXBOdi9uS0FQNkZkZ2JER0ljN2tlUXNHdlFMTWlDeU9rVzdFU2phdG5jYjhZOUNaWkVEb29OdGVLdmlscXg4eHRFS1VtWHljU2ZNeUlDbk5yRW5nR3VXS1AvN3NacTNISE5BWSt2azBHdU1IaG5UWkl6bTdXUmhVYnNRclRYb0ljNy9UTnZVT3lmSVFIcTZ0KzZpZGdtc1NDejBkajVsSjkzVXNSM0NWZExCUk85NHQxZGNEOWorUnFhV2hrWmpiYUE4SlY=';
echo "3. 尝试不同的解密方法\n\n";
// 方法1: 标准RSA解密用商户私钥
echo "方法1: 使用商户私钥解密(标准方式)\n";
try {
$urlDecoded = urldecode($ccbParamSJ);
$decrypted = CcbRSA::decrypt($urlDecoded, $privateKey);
echo " 解密成功,长度: " . strlen($decrypted) . "\n";
// 检查是否是可打印字符
$isPrintable = ctype_print($decrypted);
echo " 是否为可打印字符: " . ($isPrintable ? '是' : '否') . "\n";
if ($isPrintable) {
echo " 解密内容: {$decrypted}\n";
parse_str($decrypted, $params);
if (!empty($params)) {
echo " ✅ 解析成功!参数:\n";
foreach ($params as $k => $v) {
echo " {$k} = {$v}\n";
}
}
} else {
echo " 内容预览(十六进制): " . bin2hex(substr($decrypted, 0, 50)) . "...\n";
echo " ❌ 解密结果不是有效文本\n";
}
} catch (\Exception $e) {
echo " ❌ 解密失败: " . $e->getMessage() . "\n";
}
echo "\n";
// 方法2: 尝试用公钥解密(建行可能用建行私钥加密)
echo "方法2: 尝试用商户公钥解密(反向,测试建行是否用私钥加密)\n";
try {
$urlDecoded = urldecode($ccbParamSJ);
// 格式化公钥为PEM
$pemPublicKey = "-----BEGIN PUBLIC KEY-----\n";
$pemPublicKey .= chunk_split($publicKey, 64, "\n");
$pemPublicKey .= "-----END PUBLIC KEY-----\n";
$pubKeyResource = openssl_pkey_get_public($pemPublicKey);
if (!$pubKeyResource) {
throw new \Exception('公钥格式错误');
}
// BASE64解码
$encryptedData = base64_decode($urlDecoded);
// 获取密钥大小
$keyDetails = openssl_pkey_get_details($pubKeyResource);
$keySize = $keyDetails['bits'] / 8;
// 分块解密
$dataBlocks = str_split($encryptedData, $keySize);
$decrypted = '';
foreach ($dataBlocks as $block) {
$decryptedBlock = '';
// 尝试用公钥解密
$success = @openssl_public_decrypt($block, $decryptedBlock, $pubKeyResource, OPENSSL_PKCS1_PADDING);
if (!$success) {
throw new \Exception('公钥解密失败');
}
$decrypted .= $decryptedBlock;
}
openssl_free_key($pubKeyResource);
echo " 解密成功,长度: " . strlen($decrypted) . "\n";
$isPrintable = ctype_print($decrypted);
echo " 是否为可打印字符: " . ($isPrintable ? '是' : '否') . "\n";
if ($isPrintable) {
echo " 解密内容: {$decrypted}\n";
echo " ✅ 可能建行用的是建行私钥加密!\n";
} else {
echo " ❌ 解密结果不是有效文本\n";
}
} catch (\Exception $e) {
echo " ❌ 解密失败: " . $e->getMessage() . "\n";
}
echo "\n";
// 方法3: 检查是否需要建行公钥
echo "方法3: 建议检查项\n";
echo " - 确认建行后台配置的商户公钥是否为:\n";
echo " " . substr($publicKey, 0, 50) . "...\n";
echo " - 检查建行是否提供了建行平台公钥(用于解密建行发来的数据)\n";
echo " - 联系建行技术支持确认ccbParamSJ的加密方式\n";
echo "\n========================================\n";
echo "诊断完成\n";
echo "========================================\n";

View File

@ -0,0 +1,147 @@
<?php
/**
* 正确的建行解密流程(参考 Java demo
*/
require_once __DIR__ . '/../library/ccblife/CcbRSA.php';
use addons\shopro\library\ccblife\CcbRSA;
echo "========================================\n";
echo " 建行 ccbParamSJ 解密测试\n";
echo " (参考 Java demo 实现)\n";
echo "========================================\n\n";
// 从.env读取私钥
$envFile = __DIR__ . '/../../../.env';
$envLines = file($envFile, FILE_IGNORE_NEW_LINES);
$privateKey = '';
foreach ($envLines as $line) {
if (stripos($line, 'private_key=') === 0) {
$privateKey = trim(substr($line, 12));
break;
}
}
if (empty($privateKey)) {
die("错误: 无法读取私钥\n");
}
echo "私钥长度: " . strlen($privateKey) . "\n\n";
// 建行传来的密文
$ccbParamSJ = 'cURYdUJxQ2pJTlVWQUltSW9DQWN1T2xJeFI4WjR2ZVY0NENTVmxPQW9RUVJ1bnJ2Ync5MkwzS2dXdDF1TTVSd3NvUFlsZWFURlYvK243aFNGaWRNeTJsZXRxM1VJR2k0VFdqNnU0Z2JXRnVxMnVIcjhzSVcrRW4xOGxaWDA4Snk0VW1hcFllZ05pekxmc2h6dGFCOTloTUtnVXh2ekJNbDFXZzdwT3lOcDBHZGx5N0FidW1FY1NudmlhNGcxZFAwM09BRk5sUE9USkM4QlJNNWtGTzRKVjFqTWx6RlhrblNwWG0rYmxpS2kyOFpYLy91OGZCR1NPNGp2dHN6b3N1TGxNZnUzd2JNeVUyOVpFTzYxWDRyNm0yb2c0ekZCQWpjc1VLdU9Kcmh1VjVwVDdQZlBlQ0MwRWhLQXFWWXBOdi9uS0FQNkZkZ2JER0ljN2tlUXNHdlFMTWlDeU9rVzdFU2phdG5jYjhZOUNaWkVEb29OdGVLdmlscXg4eHRFS1VtWHljU2ZNeUlDbk5yRW5nR3VXS1AvN3NacTNISE5BWSt2azBHdU1IaG5UWkl6bTdXUmhVYnNRclRYb0ljNy9UTnZVT3lmSVFIcTZ0KzZpZGdtc1NDejBkajVsSjkzVXNSM0NWZExCUk85NHQxZGNEOWorUnFhV2hrWmpiYUE4SlY=';
echo "========================================\n";
echo "开始解密...\n";
echo "========================================\n\n";
try {
// 步骤1: URLDecode
echo "步骤1: URLDecode\n";
$step1 = urldecode($ccbParamSJ);
echo " 长度: " . strlen($step1) . "\n";
echo " 是否改变: " . ($step1 === $ccbParamSJ ? '否' : '是') . "\n\n";
// 步骤2: 第一次 BASE64 解码
echo "步骤2: 第一次 BASE64 解码\n";
$step2 = base64_decode($step1);
if ($step2 === false) {
throw new \Exception('第一次 BASE64 解码失败');
}
echo " 解码后长度: " . strlen($step2) . "\n";
echo " 内容预览: " . substr($step2, 0, 50) . "...\n";
// 检查是否还是 BASE64
$isBase64 = preg_match('/^[A-Za-z0-9+\/=]+$/', $step2);
echo " 是否仍为 BASE64: " . ($isBase64 ? '是(双重编码)' : '否') . "\n\n";
// 步骤3: 第二次 BASE64 解码(建行用了双重 BASE64
echo "步骤3: 第二次 BASE64 解码\n";
$step3 = base64_decode($step2);
if ($step3 === false) {
throw new \Exception('第二次 BASE64 解码失败');
}
echo " 解码后长度: " . strlen($step3) . "\n";
echo " 十六进制预览: " . bin2hex(substr($step3, 0, 32)) . "...\n\n";
// 步骤4: 用私钥 RSA 解密
echo "步骤4: RSA 私钥解密\n";
// 注意:这里不能再用 CcbRSA::decrypt(),因为它内部会再次 BASE64 解码
// 我们需要直接调用 openssl_private_decrypt
// 格式化私钥
$pemPrivateKey = $privateKey;
if (strpos($pemPrivateKey, '-----BEGIN') === false) {
$pemPrivateKey = preg_replace('/\s+/', '', $pemPrivateKey);
$pemPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n" .
chunk_split($pemPrivateKey, 64, "\n") .
"-----END RSA PRIVATE KEY-----\n";
}
$priKey = openssl_pkey_get_private($pemPrivateKey);
if (!$priKey) {
throw new \Exception('私钥格式错误: ' . openssl_error_string());
}
// 获取密钥大小
$keyDetails = openssl_pkey_get_details($priKey);
$keySize = $keyDetails['bits'] / 8; // 128 字节 (1024 位)
echo " 密钥大小: {$keySize} 字节\n";
echo " 加密数据长度: " . strlen($step3) . " 字节\n";
// 分块解密
$blocks = str_split($step3, $keySize); // 使用 step3第二次解码后的数据
$decrypted = '';
echo " 分块数: " . count($blocks) . "\n";
foreach ($blocks as $i => $block) {
$decryptedBlock = '';
$success = openssl_private_decrypt($block, $decryptedBlock, $priKey, OPENSSL_PKCS1_PADDING);
if (!$success) {
throw new \Exception("解密第 " . ($i+1) . " 块失败: " . openssl_error_string());
}
$decrypted .= $decryptedBlock;
}
openssl_free_key($priKey);
echo " 解密成功!\n";
echo " 解密后长度: " . strlen($decrypted) . "\n\n";
// 步骤5: 显示结果
echo "步骤5: 解密结果\n";
echo " 原始内容:\n";
echo " ----------------------------------------\n";
echo " {$decrypted}\n";
echo " ----------------------------------------\n\n";
// 步骤6: 解析参数
echo "步骤6: 解析 URL 参数\n";
parse_str($decrypted, $params);
if (empty($params)) {
echo " ⚠️ 解析为空,可能不是 URL 参数格式\n";
} else {
echo " 解析成功!参数列表:\n";
foreach ($params as $key => $value) {
echo " {$key} = {$value}\n";
}
}
echo "\n========================================\n";
echo "✅ 解密成功!\n";
echo "========================================\n";
} catch (\Exception $e) {
echo "\n========================================\n";
echo "❌ 解密失败\n";
echo "========================================\n";
echo "错误: " . $e->getMessage() . "\n";
echo "堆栈:\n" . $e->getTraceAsString() . "\n";
}

View File

@ -0,0 +1,136 @@
<?php
/**
* RSA解密测试脚本
* 测试建行ccbParamSJ参数解密
*/
// 手动加载必要的类
require_once __DIR__ . '/../library/ccblife/CcbRSA.php';
use addons\shopro\library\ccblife\CcbRSA;
echo "========================================\n";
echo " 建行 RSA 解密测试\n";
echo "========================================\n\n";
// 测试参数
$ccbParamSJ = 'cURYdUJxQ2pJTlVWQUltSW9DQWN1T2xJeFI4WjR2ZVY0NENTVmxPQW9RUVJ1bnJ2Ync5MkwzS2dXdDF1TTVSd3NvUFlsZWFURlYvK243aFNGaWRNeTJsZXRxM1VJR2k0VFdqNnU0Z2JXRnVxMnVIcjhzSVcrRW4xOGxaWDA4Snk0VW1hcFllZ05pekxmc2h6dGFCOTloTUtnVXh2ekJNbDFXZzdwT3lOcDBHZGx5N0FidW1FY1NudmlhNGcxZFAwM09BRk5sUE9USkM4QlJNNWtGTzRKVjFqTWx6RlhrblNwWG0rYmxpS2kyOFpYLy91OGZCR1NPNGp2dHN6b3N1TGxNZnUzd2JNeVUyOVpFTzYxWDRyNm0yb2c0ekZCQWpjc1VLdU9Kcmh1VjVwVDdQZlBlQ0MwRWhLQXFWWXBOdi9uS0FQNkZkZ2JER0ljN2tlUXNHdlFMTWlDeU9rVzdFU2phdG5jYjhZOUNaWkVEb29OdGVLdmlscXg4eHRFS1VtWHljU2ZNeUlDbk5yRW5nR3VXS1AvN3NacTNISE5BWSt2azBHdU1IaG5UWkl6bTdXUmhVYnNRclRYb0ljNy9UTnZVT3lmSVFIcTZ0KzZpZGdtc1NDejBkajVsSjkzVXNSM0NWZExCUk85NHQxZGNEOWorUnFhV2hrWmpiYUE4SlY=';
echo "输入参数:\n";
echo "ccbParamSJ 长度: " . strlen($ccbParamSJ) . "\n\n";
// 直接使用配置文件中的默认私钥BASE64格式
// 这是配置文件 ccblife.php 中的默认值
$privateKey = 'MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALrJmPmtQfP6mURtMxLEXqJHLldN3zYukoaRxG0lw2IdcC86H9C9brFz4YlJ+98z2mdELJaQWu8VWI4actSuPKgHTBr9MSpaii0QQpdINpwXJD9AglIrT7MxhMLYx3qAYDhjKUlC5hnWVYOg4sG32k/3dCebRHY8RDlrXUfHB2+VAgMBAAECgYArgn5R2pv8WymMmOtGudtZbb9LsuYF1v9mvVnGGv/SQQ060w1KMHYye83TjxpOueNsHqNMR0AHZS+Fmn+ZLyUNj9S77oQvUx5HQvY2/TDnsKbETzEMDybIWB+XdLsUkOrB3peVLTbk25i6oSNPOT2Fvd8TWbDqzBL9Ci27uJH72QJBAP/DfDLYoYx9OIRCykkxrDdQVFEkzhXj0wIkLa0Wnf8kP/JfBqvr0AGUPF8nEfh7fLVXYQlh5ab2FL5KvUifSL8CQQC69crW0fryyDHePp6OIVRUbw0T93h52vbGXnoQ6wdvKxZeL3MsfdNUvsJYeSxmtyY+LLgz1p3qOoEn6UpLvCirAkEA4N7qUvY+y3vJdhgXLNV8mkGJcLKQc5SUkJxogHeTQKGJi7ra7ctuXgUMM4jxduxz0CjcS1iEhxBzWn/x/mj1lwJBALgtv39VKLTXx1i7s5Ms/liXdfi/iC3zKbxOAk58WryHY+exMvMXmYMY0Xg7FySxNLl3cJeQy8ydifL5fbmSSTUCQQCj/YUbcTP8BQ6N0AgFdBwmXJyiNkB9zaDI5cEtpSCgq72m8lfn883GJ1MT7nKVXeX69/q5yDQUYiYPBXH4lCEC';
echo "使用默认私钥(来自配置文件)\n";
echo "私钥长度: " . strlen($privateKey) . "\n\n";
// 执行解密
echo "========================================\n";
echo "开始解密...\n";
echo "========================================\n\n";
try {
// 步骤1: URLDecode
echo "步骤1: URLDecode\n";
$urlDecoded = urldecode($ccbParamSJ);
echo " URLDecode 后长度: " . strlen($urlDecoded) . "\n";
echo " 是否改变: " . ($urlDecoded === $ccbParamSJ ? '否' : '是') . "\n\n";
// 步骤2: 显示密钥信息
echo "步骤2: 私钥信息\n";
echo " 私钥长度: " . strlen($privateKey) . "\n";
echo " 是否包含 PEM 头: " . (strpos($privateKey, '-----BEGIN') !== false ? '是' : '否') . "\n\n";
// 步骤3: 尝试 RSA 解密
echo "步骤3: RSA 解密\n";
// 直接调用 CcbRSA::decrypt
$decrypted = CcbRSA::decrypt($urlDecoded, $privateKey);
echo " 解密成功!\n";
echo " 解密后长度: " . strlen($decrypted) . "\n";
echo " 解密内容: " . $decrypted . "\n\n";
// 步骤4: 解析参数
echo "步骤4: 解析参数\n";
parse_str($decrypted, $params);
echo " 解析结果:\n";
foreach ($params as $key => $value) {
echo " {$key} = {$value}\n";
}
echo "\n========================================\n";
echo "✅ 解密成功!\n";
echo "========================================\n";
} catch (\Exception $e) {
echo "\n========================================\n";
echo "❌ 解密失败!\n";
echo "========================================\n";
echo "错误信息: " . $e->getMessage() . "\n";
echo "错误行号: " . $e->getLine() . "\n";
echo "错误文件: " . $e->getFile() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
// 额外诊断
echo "\n========================================\n";
echo "诊断信息\n";
echo "========================================\n";
// 检查 BASE64 解码
echo "\n1. BASE64 解码测试:\n";
$base64Decoded = base64_decode($urlDecoded);
if ($base64Decoded === false) {
echo " ❌ BASE64 解码失败\n";
} else {
echo " ✓ BASE64 解码成功\n";
echo " 解码后长度: " . strlen($base64Decoded) . "\n";
echo " 十六进制预览: " . bin2hex(substr($base64Decoded, 0, 32)) . "...\n";
}
// 检查私钥格式
echo "\n2. 私钥格式测试:\n";
try {
// 格式化私钥
$formattedKey = $privateKey;
if (strpos($formattedKey, '-----BEGIN') === false) {
$formattedKey = preg_replace('/\s+/', '', $formattedKey);
$formattedKey = "-----BEGIN RSA PRIVATE KEY-----\n" .
chunk_split($formattedKey, 64, "\n") .
"-----END RSA PRIVATE KEY-----\n";
}
$keyResource = openssl_pkey_get_private($formattedKey);
if ($keyResource === false) {
echo " ❌ 私钥格式错误: " . openssl_error_string() . "\n";
} else {
echo " ✓ 私钥格式正确\n";
$keyDetails = openssl_pkey_get_details($keyResource);
echo " 密钥类型: " . ($keyDetails['type'] === OPENSSL_KEYTYPE_RSA ? 'RSA' : '未知') . "\n";
echo " 密钥位数: " . $keyDetails['bits'] . "\n";
openssl_free_key($keyResource);
}
} catch (\Exception $e2) {
echo " ❌ 私钥检查失败: " . $e2->getMessage() . "\n";
}
// 检查数据长度
echo "\n3. 数据长度检查:\n";
if (isset($base64Decoded)) {
$dataLen = strlen($base64Decoded);
echo " 加密数据长度: {$dataLen} 字节\n";
if (isset($keyDetails)) {
$keySize = $keyDetails['bits'] / 8;
$blockCount = $dataLen / $keySize;
echo " 密钥大小: {$keySize} 字节\n";
echo " 分块数量: " . $blockCount . " (应该是整数)\n";
if ($blockCount != floor($blockCount)) {
echo " ⚠️ 警告: 数据长度不是密钥大小的整数倍\n";
}
}
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* 验证 RSA 密钥对是否匹配
*/
require_once __DIR__ . '/../library/ccblife/CcbRSA.php';
use addons\shopro\library\ccblife\CcbRSA;
echo "========================================\n";
echo " RSA 密钥对验证\n";
echo "========================================\n\n";
// 从 .env 读取密钥
$envFile = __DIR__ . '/../../../.env';
$envLines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$privateKey = '';
$publicKey = '';
foreach ($envLines as $line) {
if (stripos($line, 'private_key=') !== false) {
$privateKey = trim(substr($line, strpos($line, '=') + 1));
}
if (stripos($line, 'public_key=') !== false) {
$publicKey = trim(substr($line, strpos($line, '=') + 1));
}
}
echo "1. 密钥信息\n";
echo " 私钥长度: " . strlen($privateKey) . "\n";
echo " 公钥长度: " . strlen($publicKey) . "\n\n";
// 验证密钥对
echo "2. 验证密钥对是否匹配\n";
try {
// 测试数据
$testData = "userid=test123&mobile=13800138000&openid=testOpenId";
echo " 原始数据: {$testData}\n";
// 用公钥加密
$encrypted = CcbRSA::encrypt($testData, $publicKey);
echo " ✓ 公钥加密成功\n";
echo " 加密后长度: " . strlen($encrypted) . "\n";
// 用私钥解密
$decrypted = CcbRSA::decrypt($encrypted, $privateKey);
echo " ✓ 私钥解密成功\n";
echo " 解密结果: {$decrypted}\n";
// 验证
if ($decrypted === $testData) {
echo "\n ✅ 密钥对匹配!公钥和私钥配对正确。\n\n";
} else {
echo "\n ❌ 密钥对不匹配!解密结果与原始数据不一致。\n\n";
}
} catch (\Exception $e) {
echo " ❌ 错误: " . $e->getMessage() . "\n\n";
}
// 显示公钥信息(用于确认是否提交给建行)
echo "3. 你的公钥BASE64格式提交给建行的应该是这个\n";
echo "----------------------------------------\n";
echo $publicKey . "\n";
echo "----------------------------------------\n\n";
echo "4. 你的公钥PEM格式\n";
echo "----------------------------------------\n";
$pemPublicKey = "-----BEGIN PUBLIC KEY-----\n";
$pemPublicKey .= chunk_split($publicKey, 64, "\n");
$pemPublicKey .= "-----END PUBLIC KEY-----\n";
echo $pemPublicKey;
echo "----------------------------------------\n\n";
echo "⚠️ 重要提示:\n";
echo "1. 请确认建行那边配置的公钥是否和上面显示的公钥一致\n";
echo "2. 如果不一致,需要重新提交正确的公钥给建行\n";
echo "3. 如果一致但仍然解密失败,可能是建行加密方式有问题\n";