微信推出了API加密解密的写法,主要应用于一些涉及交易的接口,如同城配送~,在对接同城配送的过程中,我踩了许多的坑,自身原因一部分、文档原因一部分,现在就来讲讲PHP如何一步步的实现解决API加密、解密的写法:
1.首先在 开发->开发管理->AP安全 ,设置几个密钥,并获取平台证书
2.直接上代码。
<?php
/**
* 封装微信api签名安全类
*/
namespace common\components\wxapi;
use common\helpers\MiniHelper;
use phpseclib\Crypt\RSA;
use Yii;
use yii\base\ErrorException;
class WxApi
{
private $appId;
private $aes;
private $rsa;
private $cert;
private $url;
public function __construct()
{
$this->appId = Yii::$app->params['miniWechat']['appId'];
$wechatSafeApi = Yii::$app->params['wechatSafeApi'];
$this->aes['sn'] = $wechatSafeApi['aes-sn'];
$this->aes['key'] = file_get_contents(dirname(Yii::$app->getBasePath()) . $wechatSafeApi['aes-key']);
$this->rsa['sn'] = $wechatSafeApi['rsa-sn'];
$this->rsa['rsa-public-key'] = file_get_contents(dirname(Yii::$app->getBasePath()) . $wechatSafeApi['rsa-public-key']);
$this->rsa['rsa-private-key'] = file_get_contents(dirname(Yii::$app->getBasePath()) . $wechatSafeApi['rsa-private-key']);
$this->cert['sn'] = $wechatSafeApi['cert-sn'];
$this->cert['cert-key'] = file_get_contents(dirname(Yii::$app->getBasePath()) . $wechatSafeApi['cert-key']);
}
/**
* Name:对外方法用于所有微信api的请求方法
* User: zcw
* Date: 2023/7/14
* Time: 9:51
* @param $url
* @param $req
* @throws ErrorException
* @throws \Exception
*/
public function request($url, $req)
{
$accessToken = $this->getAccessToken();
$this->url = $url;
$urls = $url . "?access_token=" . $accessToken;
//1.数据加密
$newRe = $this->getRequestParam($url, $req);
//2.获取签名
$signature = $this->getSignature($newRe);
//本地验签 非必需
$checkLocalSig = $this->checkLocalSignature($newRe, $signature);
if (!$checkLocalSig) {
throw new ErrorException('本地验签错误');
}
$appId = $this->appId;
$headerArray = ['Wechatmp-Appid:' . $appId, 'Wechatmp-TimeStamp:' . $newRe['ts'], 'Wechatmp-Signature:' . $signature];
$data = $this->curlPost($urls, $newRe['reqData'], $headerArray);
$headers = $this->httpParseHeaders($data['header']);
$body = \Qiniu\json_decode($data['body'], true);
//请求平台报错
if (isset($body['errcode'])) {
throw new ErrorException($body['errmsg']);
}
// 3.响应参数验签 目前存在问题
$vertify = $this->vertifyResponse($data);
//4.参数解密
return $this->jM($headers['Wechatmp-TimeStamp'], $body);
}
/**
* Name:获取accessToken
* User: zcw
* Date: 2023/7/14
* Time: 9:15
*/
public function getAccessToken()
{
$qr = new MiniHelper();
return $qr->getAccessToken();
}
/**
* Name:post请求
* User: zcw
* Date: 2023/7/14
* Time: 9:19
* @param $url
* @param $field
* @param $header
* @return array
*/
public function curlPost($url, $field, $header)
{
$headerArray = array("Content-type:application/json;charset=utf-8", "Accept:application/json");
$headerArray = array_merge($headerArray, $header);
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $headerArray);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $field);
//输出响应头部
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, true);
$str = curl_exec($curl);
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
$headers = substr($str, 0, $headerSize);
$body = substr($str, $headerSize);
curl_close($curl);
return ['body' => $body, 'header' => $headers];
}
/**
* Name:对外方法用于所有微信api的请求通道
* User: zcw
* Date: 2023/7/14
* Time: 9:21
* @param $url
* @param $req
* @return array
* @throws \Exception
*/
public function getRequestParam($url, $req)
{
$key = base64_decode($this->aes['key']);
$sn = $this->aes['sn'];
$appId = $this->appId;
$time = time();
//16位随机字符
$nonce = rtrim(base64_encode(random_bytes(16)), '=');
$addReq = ["_n" => $nonce, "_appid" => $appId, "_timestamp" => $time];
$realReq = array_merge($addReq, $req);
$realReq = json_encode($realReq);
//额外参数
$aad = $url . "|" . $appId . "|" . $time . "|" . $sn;
//12位随机字符
$iv = random_bytes(12);
$cipher = openssl_encrypt($realReq, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $tag, $aad);
$iv = base64_encode($iv);
$data = base64_encode($cipher);
$authTag = base64_encode($tag);
$reqData = ["iv" => $iv, "data" => $data, "authtag" => $authTag];
//校验本地加密是否正确 非必须
// $checkParam = $this->checkParam($key, $authTag, $iv, $data, $aad);
return ['ts' => $time, 'reqData' => json_encode($reqData)];
}
/**
* Name:请求前本地验签
* User: zcw
* Date: 2023/7/14
* Time: 9:57
* @param $key
* @param $authTag
* @param $iv
* @param $data
* @param $aad
*/
private function checkParam($key, $authTag, $iv, $data, $aad)
{
$iv = base64_decode($iv);
$data = base64_decode($data);
$authTag = base64_decode($authTag);
return openssl_decrypt($data, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad);
}
/**
* Name:获取签名
* User: zcw
* Date: 2023/7/14
* Time: 10:03
* @param array $newRe
*/
private function getSignature(array $newRe)
{
$time = $newRe['ts'];
$key = $this->rsa['rsa-private-key'];
$url = $this->url;
$appId = $this->appId;
$reqData = $newRe['reqData'];
$payload = "$url\n$appId\n$time\n$reqData";
$rsa = new RSA();
$rsa->loadKey($key);
$rsa->setHash("sha256");
$rsa->setMGFHash("sha256");
$signature = $rsa->sign($payload);
return base64_encode($signature);
}
/**
* Name:请求前本地验签
* User: zcw
* Date: 2023/7/14
* Time: 10:11
* @param array $newRe
* @param string $signature
*/
private function checkLocalSignature(array $newRe, string $signature)
{
$signature = base64_decode($signature);
$rsaPubKey = $this->rsa['rsa-public-key'];
$appId = $this->appId;
$url = $this->url;
$time = $newRe['ts'];
$reqData = $newRe['reqData'];
$payload = "$url\n$appId\n$time\n$reqData";
$payload = utf8_encode($payload);
$rsa = new RSA();
$rsa->loadKey($rsaPubKey);
$rsa->setHash("sha256");
$rsa->setMGFHash("sha256");
return $rsa->verify($payload, $signature);
}
/**
* Name:解析头部信息
* User: zcw
* Date: 2023/7/14
* Time: 10:28
* @param $headerString
* @return array
*/
private function httpParseHeaders($headerString)
{
$headers = [];
$lines = explode("\r\n", $headerString);
foreach ($lines as $line) {
$line = trim($line);
if (!empty($line)) {
$parts = explode(':', $line, 2);
$key = trim($parts[0]);
$value = isset($parts[1]) ? trim($parts[1]) : '';
$headers[$key] = $value;
}
}
return $headers;
}
/**
* Name:解密参数
* User: zcw
* Date: 2023/7/14
* Time: 10:31
* @param $ts
* @param $body
* @return mixed|null
* @throws ErrorException
*/
private function jM($ts, $body)
{
$url = $this->url;
$appId = $this->appId;
$sn = $this->aes['sn'];
$aad = $url . '|' . $appId . '|' . $ts . '|' . $sn;
$key = $this->aes['key'];
$key = base64_decode($key);
$iv = base64_decode($body['iv']);
$data = base64_decode($body['data']);
$authTag = base64_decode($body['authtag']);
$result = openssl_decrypt($data, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad);
if (!$result) {
throw new ErrorException();
}
$result = \Qiniu\json_decode($result,true);
return $result;
}
/**
* Name:验证响应值
* User: zcw
* Date: 2023/7/14
* Time: 11:16
* @param $data
*/
private function vertifyResponse($data)
{
$headers = $this->httpParseHeaders($data['header']);
$nowTime = time();
$reTime = $headers['Wechatmp-TimeStamp'];
$appId = $this->appId;
$cert = $this->cert;
$sn = $cert['sn'];
$key = $cert['cert-key'];
$url = $this->url;
if ($appId != $headers['Wechatmp-Appid'] || $nowTime - $reTime > 300){
throw new \ErrorException('返回值安全字段校验失败');
}
if ($sn == $headers['Wechatmp-Serial']) {
$signature = $headers['Wechatmp-Signature'];
} elseif ($sn == $headers['Wechatmp-Serial-Deprecated']) {
$signature = $headers['Wechatmp-Signature-Deprecated'];
} else {
throw new \ErrorException('返回值sn不匹配');
}
$reData = $data['body'];
$payload = "$url\n$appId\n$reTime\n$reData";
$payload = utf8_encode($payload);
$signature = base64_decode($signature);
$rsa = new RSA();
$rsa->loadKey($key);
$rsa->setHash("sha256");
$rsa->setMGFHash("sha256");
return $rsa->verify($payload, $signature);
}
}
以上是我的代码 ,每个方法都有明确注释,除了返回值使用平台证书未通过以外 ,其余几个环节均正常,大家也可以帮我找找平台证书对返回数据验签失败的原因,有需要也可以加我微信 abc1783475843
已解决 感谢
感谢!终于有完整的php版本了
楼主代码可以直接使用,注意公私钥格式就行,格式不对就会报错
非常有用,但是我也验签失败找不出原因,请有能力的大佬帮忙解决验签失败的问题
注意以下几个问题;
1.手动创建订单成功后,微信方会回调你的接口,此前认为这是同步操作,应该不会回调我的接口。因为未对此状态进行处理,出现了数据“被篡改”问题。
2.商家主动取消订单时,可能会出现返回错误码,但是取消成功的情况,为了保证数据库能更新成功,特此在取消订单后,再次进行订单状态查询,发现查询状态不是最新的,卒!(官方优化或代码中加sleep)
感谢贴主提供的代码,接口已经通了,另外我也贡献一下验证签名的代码,验证签名的地方改一下
// 此处改一下,从证书中提取公钥 $pubkey = $this->getPublicKey($key); $rsa = new RSA(); $rsa->loadKey($pubkey); $rsa->setHash("sha256"); $rsa->setMGFHash("sha256"); return $rsa->verify($payload, $signature);
以下代码从cert中提取公钥匙
/** * 从证书中提取公钥 * @param string $cert 证书内容 * @param string $certPath 证书路径 * @return mixed */ private function getPublicKey($cert, $certPath = '') { $cert = $cert ?: file_get_contents($certPath); $pkey = openssl_pkey_get_public($cert); $keyData = openssl_pkey_get_details($pkey); $public_key = str_replace('-----BEGIN PUBLIC KEY-----', '', $keyData['key']); $public_key = trim(str_replace('-----END PUBLIC KEY-----', '', $public_key)); return $public_key; }
强!
很多人失败的原因都是因为加密的方式存在问题,需要引入一个RSA类