详解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 中的批处理的实现
Jun 14 PHP
PHP 杂谈《重构-改善既有代码的设计》之一 重新组织你的函数
Apr 09 PHP
PHP 透明水印生成代码
Aug 27 PHP
在PHP中设置、使用、删除Cookie的解决方法
May 06 PHP
基于PHP一些十分严重的缺陷详解
Jun 03 PHP
php针对cookie操作的队列操作类实例
Dec 10 PHP
php源码分析之DZX1.5字符串截断函数cutstr用法
Jun 17 PHP
php文档工具PHP Documentor安装与使用方法
Jan 25 PHP
PHP实现基于mysqli的Model基类完整实例
Apr 08 PHP
Yii2增加验证码步骤详解
Apr 25 PHP
一次因composer错误使用引发的问题与解决
Mar 06 PHP
PHP大文件及断点续传下载实现代码
Aug 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
PHP 日志缩略名的创建函数代码
2010/05/26 PHP
一些php技巧与注意事项分析
2011/02/03 PHP
php中JSON的使用方法
2015/04/30 PHP
PHP读取zip文件的方法示例
2016/11/17 PHP
fancybox1.3.1 基于Jquery的插件在IE中图片显示问题
2010/10/01 Javascript
javascript中setInterval的用法
2015/07/19 Javascript
Jquery简单分页实现方法
2015/07/24 Javascript
贴近用户体验的Jquery日期、时间选择插件
2015/08/19 Javascript
Javascript中replace()小结
2015/09/30 Javascript
用js读写cookie的简单方法(推荐)
2016/08/08 Javascript
jQuery实现的放大镜效果示例
2016/09/13 Javascript
微信小程序开发的四十个技术窍门总结(推荐)
2017/01/23 Javascript
详解Vue CLI3配置解析之css.extract
2018/09/14 Javascript
VUE简单的定时器实时刷新的实现方法
2019/01/20 Javascript
Nodejs实现的操作MongoDB数据库功能完整示例
2019/02/02 NodeJs
使用ESLint禁止项目导入特定模块的方法步骤
2019/03/04 Javascript
vue中axios请求的封装实例代码
2019/03/23 Javascript
js计算最大公约数和最小公倍数代码实例
2019/09/11 Javascript
在LayUI图片上传中,解决由跨域问题引起的请求接口错误的方法
2019/09/24 Javascript
Vue的transition-group与Virtual Dom Diff算法的使用
2019/12/09 Javascript
JavaScript数组类型Array相关的属性与方法详解
2020/09/08 Javascript
跟老齐学Python之再深点,更懂list
2014/09/20 Python
Python的ORM框架中SQLAlchemy库的查询操作的教程
2015/04/25 Python
请不要重复犯我在学习Python和Linux系统上的错误
2016/12/12 Python
Django使用详解:ORM 的反向查找(related_name)
2018/05/30 Python
Django使用paginator插件实现翻页功能的实例
2018/10/24 Python
Python进阶之使用selenium爬取淘宝商品信息功能示例
2019/09/16 Python
通过实例了解python property属性
2019/11/01 Python
雅诗兰黛(Estee Lauder)英国官方网站:世界顶级化妆品牌
2016/12/29 全球购物
会计专业毕业生自我评价
2013/09/25 职场文书
超市活动计划书
2014/04/24 职场文书
《三亚落日》教学反思
2014/04/26 职场文书
《陈毅探母》教学反思
2014/05/01 职场文书
党的群众路线教育实践活动领导班子对照检查材料
2014/09/25 职场文书
六一儿童节标语
2014/10/08 职场文书
JS轻量级函数式编程实现XDM二
2022/06/16 Javascript