评论

微信API加密,排坑整理,PHP版本

php对接微信api加密

微信推出了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

最后一次编辑于  2023-07-24  
点赞 6
收藏
评论

8 个评论

  • 林
    2023-10-11

    已解决 感谢

    2023-10-11
    赞同
    回复
  • 张。。
    张。。
    2023-10-09

    感谢!终于有完整的php版本了

    2023-10-09
    赞同
    回复
  • sixth
    sixth
    2023-09-23

    楼主代码可以直接使用,注意公私钥格式就行,格式不对就会报错 

    2023-09-23
    赞同
    回复
  • 刘洋
    刘洋
    2023-08-20

    非常有用,但是我也验签失败找不出原因,请有能力的大佬帮忙解决验签失败的问题

    2023-08-20
    赞同
    回复 2
    • 刘洋
      刘洋
      2023-08-24
      还有解密经常出现false,搞老1周了也没找出问题
      2023-08-24
      回复
    • sixth
      sixth
      2023-09-23
      验签失败看下参数格式,参数格式不对会报错
      2023-09-23
      回复
  • 😀
    😀
    2023-08-07

    注意以下几个问题;

    1.手动创建订单成功后,微信方会回调你的接口,此前认为这是同步操作,应该不会回调我的接口。因为未对此状态进行处理,出现了数据“被篡改”问题。

    2.商家主动取消订单时,可能会出现返回错误码,但是取消成功的情况,为了保证数据库能更新成功,特此在取消订单后,再次进行订单状态查询,发现查询状态不是最新的,卒!(官方优化或代码中加sleep)

    2023-08-07
    赞同
    回复 1
    • 刘洋
      刘洋
      2023-08-22
      请问解密有时候能成功返回json字符串,有时候失败直接false,知道原因吗?
      2023-08-22
      回复
  • 金钱豹
    金钱豹
    2023-07-30

    感谢贴主提供的代码,接口已经通了,另外我也贡献一下验证签名的代码,验证签名的地方改一下

    // 此处改一下,从证书中提取公钥
    $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;
     }
    
    2023-07-30
    赞同
    回复 14
    • 金钱豹
      金钱豹
      2023-07-30
      另外,可以将提取出的公钥保存一份,不用每次都提取。
      2023-07-30
      回复
    • I'm Henry
      I'm Henry
      2023-08-04
      所以按照你这个方式修改就可以修正验签的错误对吧?
      2023-08-04
      回复
    • I'm Henry
      I'm Henry
      2023-08-04回复金钱豹
      强!
      2023-08-04
      回复
    • 😀
      😀
      2023-08-07
      感谢~
      2023-08-07
      回复
    • 刘洋
      刘洋
      2023-08-22
      请问解密有时候能成功返回json字符串,有时候失败直接false,知道原因吗?
      2023-08-22
      回复
    查看更多(9)
  • I'm Henry
    I'm Henry
    2023-07-25

    强!

    2023-07-25
    赞同
    回复
  • 😀
    😀
    2023-07-25

    很多人失败的原因都是因为加密的方式存在问题,需要引入一个RSA类

    2023-07-25
    赞同
    回复 5
    • 刘洋
      刘洋
      2023-08-22
      请问解密有时候能成功返回json字符串,有时候失败直接false,知道原因吗?
      2023-08-22
      回复
    • 可乐
      可乐
      2023-09-23
      麻烦问下老哥 引用的是哪个RSA类 有composer吗
      2023-09-23
      回复
    • sixth
      sixth
      2023-09-25回复可乐
      用这个phpseclib
      2023-09-25
      回复
    • 可乐
      可乐
      2023-09-25回复sixth
      好的 感谢
      2023-09-25
      回复
    • 樂天Pad掌门人
      樂天Pad掌门人
      2023-10-30回复sixth
      composer require phpseclib/phpseclib,是这个吗,用的哪个版本的,3.0、2.0还是1.0?
      2023-10-30
      回复
登录 后发表内容