小程序API加密 PHP版本

小程序API加密 PHP版本

在小程序管理后台开启api加密后,开发者需要对原API的请求内容加密与签名,同时API的回包内容需要开发者验签与解密。
https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/getting_started/api_signature.html

第一步:生成获取所需密钥

  • 登录小程序后台

  • 在左侧菜单点击 开发 > 开发管理 进入开发设置 TAB,在下面找到 API安全一栏,扫码进入生成操作。

  • 对称与非对称加密都点击随机生成,并且在生成后点击校验,并且下载相应的私钥。此处分别可以获取到 AES256_SNAES256_KEYRSA_SNRSA_PUBLIC_KEYRSA_PRIVATE_KEY。(24小时只能修改一次,记得在第一次就保存好 RSA_PRIVATE_KEY再进入的话保存的是公钥)。

  • 点击保存,在此时这一栏可以获取到 CERT_SNCERT_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);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值