个人案例
- 微信支付V3版本小程序支付 php签名,验签,数据解密代码分享
数据解密需要用到sodium扩展 大部分php版本需要安装 证书序列号可以在这里查看https://myssl.com/cert_decode.html 我用的php7.4版本 直接上代码: <?php //微信原生支付 class Wxpay { private static $appid = 'xxxxxxxxxxxxxx'; //appid private static $mchid = 'xxxxxxxxxxxxxx'; //商户号 private static $mch_apiv3key = 'xxxxxxxxxxxxxx'; //商户平台apiv3密钥(商户平台手动生成) private static $serial_no = 'xxxxxxxxxxxxxx';//商户API证书序列号serial_no private static $wx_serial_no = 'xxxxxxxxxxxxxx';//平台证书序列号 /* * 支付(小程序支付) * @param type $sn 订单编号 * @param type $money 金额 * @param type $openid 用户小程序openid * @return type */ public static function getPayParam($sn, $money, $openid) { $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'; $notify_url = url('/api/weixin/notify'); $data = []; $data['appid'] = self::$appid; $data['mchid'] = self::$mchid; //商户号 $data['description'] = 'xxx'; //描述? $data['out_trade_no'] = $sn; //商户系统内部订单号 $data['time_expire'] = date('Y-m-d') . 'T' . date('H:i:s', (time() + 1800)) . '+08:00'; //订单失效时间2018-06-08T10:34:56+08:00 $data['notify_url'] = $notify_url; //异步通知接口地址 $data['amount'] = ['total' => $money * 100, 'currency' => 'CNY']; //金额 $data['payer'] = ['openid' => $openid]; //用户 $re = self::wxCurl($url, $data, 'POST'); if (!isset($re['prepay_id'])) { exit('参数获取失败'); } $result = []; $result['appId'] = self::$appid; $result['timeStamp'] = (string)time(); $result['nonceStr'] = uniqid(); $result['package'] = 'prepay_id=' . $re['prepay_id']; $result['signType'] = 'RSA'; $result['paySign'] = self::getPaySign($result); return $result; } /** * 查询订单 * @param type $sn */ public static function select($sn, $return = false) { $mchid = self::$mchid; //商户号 $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/' . $sn . '?mchid=' . $mchid; $re = self::wxCurl($url, [], 'GET'); if ($return) { return $re; } if (isset($re['trade_state']) && $re['trade_state'] == 'SUCCESS') { return true; } return false; } /** * 关闭订单 * @param type $sn */ public static function close($sn) { $mchid = self::$mchid; //商户号 $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/' . $sn . '/close'; $re = self::wxCurl($url, ['mchid' => $mchid], 'POST'); return true; } /** * 退款 * @param type $sn */ public static function refund($order_sn, $refund_sn, $total, $refund, $msg = '退款') { $url = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds'; $data = []; $data['notify_url'] = url('ag/weixin/notify_refund'); $data['out_trade_no'] = $order_sn; //订单号 $data['out_refund_no'] = $refund_sn; //退款单号 $data['reason'] = $msg; $data['amount'] = ['refund' => $refund * 100, 'total' => $total * 100, 'currency' => 'CNY']; $re = self::wxCurl($url, $data, 'POST'); return $re; } //请求 public static function wxCurl($url, $data = [], $method = 'GET') { $Authorization = self::getReSign($url, $data, $method); $header = [ 'Content-Type: application/json', 'Accept: application/json', 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36 Edg/89.0.774.63', 'Authorization: ' . $Authorization ]; $redata = $data ? json_encode($data) : ''; //CURL $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_FAILONERROR, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //有post参数-设置 if ($method=='POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $redata); } curl_setopt($ch, CURLOPT_HTTPHEADER, $header); $result = curl_exec($ch); curl_close($ch); return $result ? json_decode($result, true) : []; } //后端请求签名 public static function getReSign($url, $data, $method = 'GET') { $url_parts = parse_url($url); $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")); $http_method = $method; $timestamp = time(); $nonce = uniqid(); $body = $data ? json_encode($data) : ''; $mchid = self::$mchid; //商户id $serial_no = self::$serial_no; //证书编号 $private_key = self::getPrivateKey(BASE_PATH . 'cert/apiclient_key.pem'); //商户私钥 $message = $http_method . "\n" . $canonical_url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n"; openssl_sign($message, $raw_sign, $private_key, 'sha256WithRSAEncryption'); $sign = base64_encode($raw_sign); $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $mchid, $nonce, $timestamp, $serial_no, $sign); return 'WECHATPAY2-SHA256-RSA2048 ' . $token; } //前端小程序签名 public static function getPaySign($result) { $private_key = self::getPrivateKey(BASE_PATH . 'cert/apiclient_key.pem'); //商户私钥 $message = $result['appId'] . "\n" . $result['timeStamp'] . "\n" . $result['nonceStr'] . "\n" . $result['package'] . "\n"; openssl_sign($message, $raw_sign, $private_key, 'sha256WithRSAEncryption'); $sign = base64_encode($raw_sign); return $sign; } //验证签名 public static function checkSign() { $header = $_SERVER;//请求头 $serial_no = $header['wechatpay-serial'] ?? ''; //微信平台序列号 $timeStamp = $header['wechatpay-timestamp'] ?? ''; $nonce = $header['wechatpay-nonce'] ?? ''; $body = file_get_contents('php://input');//获取原始请求体 $wx_sign = $header['wechatpay-signature'] ?? ''; $wx_serial_no = self::$wx_serial_no; //保存的平台序列号(从下载的平台证书里面获取) if (!$serial_no || $wx_serial_no != $serial_no) {//证书过期 return false; } $message = $timeStamp . "\n" . $nonce . "\n" . $body . "\n"; $wx_sign = base64_decode($wx_sign); $public_key = self::getPublicKey(BASE_PATH . 'cert/wx_public_cert.pem'); //平台公钥 $res = openssl_verify($message, $wx_sign, $public_key, OPENSSL_ALGO_SHA256); if ($res == 1) { return true; } return false; } //获取私钥 public static function getPrivateKey($filepath) { return openssl_get_privatekey(file_get_contents($filepath)); } //获取公钥 public static function getPublicKey($filepath) { return openssl_pkey_get_public(file_get_contents($filepath)); } //加密数据(基本用不上) public static function getEncrypt($str) { //$str是待加密字符串 $public_key_path = BASE_PATH . 'cert/wx_public_cert.pem'; //'平台证书路径'; $public_key = file_get_contents($public_key_path); $encrypted = ''; if (openssl_public_encrypt($str, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)) { //base64编码 $sign = base64_encode($encrypted); } else { throw new Exception('encrypt failed'); } return $sign; } //解密数据(下载证书用的) public static function decryptToString($ciphertext, $associatedData, $nonceStr) { $aesKey =self::$mch_apiv3key; //商户apiv3密钥解密 $str = base64_decode($ciphertext); if (strlen($str) <= 16) { return ''; } // ext-sodium (default installed on >= PHP 7.2) 如果没有该函数需要安装sodium扩展 return sodium_crypto_aead_aes256gcm_decrypt($str, $associatedData, $nonceStr, $aesKey); } //下载平台证书 public static function downCert() { $url = 'https://api.mch.weixin.qq.com/v3/certificates'; $re = self::wxCurl($url, [], 'GET'); if (!isset($re['data'])) { exit('获取证书失败'); } $ciphertext = $re['data'][0]['encrypt_certificate']['ciphertext']; $associatedData = $re['data'][0]['encrypt_certificate']['associated_data']; $nonceStr = $re['data'][0]['encrypt_certificate']['nonce']; $data = self::decryptToString($ciphertext, $associatedData, $nonceStr); if (!$data) { exit('获取证书解密失败'); } //保存平台证书 (https://myssl.com/cert_decode.html)获取证书序列号 file_put_contents(BASE_PATH . '/cert/wx_public_cert.pem', $data); return $data; } }
2021-10-11 - 小程序登录、用户信息相关接口调整说明
公告更新时间:2021年04月15日考虑到近期开发者对小程序登录、用户信息相关接口调整的相关反馈,为优化开发者调整接口的体验,回收wx.getUserInfo接口可获取用户授权的个人信息能力的截止时间由2021年4月13日调整至2021年4月28日24时。为优化用户的使用体验,平台将进行以下调整: 2021年2月23日起,若小程序已在微信开放平台进行绑定,则通过wx.login接口获取的登录凭证可直接换取unionID2021年4月28日24时后发布的小程序新版本,无法通过wx.getUserInfo与<button open-type="getUserInfo"/>获取用户个人信息(头像、昵称、性别与地区),将直接获取匿名数据(包括userInfo与encryptedData中的用户个人信息),获取加密后的openID与unionID数据的能力不做调整。此前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。新增getUserProfile接口(基础库2.10.4版本开始支持),可获取用户头像、昵称、性别及地区信息,开发者每次通过该接口获取用户个人信息均需用户确认。具体接口文档:《getUserProfile接口文档》由于getUserProfile接口从2.10.4版本基础库开始支持(覆盖微信7.0.9以上版本),考虑到开发者在低版本中有获取用户头像昵称的诉求,对于未支持getUserProfile的情况下,开发者可继续使用getUserInfo能力。开发者可参考getUserProfile接口文档中的示例代码进行适配。请使用了wx.getUserInfo接口或<button open-type="getUserInfo"/>的开发者尽快适配。开发者工具1.05.2103022版本开始支持getUserProfile接口调试,开发者可下载该版本进行改造。 小游戏不受本次调整影响。 一、调整背景很多开发者在打开小程序时就通过组件方式唤起getUserInfo弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。 二、调整说明通过wx.login接口获取的登录凭证可直接换取unionID 若小程序已在微信开放平台进行绑定,原wx.login接口获取的登录凭证若需换取unionID需满足以下条件: 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用2月23日后,开发者调用wx.login获取的登录凭证可以直接换取unionID,无需满足以上条件。 回收wx.getUserInfo接口可获取用户个人信息能力 4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。 具体变化如下表: [图片] 即wx.getUserInfo接口的返回参数不变,但开发者获取的userInfo为匿名信息。 [图片] 此外,针对scope.userInfo将做如下调整: 若开发者调用wx.authorize接口请求scope.userInfo授权,用户侧不会触发授权弹框,直接返回授权成功若开发者调用wx.getSetting接口请求用户的授权状态,会直接读取到scope.userInfo为true新增getUserProfile接口 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。 插件用户信息功能页 插件申请获取用户头像昵称与用户身份标识符仍保留功能页的形式,不作调整。用户在用户信息功能页中授权之后,插件就可以直接调用 wx.login 和 wx.getUserInfo 。 三、最佳实践调整后,开发者如需获取用户身份标识符只需要调用wx.login接口即可。 开发者若需要在界面中展示用户的头像昵称信息,可以通过<open-data>组件进行渲染,该组件无需用户确认,可以在界面中直接展示。 在部分场景(如社交类小程序)中,开发者需要在获取用户的头像昵称信息,可调用wx.getUserProfile接口,开发者每次通过该接口均需用户确认,请开发者妥善处理调用接口的时机,避免过度弹出弹窗骚扰用户。 微信团队 2021年4月15日
2021-04-15