小程序API加密 PHP版本
在小程序管理后台开启api加密后,开发者需要对原API的请求内容加密与签名,同时API的回包内容需要开发者验签与解密。
https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/getting_started/api_signature.html
第一步:生成获取所需密钥
-
登录小程序后台
-
在左侧菜单点击 开发 > 开发管理 进入开发设置 TAB,在下面找到 API安全一栏,扫码进入生成操作。
-
对称与非对称加密都点击随机生成,并且在生成后点击校验,并且下载相应的私钥。此处分别可以获取到
AES256_SN
、AES256_KEY
、RSA_SN
、RSA_PUBLIC_KEY
、RSA_PRIVATE_KEY
。(24小时只能修改一次,记得在第一次就保存好RSA_PRIVATE_KEY
再进入的话保存的是公钥)。 -
点击保存,在此时这一栏可以获取到
CERT_SN
和CERT_KEY
。
如此我们就获取到了所有需要的密钥了。
第二步:写入相应的加密类
这里放上我所使用的代码,整理于微信社区:
<?php
/**
* 封装微信api签名安全类
*/
namespace app\namespace;
use ErrorException;
// composer require phpseclib/phpseclib=2.0.3
use phpseclib\Crypt\RSA;
class ApiAuth
{
private string $appId;
private ?string $accessToken;
private array $aes;
private array $rsa;
private array $cert;
private string $url;
public function __construct(
$appid,
$accessToken,
$AES256_SN,
$AES256_KEY,
$RSA_PRIVATE_SN,
$RSA_PUBLIC_KEY,
$RSA_PRIVATE_KEY,
$CERT_SN,
$CERT_KEY
)
{
$this->appId = $appid;
$this->accessToken = $accessToken;
$this->aes['sn'] = $AES256_SN;
$this->aes['key'] = $AES256_KEY;
$this->rsa['sn'] = $RSA_PRIVATE_SN;
$this->rsa['rsa-public-key'] = $RSA_PUBLIC_KEY;
$this->rsa['rsa-private-key'] = $RSA_PRIVATE_KEY;
$this->cert['sn'] = $CERT_SN;
$this->cert['cert-key'] = $CERT_KEY;
}
/**
* Name:对外方法用于所有微信api的请求方法
* User: zcw
* Date: 2023/7/14
* Time: 9:51
* @param $url
* @param $req
* @return mixed|null
* @throws \ErrorException
* @throws \Exception
*/
public function request($url, $req)
{
$accessToken = $this->accessToken;
$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 = json_decode($data['body'], true);
//请求平台报错
if (isset($body['errcode'])) {
throw new ErrorException($body['errmsg']);
}
// 3.响应参数验签 目前存在问题
$verify = $this->verifyResponse($data);
//4.参数解密
return $this->jM($headers['Wechatmp-TimeStamp'], $body);
}
/**
* 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): array
{
$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): array
{
$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
* @return false|string
*/
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
* @return string
*/
private function getSignature(array $newRe): string
{
$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
* @return bool|string
*/
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);
}
/**
* 从证书中提取公钥
* @param string $cert 证书内容
* @return string
*/
private function getPublicKey(string $cert): string
{
$pkey = openssl_pkey_get_public($cert);
$keyData = openssl_pkey_get_details($pkey);
$public_key = str_replace('-----BEGIN PUBLIC KEY-----', '', $keyData['key']);
return trim(str_replace('-----END PUBLIC KEY-----', '', $public_key));
}
/**
* Name:解析头部信息
* User: zcw
* Date: 2023/7/14
* Time: 10:28
* @param $headerString
* @return array
*/
private function httpParseHeaders($headerString): array
{
$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();
}
return json_decode($result, true);
}
/**
* Name:验证响应值
* User: zcw
* Date: 2023/7/14
* Time: 11:16
* @param $data
* @return bool|string
* @throws \ErrorException
*/
private function verifyResponse($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);
$publicKey = $this->getPublicKey($key);
$rsa = new RSA();
$rsa->loadKey($publicKey);
$rsa->setHash("sha256");
$rsa->setMGFHash("sha256");
return $rsa->verify($payload, $signature);
}
}
调用示例
$contents = (new ApiAuth(
$appid,
$accessToken,
$AES256_SN,
$AES256_KEY,
$RSA_PRIVATE_SN,
$RSA_PUBLIC_KEY,
$RSA_PRIVATE_KEY,
$CERT_SN,
$CERT_KEY
))->request($api, $params);