微信小程序客服消息AES解密问题

微信官方的demo下载地址:https://res.wx.qq.com/op_res/-serEQ6xSDVIjfoOHcX78T1JAYX-pM_fghzfiNYoD8uHVd3fOeC0PC_pvlg4-kmP

DEMO里面有php版本的加解密示例代码, 我们把文件名为 pkcs7Encoder.php 里面的解密代码列出来看看:

	/**
	 * 对密文进行解密
	 * @param string $encrypted 需要解密的密文
	 * @return string 解密得到的明文
	 */
	public function decrypt($encrypted, $appid)
	{

		try {
			//使用BASE64对需要解密的字符串进行解码
			$ciphertext_dec = base64_decode($encrypted);
			$module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
			$iv = substr($this->key, 0, 16);
			mcrypt_generic_init($module, $this->key, $iv);

			//解密
			$decrypted = mdecrypt_generic($module, $ciphertext_dec);
			mcrypt_generic_deinit($module);
			mcrypt_module_close($module);
		} catch (Exception $e) {
			return array(ErrorCode::$DecryptAESError, null);
		}


		try {
			//去除补位字符
			$pkc_encoder = new PKCS7Encoder;
			$result = $pkc_encoder->decode($decrypted);
			//去除16位随机字符串,网络字节序和AppId
			if (strlen($result) < 16)
				return "";
			$content = substr($result, 16, strlen($result));
			$len_list = unpack("N", substr($content, 0, 4));
			$xml_len = $len_list[1];
			$xml_content = substr($content, 4, $xml_len);
			$from_appid = substr($content, $xml_len + 4);
		} catch (Exception $e) {
			//print $e;
			return array(ErrorCode::$IllegalBuffer, null);
		}
		if ($from_appid != $appid)
			return array(ErrorCode::$ValidateAppidError, null);
		return array(0, $xml_content);

	}

可以看出,解密代码使用了 mcrypt_module_open() 、 mcrypt_generic_init() 、 mdecrypt_generic() 这些mcrypt扩展函数,PHP7.1以后的版本已不再支持,要用openssl扩展中的 openssl_decrypt() 代替,可修改如下:

	/**
	 * 对密文进行解密
	 * @param string $encrypted 需要解密的密文
	 * @return string 解密得到的明文
	 */
	public function decrypt($encrypted, $appid)
	{

		try {
			//使用BASE64对需要解密的字符串进行解码
			$iv = substr($this->key, 0, 16);
            $decrypted = openssl_decrypt(base64_decode($encrypted), 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv);
        } catch (\Exception $e) {
			return array(ErrorCode::$DecryptAESError, null);
		}

		try {
			//去除补位字符
			$pkc_encoder = new PKCS7Encoder;
			$result = $pkc_encoder->decode($decrypted);
			//去除16位随机字符串,网络字节序和AppId
			if (strlen($result) < 16)
				return "";
			$content = substr($result, 16, strlen($result));
			$len_list = unpack("N", substr($content, 0, 4));
			$xml_len = $len_list[1];
			$xml_content = substr($content, 4, $xml_len);
			$from_appid = substr($content, $xml_len + 4);
		} catch (\Exception $e) {
			return array(ErrorCode::$IllegalBuffer, null);
		}
		if ($from_appid != $appid)
			return array(ErrorCode::$ValidateAppidError, null);
		return array(0, $xml_content);
	}

值得注意的是 openssl_decrypt() 的第四个参数使用了“OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING”,网上的其他教程大多使用“OPENSSL_RAW_DATA”参数,实测发现,使用“OPENSSL_RAW_DATA”参数会导致无法解密小程序卡片类型(即msg_type=miniprogrampage)的客服消息。

php将图片文件转成base64编码

/**
 * 获取图片的Base64编码(不支持url)
 * @date 2020-03-28
 * @param $img_file 传入本地图片地址
 * @param $type 是否带前缀, 1:带, 0:不带
 * @return array 0编码结果  1图片类型
 */
function imgToBase64($img_file, $type=1) {
    $img_base64 = '';
    if (file_exists($img_file)) {
        $app_img_file = $img_file; // 图片路径
        $img_info = getimagesize($app_img_file); // 取得图片的大小,类型等
        $fp = fopen($app_img_file, "r"); // 图片是否可读权限
        if ($fp) {
            $filesize = filesize($app_img_file);
            $content = fread($fp, $filesize);
            $file_content = chunk_split(base64_encode($content)); // base64编码
            switch ($img_info[2]) {           //判读图片类型
                case 1: $img_type = "gif";
                    break;
                case 2: $img_type = "jpg";
                    break;
                case 3: $img_type = "png";
                    break;
            }
			//合成图片的base64编码
            if($type){
				$img_base64 = 'data:image/' . $img_type . ';base64,' . $file_content;
			}else{
				$img_base64 = $file_content;
			}
        }
        fclose($fp);
    }
    return array($img_base64, $img_type);  //返回图片的base64及图片类型
}
//用法
$result = imgToBase64('/www/html/ken.01h.net/xxx.png');
//解码
$base64_string = explode(',', $result[0]); //截取data:image/png;base64, 这个逗号后的字符
$data = base64_decode($base64_string[1]);  //对截取后的字符使用base64_decode进行解码
file_put_contents('/www/html/ken.01h.net/yyy.'.$result[1], $data); //写入文件并保存

PHP使用对称加密算法(3DES)加密及解密示例

<?php
/**
 *    DESede(3DES)操作类库
 *    @author    Ken(QQ:2480990710)
 *    @blog		 http://ken.01h.net/
 *    @date      2020-3-25
 */
class DESede
{
    private $key = "";
    private $iv = "";

    /**
     * 构造
     * @param string $key
     * @param string $iv
     */
    function __construct($key, $iv)
    {
        $this->key = $key;
        $this->iv = $iv;
    }

    /**
     *加密
     * @param <type> $value
     * @return <type>
     */
    public function encrypt($value)
    {
        $td = mcrypt_module_open(MCRYPT_3DES, '', MCRYPT_MODE_ECB, '');  //ecb模式
        $value = $this->PaddingPKCS7($value);
        @mcrypt_generic_init($td, $this->key, $this->iv);
        $ret = mcrypt_generic($td, $value);
        mcrypt_generic_deinit($td);
        mcrypt_module_close($td);
        return $ret;
    }

    /**
     *解密
     * @param <type> $value
     * @return <type>
     */
    public function decrypt($value)
    {
        $td = mcrypt_module_open(MCRYPT_3DES, '', MCRYPT_MODE_ECB, '');  //ecb模式
        @mcrypt_generic_init($td, $this->key, $this->iv);
        $ret = trim(mdecrypt_generic($td, $value));
        $ret = $this->UnPaddingPKCS7($ret);
        mcrypt_generic_deinit($td);
        mcrypt_module_close($td);
        return $ret;
    }

    private function PaddingPKCS7($data)
    {
        $block_size = mcrypt_get_block_size('tripledes', 'ecb');  //ecb模式
        $padding_char = $block_size - (strlen($data) % $block_size);
        $data .= str_repeat(chr($padding_char), $padding_char);
        return $data;
    }

    private function UnPaddingPKCS7($text)
    {
        $pad = ord($text{strlen($text) - 1});
        if ($pad > strlen($text)) {
            return false;
        }
        if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
            return false;
        }
        return substr($text, 0, -1 * $pad);
    }
}

//用法
$key = '0123456789ABCDEFFEDCBA98765432100123456789ABCDEF';
$iv = '';
$msg = '6210985840034852662';
$key = hex2bin($key);  //上面的key是16进制的,所以这里转为二进制。
$des = new DESede($key, $iv);
$result1 = $des->encrypt($msg);
echo '加密结果(base64编码): ' . base64_encode($result1) . '<br />';
echo '加密结果(16进制): ' . bin2hex($result1) . '<br />';
$result2 = $des->decrypt($result1);
echo '解密结果: ' . $result2;

?>

关于WEB资金系统转账的安全保护,防止极快的重复并发请求绕过系统余额不足的判断

发现很多项目都有一个漏洞:比如账户A有100元,账户B有0元,然后从账户A转100元给账户B,正常的代码流程是,先检测账户A余额是否足够,然后从账户A中扣除100元,再给账户B增加100元,结果变成账户A剩下0元,账户B剩下100元,转账操作成功,逻辑上并没有问题,但是,如果该转账请求瞬间执行30次,结果发现其中大约有4、5次都是成功的,其余的请求则会报余额不足,原因是那4、5次成功的请求中余额不足的判断是同一时间执行的,所以都可以通过,假如有5次都通过了,结果很有可能就变成,账户A扣除5×100元(余额-400元),账户B增加了5×100元(余额500元),这样问题就大了,我们期望的结果是,无论瞬间请求多少次都要保证只有一次可以通过余额不足的检测,那么解决的办法就是让这段代码队列执行。

在PHP中让代码队列执行最常用的方法就是非阻塞的文件排他锁,示例如下:

//非阻塞的文件排他锁
$fp = fopen("/lock.txt", "w+");
if(!flock($fp, LOCK_EX | LOCK_NB)){
	echo '系统繁忙';
	exit();
}
//读取用户余额,判断是否足够
//......
if(){  //余额足够
	//执行转账逻辑
	//......
	flock($fp, LOCK_UN);  //释放锁
	echo '转账成功';
}else{
	echo '余额不足';
}
fclose($fp);

银联条码支付小微商户进件PHP版DEMO

本示例依赖于银联条码支付【统一】扫码支付PHP版DEMO,此DEMO可在银联官网下载(https://up.95516.com/open/openapi/doc?index_1=1&index_2=1&chapter_1=238&chapter_2=268),新增内容已在代码中标注说明。

<?php
/**
 * 支付接口调测例子
 * ================================================================
 * index 进入口,方法中转
 * submitOrderInfo 提交订单信息
 * queryOrder 查询订单
 * 
 * ================================================================
 */
require('Utils.class.php');
require('config/config.php');
require('class/RequestHandler.class.php');
require('class/ClientResponseHandler.class.php');
require('class/PayHttpClient.class.php');

Class Request{
  

    private $resHandler = null;
    private $reqHandler = null;
    private $pay = null;
    private $cfg = null;
    
    public function __construct(){
        $this->Request();
    }

    public function Request(){
        $this->resHandler = new ClientResponseHandler();
        $this->reqHandler = new RequestHandler();
        $this->pay = new PayHttpClient();
        $this->cfg = new Config();

        $this->reqHandler->setGateUrl($this->cfg->C('url'));

        $sign_type = $this->cfg->C('sign_type');
        
        if ($sign_type == 'MD5') {
            $this->reqHandler->setKey($this->cfg->C('key'));
            $this->resHandler->setKey($this->cfg->C('key'));
            $this->reqHandler->setSignType($sign_type);
        } else if ($sign_type == 'RSA_1_1' || $sign_type == 'RSA_1_256') {
            $this->reqHandler->setRSAKey($this->cfg->C('private_rsa_key'));
            $this->resHandler->setRSAKey($this->cfg->C('public_rsa_key'));
            $this->reqHandler->setSignType($sign_type);
        }
    }
	
	/* ------------------------------------------------- 新增内容 start ------------------------------------------------- */
	
    /**
     * 图片上传 2020-3-14 By Ken (http://ken.01h.net/) QQ:2480990710
     */
	public function picUpload($path){
		$post_data['partner'] = $this->cfg->C('sign_agentno');//合作伙伴ID即机构号
		$post_data['serviceName'] = 'pic_upload';//服务名称
		$post_data['signType'] = $this->cfg->C('sign_type');//签名类型
		$post_data['data'] = '';//请求数据
		//签名
		$signPars = "";
		ksort($post_data);
		foreach($post_data as $k => $v) {
			if("" != $v &amp;&amp; "dataSign" != $k &amp;&amp; "signType" != $k) {
				$signPars .= $k . "=" . $v . "&amp;";
			}
		}
		$signPars = substr($signPars, 0, -1) . $this->reqHandler->getKey();
		$post_data['dataSign'] = strtolower(md5($signPars));
		//图片
		if(substr(PHP_VERSION, 0, 3)<5.5){  //PHP5.5以下版本
			$post_data['picFile'] = '@'.$path;
		}else{  //PHP5.5及以上版本
			$post_data['picFile'] = new CURLFile($path);
		}
		$this->pay->setReqContent($this->cfg->C('up_url'), $post_data);
		if($this->pay->call()){
			$res = json_decode(json_encode(simplexml_load_string($this->pay->getResContent(), 'SimpleXMLElement', LIBXML_NOCDATA)), true);  //将XML转为array
			if($res['isSuccess'] == 'T'){  //成功
				return $res['pic'];
			}else{
				return array('status'=>500,'msg'=>'Error Code:'.$res['errorCode'].' Error Message:'.$res['errorMsg']);
			}
		}else{
			return array('status'=>500,'msg'=>'Response Code:'.$this->pay->getResponseCode().' Error Info:'.$this->pay->getErrInfo());
		}
	}
	
    /**
     * 小微商户进件 2020-3-14 By Ken (http://ken.01h.net/) QQ:2480990710
     */
	public function smallMchAdd($merchant){  //merchantName 、 outMerchantId  、 chPayAuth 、 limitCreditPay  、 merchantDetail['merchantShortName'] \ ['industrId'] \ ['province'] \ ['city'] \ ['county'] \ ['address'] \ ['email'] \ ['customerPhone'] \ ['principal'] \ ['principalMobile'] \ ['idCode'] \ ['indentityPhoto'] 、 bankAccount['accountCode'] \ ['bankId'] \ ['accountName'] \ ['accountType'] \ ['contactLine'] \ ['bankName'] \ ['province'] \ ['city'] \ ['idCardType'] \ ['idCard'] \ ['tel'] 、 mchPayConfs[0]['nodename'] \ ['payTypeId'] \ ['billRate'] 、 mchPayConfs[1]['nodename'] \ ['payTypeId'] \ ['billRate'] 、 mchPayConfs[2]['nodename'] \ ['payTypeId'] \ ['billRate'] \ ['billRate1'] ……
		$post_data['partner'] = $this->cfg->C('sign_agentno');//合作伙伴ID即机构号
		$post_data['serviceName'] = 'small_mch_add';//服务名称
		$post_data['signType'] = $this->cfg->C('sign_type');//签名类型
		//数组转XML
		$merchant_xml = simplexml_load_string('<merchant />');
		$this->create_xml($merchant, $merchant_xml);
		$data = $merchant_xml->saveXML();
		$post_data['data'] = str_replace('<?xml version="1.0"?>', '', $data);//请求数据
		//签名
		$signPars = "";
		ksort($post_data);
		foreach($post_data as $k => $v) {
			if("" != $v &amp;&amp; "dataSign" != $k &amp;&amp; "signType" != $k) {
				$signPars .= $k . "=" . $v . "&amp;";
			}
		}
		$signPars = substr($signPars, 0, -1) . $this->reqHandler->getKey();
		$post_data['dataSign'] = strtolower(md5($signPars));
		//组装请求串
        $o = "";
        foreach ($post_data as $k => $v) {
            $o.= "$k=" . urlencode($v) . "&amp;";
        }
        $post_data = substr($o, 0, -1);
		$this->pay->setReqContent($this->cfg->C('up_url'), $post_data);
		if($this->pay->call()){
			$res = json_decode(json_encode(simplexml_load_string($this->pay->getResContent(), 'SimpleXMLElement', LIBXML_NOCDATA)), true);  //将XML转为array
			if($res['isSuccess'] == 'T'){  //成功
				return $res['merchant'];
			}else{
				return array('status'=>500,'msg'=>'Error Code:'.$res['errorCode'].' Error Message:'.$res['errorMsg']);
			}
		}else{
			return array('status'=>500,'msg'=>'Response Code:'.$this->pay->getResponseCode().' Error Info:'.$this->pay->getErrInfo());
		}
	}
	private function create_xml($arr, $xml) {
		foreach($arr as $k=>$v) {
			if(is_array($v)) {  //数组
				if(array_keys($v) !== range(0, count($v)-1)) {  //关联数组
					$x = $xml->addChild($k);
					$this->create_xml($v, $x);
				} else {  //索引数组
					$x = $xml->addChild($k);
					foreach ($v as $vv) {
						$y = $x->addChild($vv['nodename']);
						unset($vv['nodename']);
						$this->create_xml($vv, $y);
					}
				}
			} else {
				$xml->addChild($k, $v);
			}
		}
	}
	
	/* ------------------------------------------------- 新增内容 end ------------------------------------------------- */
	
    /**
     * 提交订单
     */
    public function submitOrderInfo($info){
		
	}

?>

调用示例如下:

<?php
//调取接口
include './request.php';
$req = new Request();
//上传身份证图片
$indentityPhoto = array();
$card_front = $req->picUpload(dirname(__FILE__).'/upload/card_front.png');
$indentityPhoto[] = $card_front;
$card_back = $req->picUpload(dirname(__FILE__).'/upload/card_back.png');
$indentityPhoto[] = $card_back;
//上传收款银行卡照片
$accountCodePhoto = $req->picUpload(dirname(__FILE__).'/upload/bankPhoto.png');
//上传门头照
$mainPhoto = $req->picUpload(dirname(__FILE__).'/upload/shopPhoto.png');
//商户信息
$info = array();
//商户基本信息
$info['merchantName'] = '商户_张三';  //商户名称
$info['outMerchantId'] = 'UP_' . date('YmdHis') . rand(10, 99);  //外商户号
$info['chPayAuth'] = '1';  //授权机构交易
$info['limitCreditPay'] = '2';  //是否可用信用卡支付, 0:是   1:否   2:不限制
//商户详情信息
$info['merchantDetail']['merchantShortName'] = $info['merchantName'];  //商户简称
$info['merchantDetail']['industrId'] = '5331';  //行业类别: 5331 杂货便利店
$info['merchantDetail']['province'] = '3900';  //省份: 3900 福建
$info['merchantDetail']['city'] = '3930';  //城市: 3930 厦门
$info['merchantDetail']['county'] = '3933';  //区县: 3933 湖里
$info['merchantDetail']['address'] = '高新软件园华骏国际大厦七楼702单元';  //地址
$info['merchantDetail']['email'] = '2480990710@qq.com';  //邮箱
$info['merchantDetail']['customerPhone'] = '13600010002';  //客服电话
$info['merchantDetail']['principal'] = '张三';  //负责人
$info['merchantDetail']['principalMobile'] = '13600010002';  //负责人手机号
$info['merchantDetail']['idCode'] = '350604198705281012';  //证件号码
$info['merchantDetail']['indentityPhoto'] = implode(';', $indentityPhoto);  //身份证图片,多张图片以分号;隔开
$info['merchantDetail']['accountCodePhoto'] = $accountCodePhoto;  //收款银行卡照片
$info['merchantDetail']['mainPhoto'] = $mainPhoto;  //门头照
//银行账户信息
$info['bankAccount']['accountCode'] = '6222021402018746261';  //银行卡号
$info['bankAccount']['bankId'] = '1';  //开户银行名编号
$info['bankAccount']['accountName'] = '张三';  //开户人
$info['bankAccount']['accountType'] = '1';  //帐户类型  1:个人
$info['bankAccount']['contactLine'] = '102100099996';  //联行号
$info['bankAccount']['bankName'] = '中国工商银行厦门支行';  //开户支行名称
$info['bankAccount']['province'] = '3900';  //开户支行所在省
$info['bankAccount']['city'] = '3930';  //开户支行所在市
$info['bankAccount']['idCardType'] = '1';  //持卡人证件类型  1:身份证
$info['bankAccount']['idCard'] = '350604198705281012';  //持卡人证件号码
$info['bankAccount']['tel'] = '13600010002';  //银行账户手机号
//商户支付类型信息
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10014751', 'billRate'=>'3.8');  //支付宝
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10014761', 'billRate'=>'3.8');
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10014771', 'billRate'=>'3.8');
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10002016', 'billRate'=>'6', 'billRate1'=>'6', 'billRate2'=>'6', 'billRate3'=>'6');  //云闪付
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10000973', 'billRate'=>'6', 'billRate1'=>'6', 'billRate2'=>'6', 'billRate3'=>'6');
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10000974', 'billRate'=>'6', 'billRate1'=>'6', 'billRate2'=>'6', 'billRate3'=>'6');
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10000959', 'billRate'=>'3.8');  //微信
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10000960', 'billRate'=>'3.8');
$info['mchPayConfs'][] = array('nodename'=>'mchPayConf', 'payTypeId'=>'10000961', 'billRate'=>'3.8');
//发送请求
$result = $req->smallMchAdd($info);
if($result['merchantId']){  //进件成功
	//
}else{
	//
}
?>