銀聯支付核心機制
- 簽名演算法: SHA-1 摘要 + RSA (私鑰簽名/公鑰驗簽)。
- 憑證體系: 商戶持有 簽名證書 (.pfx/.p12 含私鑰) 簽名;銀聯持有 驗簽證書 (.cer 公鑰) 驗證商戶簽名;商戶需配置銀聯發放的 驗簽公鑰證書 驗證銀聯回調簽名。
- 報文格式: Key=Value 字串,字典序排序,
& 連接,不 URL Encode。
UnionPay 核心類別
<?php
namespace Common\Services;
class UnionPay {
public $config = []; // 憑證路徑、前台/後台通知地址、商戶號等
public $params = []; // 提交至銀聯的所有業務參數
// 自動提交表單模板
private $formTemplate = <<<'HTML'
跳轉銀聯支付
正在跳轉支付頁面...
HTML;
// 1. 產生自動提交 HTML 表單
public function createPostForm(): string {
$this->params['signature'] = $this->sign(); // 簽名
$inputs = '';
foreach ($this->params as $k => $v) {
$inputs .= "\t\t\n";
}
return sprintf($this->formTemplate, $this->config['frontUrl'], $inputs);
}
// 2. 簽名 (私鑰 RSA-SHA1)
private function sign(): string {
$data = $this->filterForSign(); // 移除 signature
ksort($data);
$str = $this->arrayToString($data); // key=val&key=val
// 簽名方法 01: SHA1withRSA
if ($this->params['signMethod'] == '01') {
$sha1 = sha1($str);
$signed = $this->rsaSign($sha1); // openssl_sign + base64_encode
return $signed;
}
throw new \InvalidArgumentException('Unsupported signMethod');
}
// 3. 驗簽 (銀聯公鑰 RSA-SHA1)
public function verifySign(): bool {
$pubKey = $this->getVerifyPublicKey(); // 讀取 .cer 並驗證 certId 一致
$data = $this->filterForSign();
ksort($data);
$str = $this->arrayToString($data);
$sha1 = sha1($str);
$signature = base64_decode($this->params['signature']);
return openssl_verify($sha1, $signature, $pubKey) === 1;
}
// --- 私有輔助方法 ---
// filterForSign(): 移除 signature 欄位
// arrayToString(): 陣列轉 key=val&... 字串
// rsaSign(): openssl_sign($data, $sig, $privateKey, OPENSSL_ALGO_SHA1)
// getSignPrivateKey(): openssl_pkcs12_read($pfx, $certs, $pwd) -> $certs['pkey']
// getVerifyPublicKey(): 讀取 .cer -> openssl_x509_read -> openssl_pkey_get_public
// getCertId(): 解析證書 serialNumber (SN) 作為 certId 欄位值
}
?>
配置範例 (params.php)
'unionpay' => [
// 測試環境
'frontUrl' => 'https://gateway.95516.com/gateway/api/frontTransReq.do',
'signCertPath' => __DIR__ . '/../certs/unionpay/test_sign.pfx', // 簽名私鑰證書
'signCertPwd' => '000000', // 證書密碼
'verifyCertPath' => __DIR__ . '/../certs/unionpay/verify_sign.cer', // 銀聯驗簽公鑰
'merId' => '777290058110097', // 測試商戶號
// 生產環境 (上線時切換)
// 'frontUrl' => 'https://gateway.95516.com/gateway/api/frontTransReq.do',
// 'signCertPath' => __DIR__ . '/../certs/unionpay/prod_sign.pfx',
// 'verifyCertPath' => __DIR__ . '/../certs/unionpay/verify_sign.cer',
// 'merId' => 'YOUR_MERCHANT_ID',
],
發起支付 (Controller)
<?php
$up = new UnionPay();
$up->config = Yii::$app->params['unionpay'];
$orderNo = 'ORDER_' . date('YmdHis') . rand(1000,9999);
$amount = 100; // 單位:分
$up->params = [
'version' => '5.0.0',
'encoding' => 'UTF-8',
'signMethod' => '01', // RSA-SHA1
'txnType' => '01', // 消費
'txnSubType' => '01', // 自助消費
'bizType' => '000201', // B2C 網關支付
'channelType' => '08', // 手機 WAP
'accessType' => '0', // 商戶直連
'merId' => $up->config['merId'],
'orderId' => $orderNo,
'txnTime' => date('YmdHis'),
'txnAmt' => $amount, // 分
'currencyCode' => '156', // CNY
'frontUrl' => Url::to(['pay/unionpay-return'], true), // 同步跳轉
'backUrl' => Url::to(['pay/unionpay-notify'], true), // 異步通知
'certId' => $up->getSignCertId(), // 從 .pfx 解析 SN
];
// 直接輸出 HTML 表單,瀏覽器自動跳轉
echo $up->createPostForm();
exit;
?>
異步通知處理 (Notify)
<?php
$up = new UnionPay();
$up->config = Yii::$app->params['unionpay'];
$up->params = Yii::$app->request->post(); // 銀聯 POST 回調參數
if (empty($up->params)) {
echo 'fail'; exit;
}
// 1. 驗簽
if (!$up->verifySign()) {
error_log('UnionPay 驗簽失敗: ' . json_encode($up->params));
echo 'fail'; exit;
}
// 2. 判斷交易狀態
$respCode = $up->params['respCode'] ?? '';
if ($respCode === '00') { // 00 代表成功
$orderId = $up->params['orderId'];
$txnAmt = $up->params['txnAmt'];
$queryId = $up->params['queryId']; // 銀聯流水號
// TODO: 業務邏輯 - 更新訂單、發貨、防重複處理
// 注意: 同一訂單可能多次通知,需冪等處理
echo 'success'; // 必須回應 success/ok 結束
exit;
}
echo 'fail'; // 非成功狀態或驗證失敗
?>
關鍵注意事項
- 證書格式: 簽名證書必須為
.pfx (PKCS#12) 格式含私鑰;驗簽證書為 .cer (X.509) 公鑰。OpenSSL 指令轉換:openssl pkcs12 -in cert.p12 -out cert.pem -nodes。
- 金額單位:
txnAmt 固定為 分 (整數),無小數點。
- 編碼統一: 所有參數、報文、頁面編碼必須統一為 UTF-8。
- 測試卡號: 銀聯測試環境提供專用測試卡 (如 622155...),不可使用真實銀行卡。
- 幂等性: 通知處理必須冪等,防止重複扣款/發貨。建議用
queryId (銀聯流水) 做唯一鍵記錄處理狀態。