申请账号官网 https://open.unionpay.com/tjweb/index
开放平台 https://open.unionpay.com/tjweb/support/faq/mchlist?id=21
<?php
namespace Foreend\ThirdParty\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Log;
const COMPANY = "中国银联股份有限公司";
// 银联对接 接口
class UnionPayController extends Controller
{
private static $config_merId = '777290058184064' ; //商户代码,已被批准加入银联互联网系统的商户代码
private static $pfx_pwd = '000000' ; // 签名证书密码,测试环境固定000000,生产环境请修改为从 cfca 下载的正式证书的密码,正式环境证书密码位数需小于等于6位,否则上传到商户服务网站会失败
private static $pay_environment = 'test' ; // 测试环境 test 正式环境 formal
private static $config = [] ;
private static $pfx_url = '' ;
private static $enc_url = '' ;
private static $middle_url = '' ;
private static $root_url = '' ;
private static $pay_environment_url = '' ; // 当前接口 请求的地址
private static $url = [ // 测试 支付网址
'test'=>[
'appTransUrl'=>'https://gateway.test.95516.com/gateway/api/appTransReq.do', // 支付接口 获取 tn
'singleQueryUrl'=>'https://gateway.test.95516.com/gateway/api/queryTrans.do', // 查询接口 获取支付信息
],
'formal'=>[
'appTransUrl'=>'https://gateway.95516.com/gateway/api/appTransReq.do', // 支付接口 获取 tn
'singleQueryUrl'=>'https://gateway.95516.com/gateway/api/queryTrans.do', // 查询接口 获取支付信息
]
] ;
private static $verifyCerts510 = [];
private static $ifValidateCNName = false; // 是否验证验签证书的CN,测试环境请设置false,生产环境请设置true。非false的值默认都当true处理。
private static $ifValidateRemoteCert = false; // 是否验证https证书,测试环境请设置false,生产环境建议优先尝试true,不行再false。非true的值默认都当false处理。
private static $config_version = '5.1.0' ; //版本号
private static $config_encoding = 'utf-8' ; //编码方式
private static $config_signMethod = '01' ; //签名方法
private static $config_txnType = '01' ; // 交易类型
/** $config_txnType
// 00 查询交易 01:消费 02:预授权 03:预授权完成 04:退货 05: 圈存 11:代收 12:代付 13:账单支付 14:转账(保留) 21:批量交易 22:批量查询
// 31:消费撤销 32:预授权撤销 33:预授权完成撤销 71:余额查询 72:实名认证-建立绑定关系 73: 账单查询
// 74:解除绑定关系 75:查询绑定关系 77:发送短信验证码交易 78:开通查询交易 79:开通交易 94:IC 卡脚本通知
*/
private static $config_txnSubType = '01' ; //交易子类 依据实际交易类型填写 默认取值:01
private static $config_bizType = '000201' ; //产品类型
/** $config_bizType
依据实际业务场景填写(目前仅使用后 4 位,签名 2 位
默认为 00)
默认取值:000000
具体取值范围:
000101 基金业务之股票基金
000102 基金业务之货币基金
000201 B2C 网关支付
000301 认证支付 2.0
000302 评级支付
000401 代付
000501 代收
000601 账单支付
000801 跨行收单
000901 绑定支付
001001 订购
000202 B2B
*/
/**
* @param $data
* @return array
*/
public static function pay($data){
self::$config = [
'version' => self::$config_version,
'encoding' => self::$config_encoding,
'bizType' => self::$config_bizType,
'txnTime' => date('YmdHis'), //订单发送时间,格式为YYYYMMDDhhmmss,取北京时间,此处默认取demo演示页面传递的参数
'backUrl' => url('api/union/notify_url'), //后台通知地址
'currencyCode' => '156', //交易币种,境内商户固定156
'txnAmt' => $data['pay_amount'] * 100, //交易金额,单位分,此处默认取demo演示页面传递的参数
'txnType' => self::$config_txnType,
'txnSubType' => self::$config_txnSubType,
'accessType' => '0', // 接入类型
// 'signature'=>'', // 后面获取
'signMethod' => self::$config_signMethod, // 01(表示采用RSA签名) HASH表示散列算法
'channelType' => '08', //渠道类型,07-PC,08-手机
'merId' => self::$config_merId,
'orderId' => $data['numbers'], //商户订单号,8-32位数字字母,不能含“-”或“_”,此处默认取demo演示页面传递的参数,可以自行定制规则
'orderDesc' => $data['shop_name'], // 订单名称 这个好像没啥用
// 'frontUrl' => url('api/union/return_url'), //前台通知地址
//TODO 以下信息需要填写
// 请求方保留域,
// 透传字段,查询、通知、对账文件中均会原样出现,如有需要请启用并修改自己希望透传的数据。
// 出现部分特殊字符时可能影响解析,请按下面建议的方式填写:
// 1. 如果能确定内容不会出现&={}[]"'等符号时,可以直接填写数据,建议的方法如下。
// 'reqReserved' =>'透传信息1|透传信息2|透传信息3',
// 2. 内容可能出现&={}[]"'符号时:
// 1) 如果需要对账文件里能显示,可将字符替换成全角&={}【】“‘字符(自己写代码,此处不演示);
// 2) 如果对账文件没有显示要求,可做一下base64(如下)。
// 注意控制数据长度,实际传输的数据长度不能超过1024位。
// 查询、通知等接口解析时使用base64_decode解base64后再对数据做后续解析。
// 'reqReserved' => base64_encode('任意格式的信息都可以'),
//TODO 其他特殊用法请查看 pages/api_05_app/special_use_purchase.php
] ;
// 证书存放路径
self::$pfx_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_sign.pfx' ;
self::$enc_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_enc.cer' ;
self::$middle_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_middle.cer' ;
self::$root_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_root.cer' ;
self::$config['orderDesc'] = $data['shop_name'] ; // 订单名称
self::$config['orderId'] = $data['numbers'] ; // 订单号
self::$config['txnAmt'] = (int)$data['pay_amount'] * 100 ;// 订单金额
self::$config['txnTime'] = date('YmdHis'); // 交易时间 年月日时分秒
// 获取签名
$sign = self::pay_sign() ;
if($sign['code'] != 200) return $sign ;
self::$pay_environment_url = 'appTransUrl' ; // 重定义 请求接口域名
$result_arr = self::pay_post() ;
if(count($result_arr)<=0) return ['code'=>400,'msg'=>'请求无回应'] ;// 没收到200应答的情况
self::$config = $result_arr ; // 重新赋值
$pay_state = self::pay_validate() ; // 验证应答是否成功
if(!$pay_state) return ['code'=>400,'msg'=>'验签失败'] ;
return ['code'=>200,'msg'=>'成功','tn'=>$result_arr['tn']] ;
}
// 生成签名
public static function pay_sign()
{
if(self::$config['signMethod']=='01') {
return self::signByCertInfo();
} else {
return ['code'=>400,'msg'=>'暂不支持'] ;
}
}
public static function signByCertInfo()
{
// self::$config, self::$pfx_url, self::$pfx_pwd
//证书ID
$cert = self::get_cert_id() ;
if($cert['code'] != 200) return $cert ;
self::$config['certId'] = $cert['cert_id'];
$private_key = $cert['ksy'];
$params_str = self::createLinkString (true, false ); // 转换成key=val&串
$params_sha256x16 = hash( 'sha256',$params_str);//sha256签名摘要
$result = openssl_sign ( $params_sha256x16, $signature, $private_key, 'sha256'); // 签名
if (!$result) return ['code'=>400,'msg'=>'>>>>>签名失败<<<<<<<']; ;
self::$config ['signature'] = base64_encode ( $signature );
return ['code'=>200,'msg'=>'成功'] ;
}
// 获取证书信息
private static function get_cert_id()
{
$pkcs12certdata = file_get_contents ( self::$pfx_url );
if($pkcs12certdata === false ) return ['code'=>400,'msg'=>'file_get_contents fail。'];
if(openssl_pkcs12_read ( $pkcs12certdata, $certs, self::$pfx_pwd ) == FALSE ) return ['code'=>400,'msg'=>'openssl_pkcs12_read fail。'];
$x509data = $certs ['cert'];
if(!openssl_x509_read ( $x509data )) return ['code'=>400,'msg'=>'openssl_x509_read fail。'];
$certdata = openssl_x509_parse ( $x509data );
$certId = $certdata ['serialNumber'];
$certKey = $certs ['pkey'];
$certCert = $x509data;
return ['code'=>200,'msg'=>'ok','cert_id'=>$certId,'ksy'=>$certKey,'cert'=>$certCert] ;
}
/**
* 讲数组转换为string
* @param $sort // 是否需要排序
* @param $encode // 是否需要URL编码
* @return string
*/
public static function createLinkString($sort, $encode) {
$para = self::$config ;
if ($sort) {
$para = self::argSort ( $para );
}
$arr = [] ;
$i = 0 ;
foreach ($para as $k=>$v){
$arr[$i] = $k.'='.($encode ? urlencode ( $v ): $v) ;
$i++ ;
}
$linkString = implode('&',$arr) ;
return $linkString;
}
/**
* 对数组排序
*
* @param $para // 排序前的数组
* return 排序后的数组
*/
public static function argSort($para) {
ksort ( $para );
reset ( $para );
return $para;
}
// post 请求 支付
public static function pay_post()
{
$opts = self::createLinkString ( false, true );
$ch = curl_init ();
curl_setopt ( $ch, CURLOPT_URL, self::$url[self::$pay_environment][self::$pay_environment_url] );
curl_setopt ( $ch, CURLOPT_POST, 1 );
curl_setopt ( $ch, CURLOPT_SSL_VERIFYPEER, false ); // 不验证证书
curl_setopt ( $ch, CURLOPT_SSL_VERIFYHOST, false ); // 不验证HOST
curl_setopt ( $ch, CURLOPT_SSLVERSION, 1 ); // http://php.net/manual/en/function.curl-setopt.php页面搜CURL_SSLVERSION_TLSv1
curl_setopt ( $ch, CURLOPT_HTTPHEADER, array (
'Content-type:application/x-www-form-urlencoded;charset=UTF-8'
) );
curl_setopt ( $ch, CURLOPT_POSTFIELDS, $opts );
curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
$html = curl_exec ( $ch );
if(curl_errno($ch)){
// $errmsg = curl_error($ch);
curl_close ( $ch );
return null;
}
if( curl_getinfo($ch, CURLINFO_HTTP_CODE) != "200"){
// $errmsg = "http状态=" . curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close ( $ch );
return null;
}
curl_close ( $ch );
$result_arr = self::parseQString ( $html );
return $result_arr;
}
/**
* 验签
* @param $params // 应答数组 是否成功
* @return
*/
public static function pay_validate() {
$params = self::$config ;
$signature_str = $params ['signature'];
unset ( $params ['signature'] );
self::$config = $params ; // 重新定义数据
$params_str = self::createLinkString (true, false );
$strCert = $params['signPubKeyCert'];
$strCert = self::verifyAndGetVerifyCert($strCert);
if($strCert['code'] != 200) return $strCert ;
$strCert = $strCert['msg'] ; // 证书内容
$params_sha256x16 = hash('sha256', $params_str); // fcadf418d2c422912e5a823b60fb16af326e890f75f76ccdb5a80a465bbc2710
$signature = base64_decode ( $signature_str );
$isSuccess = openssl_verify ( $params_sha256x16, $signature,$strCert, "sha256" );
return $isSuccess;
}
public static function verifyAndGetVerifyCert($certBase64String){
if (array_key_exists($certBase64String, self::$verifyCerts510)){
return ['code'=>200,'msg'=>self::$verifyCerts510[$certBase64String] ] ;
}
if (self::$middle_url === null || self::$root_url === null){
return ['code'=>400,'msg'=>'rootCertPath or middleCertPath is none, exit initRootCert'] ;
}
openssl_x509_read($certBase64String);
$certInfo = openssl_x509_parse($certBase64String);
$cn = self::getIdentitiesFromCertficate($certInfo); // 中国银联股份有限公司
if(strtolower(self::$ifValidateCNName) == "true"){
if (COMPANY != $cn) return ['code'=>400,'msg'=>"cer owner is not CUP:" . $cn] ;
if (COMPANY != $cn && "00040000:SIGN" != $cn) return ['code'=>400,'msg'=>"cer owner is not CUP:" . $cn] ;
}
$from = date_create ( '@' . $certInfo ['validFrom_time_t'] );
$to = date_create ( '@' . $certInfo ['validTo_time_t'] );
$now = date_create ( date ( 'Ymd' ) );
$interval1 = $from->diff ( $now );
$interval2 = $now->diff ( $to );
if ($interval1->invert || $interval2->invert) return ['code'=>400,'msg'=>"signPubKeyCert has expired"] ;
$result = openssl_x509_checkpurpose($certBase64String, X509_PURPOSE_ANY, [self::$root_url, self::$middle_url]);
if($result === FALSE) return ['code'=>400,'msg'=>"validate signPubKeyCert by rootCert failed"] ;
if($result === TRUE){
self::$verifyCerts510[$certBase64String] = $certBase64String;
return ['code'=>200,'msg'=>self::$verifyCerts510[$certBase64String] ] ;
} else {
return ['code'=>400,'msg'=>"validate signPubKeyCert by rootCert failed with error"] ;
}
}
public static function getIdentitiesFromCertficate($certInfo){
$cn = $certInfo['subject'];
$cn = $cn['CN'];
$company = explode('@',$cn);
if(count($company) < 3) {
return null;
}
return $company[2];
/**
0 => "041"
1 => "8310000000083040"
2 => "中国银联股份有限公司"
3 => "00016495"
*/
}
/**
* key1=value1&key2=value2转array
* @param $str // key1=value1&key2=value2的字符串
* @param $$needUrlDecode 是否需要解url编码,默认不需要
*/
public static function parseQString($str, $needUrlDecode=false){
$result = array();
$len = strlen($str);
$temp = "";
$curChar = "";
$key = "";
$isKey = true;
$isOpen = false;
$openName = "\0";
for($i=0; $i<$len; $i++){
$curChar = $str[$i];
if($isOpen){
if( $curChar == $openName){
$isOpen = false;
}
$temp .= $curChar;
} elseif ($curChar == "{"){
$isOpen = true;
$openName = "}";
$temp .= $curChar;
} elseif ($curChar == "["){
$isOpen = true;
$openName = "]";
$temp .= $curChar;
} elseif ($isKey && $curChar == "="){
$key = $temp;
$temp = "";
$isKey = false;
} elseif ( $curChar == "&" && !$isOpen){
self::putKeyValueToDictionary($temp, $isKey, $key, $result, $needUrlDecode);
$temp = "";
$isKey = true;
} else {
$temp .= $curChar;
}
}
self::putKeyValueToDictionary($temp, $isKey, $key, $result, $needUrlDecode);
return $result;
}
public static function putKeyValueToDictionary($temp, $isKey, $key, &$result, $needUrlDecode) {
if ($isKey) {
$key = $temp;
if (strlen ( $key ) == 0) {
return false;
}
$result [$key] = "";
} else {
if (strlen ( $key ) == 0) {
return false;
}
if ($needUrlDecode)
$result [$key] = urldecode ( $temp );
else
$result [$key] = $temp;
}
}
// 交易状态查询 返回的数据 和异步自己返回的数据差不多 除了业务类型不同都一样
public static function union_notify_get_state($data)
{
self::$config = [
//以下信息非特殊情况不需要改动
'version' => self::$config_version, //版本号
'encoding' => self::$config_encoding, //编码方式
'bizType' => '000802', //业务类型
'txnTime' => $data["txnTime"], //请修改被查询的交易的订单发送时间,格式为YYYYMMDDhhmmss,此处默认取demo演示页面传递的参数
'txnType' => '00', //交易类型
'txnSubType' => '00', //交易子类
'accessType' => '0', //接入类型
// 'signature'=>'' , // 签名后续文件补充
'signMethod' => self::$config_signMethod, //签名方法
'merId' => self::$config_merId, //商户代码,请改自己的测试商户号,此处默认取demo演示页面传递的参数
'orderId' => $data["orderId"], //请修改被查询的交易的订单号,8-32位数字字母,不能含“-”或“_”,此处默认取demo演示页面传递的参数
// 'channelType' => '07', //渠道类型
//TODO 以下信息需要填写
];
// 证书存放路径
self::$pfx_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_sign.pfx' ;
self::$enc_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_enc.cer' ;
self::$middle_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_middle.cer' ;
self::$root_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_root.cer' ;
$sign = self::pay_sign() ;
if($sign['code'] != 200) return $sign ;
self::$pay_environment_url = 'singleQueryUrl' ; // 重定义 请求接口域名
$result_arr = self::pay_post() ;
if(count($result_arr)<=0) return ['code'=>400,'msg'=>'请求无回应'] ;// 没收到200应答的情况
self::$config = $result_arr ; // 重新赋值
$pay_state = self::pay_validate() ; // 验证应答是否成功
if(!$pay_state) return ['code'=>400,'msg'=>'验签失败'] ;
return ['code'=>200,'msg'=>'成功','tn'=>$result_arr] ;
}
}
记录使用
acp_test_sign.pfx acp_test_enc.cer acp_test_middle.cer acp_test_root.cer
四个测试文件 登录后获取