详解PHP实现支付宝小程序用户授权的工具类


Posted in PHP onDecember 25, 2018

背景

最近项目需要上线支付宝小程序,同时需要走用户的授权流程完成用户信息的存储,以前做过微信小程序的开发,本以为实现授权的过程是很简单的事情,但是再实现的过程中还是遇到了不少的坑,因此记录一下实现的过程

学到的知识

  • 支付宝开放接口的调用模式以及实现方式
  • 支付宝小程序授权的流程
  • RSA加密方式

吐槽点

支付宝小程序的入口隐藏的很深,没有微信小程序那么直接了当
支付宝小程序的开发者工具比较难用,编译时候比较卡,性能有很大的问题
每提交一次代码,支付宝小程序的体验码都要进行更换,比较繁琐,而且localStorage的东西不知道要如何删除

事先准备

  • 到支付宝开放平台注册一个开发者账号,并做好相应的认证等工作
  • 创建一个小程序,并记录好相关的小程序信息,包括支付宝公钥,私钥,app公钥等,可以借鉴支付宝官方提供的相应的公钥生成工具来生成公钥和私钥,工具的下载地址:传送门
  • 了解下支付宝小程序的签名机制,详细见https://docs.open.alipay.com/291/105974
  • 熟悉下支付宝小程序获取用户信息的过程,详细见支付宝小程序用户授权指引

授权的步骤

授权时序图

详解PHP实现支付宝小程序用户授权的工具类

实现流程

  1. 客户端通过my.getAuthCode接口获取code,传给服务端
  2. 服务端通过code,调用获取token接口获取access_token,alipay.system.oauth.token(换取授权访问令牌)
  3. 通过token接口调用支付宝会员查询接口获取会员信息,alipay.user.info.share(支付宝会员授权信息查询接口)
  4. 将获取的用户信息保存到数据库

AmpHelper工具类

<?php
/**
 * Created by PhpStorm.
 * User: My
 * Date: 2018/8/16
 * Time: 17:45
 */

namespace App\Http\Helper;

use App\Http\Helper\Sys\BusinessHelper;
use Illuminate\Support\Facades\Log;

class AmpHelper
{

  const API_DOMAIN = "https://openapi.alipay.com/gateway.do?";
  const API_METHOD_GENERATE_QR = 'alipay.open.app.qrcode.create';
  const API_METHOD_AUTH_TOKEN = 'alipay.system.oauth.token';
  const API_METHOD_GET_USER_INFO = 'alipay.user.info.share';

  const SIGN_TYPE_RSA2 = 'RSA2';
  const VERSION = '1.0';
  const FILE_CHARSET_UTF8 = "UTF-8";
  const FILE_CHARSET_GBK = "GBK";
  const RESPONSE_OUTER_NODE_QR = 'alipay_open_app_qrcode_create_response';
  const RESPONSE_OUTER_NODE_AUTH_TOKEN = 'alipay_system_oauth_token_response';
  const RESPONSE_OUTER_NODE_USER_INFO = 'alipay_user_info_share_response';
  const RESPONSE_OUTER_NODE_ERROR_RESPONSE = 'error_response';

  const STATUS_CODE_SUCCESS = 10000;
  const STATUS_CODE_EXCEPT = 20000;


  /**
   * 获取用户信息接口,根据token
   * @param $code 授权码
   * 通过授权码获取用户的信息
   */
  public static function getAmpUserInfoByAuthCode($code){
    $aliUserInfo = [];
    $tokenData = AmpHelper::getAmpToken($code);
    //如果token不存在,这种主要是为了处理支付宝的异常记录
    if(isset($tokenData['code'])){
      return $tokenData;
    }
    $token = formatArrValue($tokenData,'access_token');
    if($token){
      $userBusiParam = self::getAmpUserBaseParam($token);
      $url = self::buildRequestUrl($userBusiParam);
      $resonse = self::getResponse($url,self::RESPONSE_OUTER_NODE_USER_INFO);
      if($resonse['code'] == self::STATUS_CODE_SUCCESS){
        //有效的字段列
        $userInfoColumn = ['user_id','avatar','province','city','nick_name','is_student_certified','user_type','user_status','is_certified','gender'];
        foreach ($userInfoColumn as $column){
          $aliUserInfo[$column] = formatArrValue($resonse,$column,'');
        }

      }else{
        $exceptColumns = ['code','msg','sub_code','sub_msg'];
        foreach ($exceptColumns as $column){
          $aliUserInfo[$column] = formatArrValue($resonse,$column,'');
        }
      }
    }
    return $aliUserInfo;
  }


  /**
   * 获取小程序token接口
   */
  public static function getAmpToken($code){
    $param = self::getAuthBaseParam($code);
    $url = self::buildRequestUrl($param);
    $response = self::getResponse($url,self::RESPONSE_OUTER_NODE_AUTH_TOKEN);
    $tokenResult = [];
    if(isset($response['code']) && $response['code'] != self::STATUS_CODE_SUCCESS){
      $exceptColumns = ['code','msg','sub_code','sub_msg'];
      foreach ($exceptColumns as $column){
        $tokenResult[$column] = formatArrValue($response,$column,'');
      }
    }else{
      $tokenResult = $response;
    }
    return $tokenResult;
  }

  /**
   * 获取二维码链接接口
   * 433ac5ea4c044378826afe1532bcVX78
   * https://openapi.alipay.com/gateway.do?timestamp=2013-01-01 08:08:08&method=alipay.open.app.qrcode.create&app_id=2893&sign_type=RSA2&sign=ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE&version=1.0&biz_content=
  {"url_param":"/index.html?name=ali&loc=hz", "query_param":"name=1&age=2", "describe":"二维码描述"}
  */
  public static function generateQrCode($mpPage = 'pages/index',$queryParam = [],$describe){
    $param = self::getQrcodeBaseParam($mpPage,$queryParam,$describe );
    $url = self::buildRequestUrl($param);
    $response = self::getResponse($url,self::RESPONSE_OUTER_NODE_QR);
    return $response;
  }


  /**
   * 获取返回的数据,对返回的结果做进一步的封装和解析,因为支付宝的每个接口的返回都是由一个特定的  
   * key组成的,因此这里直接封装了而一个通用的方法,对于不同的接口只需要更改相应的node节点就可以了
   */
  public static function getResponse($url,$responseNode){
    $json = curlRequest($url);
    $response = json_decode($json,true);
    $responseContent = formatArrValue($response,$responseNode,[]);
    $errResponse = formatArrValue($response,self::RESPONSE_OUTER_NODE_ERROR_RESPONSE,[]);
    if($errResponse){
      return $errResponse;
    }
    return $responseContent;
  }

  /**
   * 获取请求的链接
   */
  public static function buildQrRequestUrl($mpPage = 'pages/index',$queryParam = []){
    $paramStr = http_build_query(self::getQrBaseParam($mpPage,$queryParam));
    return self::API_DOMAIN . $paramStr;
  }



  /**
   * 构建请求链接
   */
  public static function buildRequestUrl($param){
    $paramStr = http_build_query($param);
    return self::API_DOMAIN . $paramStr;
  }


  /**
   * 获取用户的基础信息接口
   */
  public static function getAmpUserBaseParam($token){
    $busiParam = [
      'auth_token' => $token,
    ];
    $param = self::buildApiBuisinessParam($busiParam,self::API_METHOD_GET_USER_INFO);
    return $param;

  }

  /**
   *获取二维码的基础参数
   */
  public static function getQrcodeBaseParam($page= 'pages/index/index',$queryParam = [],$describe = ''){
    $busiParam = [
      'biz_content' => self::getQrBizContent($page,$queryParam,$describe)
    ];
    $param = self::buildApiBuisinessParam($busiParam,self::API_METHOD_GENERATE_QR);
    return $param;

  }

  /**
   *获取授权的基础参数
   */
  public static function getAuthBaseParam($code,$refreshToken = ''){
    $busiParam = [
      'grant_type' => 'authorization_code',
      'code' => $code,
      'refresh_token' => $refreshToken,
    ];
    $param = self::buildApiBuisinessParam($busiParam,self::API_METHOD_AUTH_TOKEN);
    return $param;
  }


  /**
   * 构建业务参数
   */
  public static function buildApiBuisinessParam($businessParam,$apiMethod){
    $pubParam = self::getApiPubParam($apiMethod);
    $businessParam = array_merge($pubParam,$businessParam);
    $signContent = self::getSignContent($businessParam);
    error_log('sign_content ===========>'.$signContent);
    $rsaHelper = new RsaHelper();
    $sign = $rsaHelper->createSign($signContent);
    error_log('sign ===========>'.$sign);
    $businessParam['sign'] = $sign;
    return $businessParam;
  }


  /**
   * 公共参数
   *
   */
  public static function getApiPubParam($apiMethod){
    $ampBaseInfo = BusinessHelper::getAmpBaseInfo();
    $param = [
      'timestamp' => date('Y-m-d H:i:s') ,
      'method' => $apiMethod,
      'app_id' => formatArrValue($ampBaseInfo,'appid',config('param.amp.appid')),
      'sign_type' =>self::SIGN_TYPE_RSA2,
      'charset' =>self::FILE_CHARSET_UTF8,
      'version' =>self::VERSION,
    ];
    return $param;
  }


  /**
   * 获取签名的内容
   */
  public static function getSignContent($params) {
    ksort($params);
    $stringToBeSigned = "";
    $i = 0;
    foreach ($params as $k => $v) {
      if (!empty($v) && "@" != substr($v, 0, 1)) {
        if ($i == 0) {
          $stringToBeSigned .= "$k" . "=" . "$v";
        } else {
          $stringToBeSigned .= "&" . "$k" . "=" . "$v";
        }
        $i++;
      }
    }
    unset ($k, $v);
    return $stringToBeSigned;
  }


  public static function convertArrToQueryParam($param){
    $queryParam = [];
    foreach ($param as $key => $val){
      $obj = $key.'='.$val;
      array_push($queryParam,$obj);
    }
    $queryStr = implode('&',$queryParam);
    return $queryStr;
  }

  /**
   * 转换字符集编码
   * @param $data
   * @param $targetCharset
   * @return string
   */
  public static function characet($data, $targetCharset) {
    if (!empty($data)) {
      $fileType = self::FILE_CHARSET_UTF8;
      if (strcasecmp($fileType, $targetCharset) != 0) {
        $data = mb_convert_encoding($data, $targetCharset, $fileType);
      }
    }
    return $data;
  }

  /**
   * 获取业务参数内容
   */
  public static function getQrBizContent($page, $queryParam = [],$describe = ''){
    if(is_array($queryParam)){
      $queryParam = http_build_query($queryParam);
    }
    $obj = [
      'url_param' => $page,
      'query_param' => $queryParam,
      'describe' => $describe
    ];
    $bizContent = json_encode($obj,JSON_UNESCAPED_UNICODE);
    return $bizContent;
  }

}

AmpHeler工具类关键代码解析相关常量

//支付宝的api接口地址
const API_DOMAIN = "https://openapi.alipay.com/gateway.do?";
//获取支付宝二维码的接口方法
const API_METHOD_GENERATE_QR = 'alipay.open.app.qrcode.create';
//获取token的接口方法
const API_METHOD_AUTH_TOKEN = 'alipay.system.oauth.token';
//获取用户信息的接口方法
const API_METHOD_GET_USER_INFO = 'alipay.user.info.share';
//支付宝的签名方式,由RSA2和RSA两种
const SIGN_TYPE_RSA2 = 'RSA2';
//版本号,此处固定挑那些就可以了
const VERSION = '1.0';
//UTF8编码
const FILE_CHARSET_UTF8 = "UTF-8";
//GBK编码
const FILE_CHARSET_GBK = "GBK";
//二维码接口调用成功的 返回节点
const RESPONSE_OUTER_NODE_QR = 'alipay_open_app_qrcode_create_response';
//token接口调用成功的 返回节点
const RESPONSE_OUTER_NODE_AUTH_TOKEN = 'alipay_system_oauth_token_response';
//用户信息接口调用成功的 返回节点
const RESPONSE_OUTER_NODE_USER_INFO = 'alipay_user_info_share_response';
//错误的返回的时候的节点
const RESPONSE_OUTER_NODE_ERROR_RESPONSE = 'error_response';

const STATUS_CODE_SUCCESS = 10000;
const STATUS_CODE_EXCEPT = 20000;

getAmpUserInfoByAuthCode方法

这个方法是获取用户信息的接口方法,只需要传入客户端传递的code,就可以获取到用户的完整信息

getAmpToken方法

这个方法是获取支付宝接口的token的方法,是一个公用方法,后面所有的支付宝的口调用,都可以使用这个方法先获取token

getResponse方法

考虑到会调用各个支付宝的接口,因此这里封装这个方法是为了方便截取接口返回成功之后的信息,提高代码的阅读性

getApiPubParam方法

这个方法是为了获取公共的参数,包括版本号,编码,appid,签名类型等基础业务参数

getSignContent方法

这个方法是获取签名的内容,入参是一个数组,最后输出的是参数的拼接字符串

buildApiBuisinessParam($businessParam,$apiMethod)

这个是构建api独立的业务参数部分方法,businessParam参数是支付宝各个接口的业务参数部分(出去公共参数),$apiMethod是对应的接口的方法名称,如获取token的方法名为alipay.system.oauth.token

签名帮助类

<?php
/**
 * Created by PhpStorm.
 * User: Auser
 * Date: 2018/12/4
 * Time: 15:37
 */

namespace App\Http\Helper;

/**
 *$rsa2 = new Rsa2();
 *$data = 'mydata'; //待签名字符串
 *$strSign = $rsa2->createSign($data);   //生成签名
 *$is_ok = $rsa2->verifySign($data, $strSign); //验证签名
 */
class RsaHelper
{

  private static $PRIVATE_KEY;
  private static $PUBLIC_KEY;


  function __construct(){
    self::$PRIVATE_KEY = config('param.amp.private_key');
    self::$PUBLIC_KEY = config('param.amp.public_key');
  }

  /**
   * 获取私钥
   * @return bool|resource
   */
  private static function getPrivateKey()
  {
    $privKey = self::$PRIVATE_KEY;
    $privKey = "-----BEGIN RSA PRIVATE KEY-----".PHP_EOL.wordwrap($privKey, 64, PHP_EOL, true).PHP_EOL."-----END RSA PRIVATE KEY-----";
    ($privKey) or die('您使用的私钥格式错误,请检查RSA私钥配置');
    error_log('private_key is ===========>: '.$privKey);
    return openssl_pkey_get_private($privKey);
  }
  /**
   * 获取公钥
   * @return bool|resource
   */
  private static function getPublicKey()
  {
    $publicKey = self::$PUBLIC_KEY;
    $publicKey = "-----BEGIN RSA PRIVATE KEY-----".PHP_EOL.wordwrap($publicKey, 64, PHP_EOL, true).PHP_EOL."-----END RSA PRIVATE KEY-----";
    error_log('public key is : ===========>'.$publicKey);
    return openssl_pkey_get_public($publicKey);
  }
  /**
   * 创建签名
   * @param string $data 数据
   * @return null|string
   */
  public function createSign($data = '')
  {
    // var_dump(self::getPrivateKey());die;
    if (!is_string($data)) {
      return null;
    }
    return openssl_sign($data, $sign, self::getPrivateKey(),OPENSSL_ALGO_SHA256 ) ? base64_encode($sign) : null;
  }
  /**
   * 验证签名
   * @param string $data 数据
   * @param string $sign 签名
   * @return bool
   */
  public function verifySign($data = '', $sign = '')
  {
    if (!is_string($sign) || !is_string($sign)) {
      return false;
    }
    return (bool)openssl_verify(
      $data,
      base64_decode($sign),
      self::getPublicKey(),
      OPENSSL_ALGO_SHA256
    );
  }
}

调用

$originUserData = AmpHelper::getAmpUserInfoByAuthCode($code);
echo $originUserData;

注意getAmpUserInfoByAuthCode方法,调用接口成功,会返回支付宝用户的正确信息,示例如下

{
  "alipay_user_info_share_response": {
    "code": "10000",
    "msg": "Success",
    "user_id": "2088102104794936",
    "avatar": "http://tfsimg.alipay.com/images/partner/T1uIxXXbpXXXXXXXX",
    "province": "安徽省",
    "city": "安庆",
    "nick_name": "支付宝小二",
    "is_student_certified": "T",
    "user_type": "1",
    "user_status": "T",
    "is_certified": "T",
    "gender": "F"
  },
  "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
}

踩坑点

  1. 在开发之前一定要仔细阅读用户的授权流程指引文档,否则很容出错
  2. 对于用户信息接口,在获取授权信息接口并没有做明确的说明,所以需要先梳理清楚
  3. 支付宝的签名机制和微信的有很大不同,对于习惯了微信小程序开发的人来说,刚开始可能有点不适应,所以需要多看看sdk里面的实现

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
如何正确理解PHP的错误信息
Oct 09 PHP
某大型网络公司应聘时的笔试题目附答案
Mar 27 PHP
使用php判断网页是否gzip压缩
Jun 25 PHP
PHP 只允许指定IP访问(允许*号通配符过滤IP)
Jul 08 PHP
php实现获取局域网所有用户的电脑IP和主机名、及mac地址完整实例
Jul 18 PHP
php中mail函数发送邮件失败的解决方法
Dec 24 PHP
Yii获取当前url和域名的方法
Jun 08 PHP
PHP实现简单的新闻发布系统实例
Jul 28 PHP
Yii2如何批量添加数据
May 17 PHP
php封装的mysqli类完整实例
Oct 18 PHP
Yii框架弹出窗口组件CJuiDialog用法分析
Jan 07 PHP
php微信分享到朋友圈、QQ、朋友、微博
Feb 18 PHP
PHP正则匹配到2个字符串之间的内容方法
Dec 24 #PHP
PHP基于PDO扩展操作mysql数据库示例
Dec 24 #PHP
PHP封装的page分页类定义与用法完整示例
Dec 24 #PHP
tp5(thinkPHP5)框架连接数据库的方法示例
Dec 24 #PHP
php workerman定时任务的实现代码
Dec 23 #PHP
PHP时间日期增减操作示例【date strtotime实现加一天、加一月等操作】
Dec 21 #PHP
PHP面向对象程序设计(OOP)之方法重写(override)操作示例
Dec 21 #PHP
You might like
阿拉伯的咖啡与水烟
2021/03/03 咖啡文化
php编程每天必学之验证码
2016/03/03 PHP
PNGHandler-借助JS让PNG图在IE下实现透明(包括背景图)
2007/08/31 Javascript
基于Jquery的动态添加控件并取值的实现代码
2010/09/24 Javascript
node.js开机自启动脚本文件
2014/12/24 Javascript
js实现文本框中输入文字页面中div层同步获取文本框内容的方法
2015/03/03 Javascript
javascript显示上周、上个月日期的处理方法
2016/02/03 Javascript
EasyUI布局 高度自适应
2016/06/04 Javascript
localStorage实现便签小程序
2016/11/28 Javascript
Bootstrap风格的WPF样式
2016/12/07 Javascript
解决微信内置浏览器返回上一页强制刷新问题方法
2017/02/05 Javascript
jQuery实现Table表格隔行变色及高亮显示当前选择行效果示例
2017/02/14 Javascript
jQuery弹出窗口简单实现代码
2017/03/09 Javascript
详解webpack编译多页面vue项目的配置问题
2017/12/11 Javascript
vue进行图片的预加载watch用法实例讲解
2018/02/07 Javascript
[02:04]2020年夜魇暗潮预告片
2020/10/30 DOTA
pytorch + visdom 处理简单分类问题的示例
2018/06/04 Python
Python设计模式之抽象工厂模式原理与用法详解
2019/01/15 Python
pandas.read_csv参数详解(小结)
2019/06/21 Python
python3.8 微信发送服务器监控报警消息代码实现
2019/11/05 Python
python 画图 图例自由定义方式
2020/04/17 Python
关于h5中的fetch方法解读(小结)
2017/11/15 HTML / CSS
中国医药集团国药在线:国药网
2017/02/06 全球购物
英国天然抗衰老护肤品品牌:Nakin Skin Care
2019/04/16 全球购物
意大利奢侈品购物网站:Deliberti
2019/10/08 全球购物
采用怎样的方法保证数据的完整性
2013/12/02 面试题
学生发电厂实习自我鉴定
2013/09/22 职场文书
现金会计岗位职责
2013/12/05 职场文书
工程招投标邀请书
2014/01/30 职场文书
小学三年级学生评语
2014/04/22 职场文书
法律顾问服务方案
2014/05/15 职场文书
办公楼租房协议书范本
2014/11/25 职场文书
详解Node.js如何处理ES6模块
2021/05/15 Javascript
html+css实现环绕倒影加载特效
2021/07/07 HTML / CSS
css中z-index: 0和z-index: auto的区别
2021/08/23 HTML / CSS
JS实现简单九宫格抽奖
2022/06/28 Javascript