支付宝小程序 PHP 获取用户手机号 验签解密 RSA解密 AES解密

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 支付宝小程序 PHP 获取用户手机号 验签解密 RSA解密 AES解密

需求

支付宝小程序端,获取到加密的用户手机号数据,需要经过服务端对数据进行解密,得到用户的手机号

问题

用户信息为敏感信息,需要用到敏感信息加密解密方法中的方式进行解密

服务端为PHP,由于官方没有对应的演示demo,经过摸索测试,还是出现了验签不通过,并且解密不成功的情况

解决过程

1.分析官方的java实例代码

String response = "小程序前端提交的";
//1. 获取验签和解密所需要的参数
Map<String, String> openapiResult = JSON.parseObject(response,
            new TypeReference<Map<String, String>>() {
            }, Feature.OrderedField);
String signType = StringUtil.defaultIfBlank(openapiResult.get("signType"), "RSA2");
String charset = StringUtil.defaultIfBlank(openapiResult.get("charset"), "UTF-8");
String encryptType = StringUtil.defaultIfBlank(openapiResult.get("encryptType"), "AES");
String sign = openapiResult.get("sign");
String content = openapiResult.get("response");
//如果密文的
boolean isDataEncrypted = !content.startsWith("{");
boolean signCheckPass = false;
//2. 验签
String signContent = content;
String signVeriKey = "你的小程序对应的支付宝公钥(为扩展考虑建议用appId+signType做密钥存储隔离)";
String encryptType = "你的小程序对应的加解密密钥(为扩展考虑建议用appId+encryptType做密钥存储隔离)"
//如果是加密的报文则需要在密文的前后添加双引号
if (isDataEncrypted) {
    signContent = "\"" + signContent + "\"";
}
try {
    signCheckPass = AlipaySignature.rsaCheck(signContent, sign, signVeriKey, charset, signType);
} catch (AlipayApiException e) {
    //验签异常, 日志
}
if(!signCheckPass) {
    //验签不通过(异常或者报文被篡改),终止流程(不需要做解密)
    throw new Exception("验签失败");
}
//3. 解密
String plainData = null;
if (isDataEncrypted) {
    try {
        AlipayEncrypt.decryptContent(content, encryptType, decryptKey, charset);
    } catch (AlipayApiException e) {
        //解密异常, 记录日志
       throw new Exception("解密异常");
    }
} else {
    plainData = content;
}

直接翻译这段代码为PHP的,并采用阿里云的SDK调用核心的两个方法。

经过不断的尝试,还是以失败告终。

2.分析定位阿里云SDK源码

阿里云的SDK中,首先通用的初始化了AopClient,将小程序的参数在里面初始化,其他操作并没有。

经过检查,AopClient中解密和校验的代码如下

/** rsaCheckV1 & rsaCheckV2
   *  验证签名
   *  在使用本方法前,必须初始化AopClient且传入公钥参数。
   *  公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。
   **/
  public function rsaCheckV1($params, $rsaPublicKeyFilePath,$signType='RSA') {
    $sign = $params['sign'];
    $params['sign_type'] = null;
    $params['sign'] = null;
    return $this->verify($this->getSignContent($params), $sign, $rsaPublicKeyFilePath,$signType);
  }
  public function rsaCheckV2($params, $rsaPublicKeyFilePath, $signType='RSA') {
    $sign = $params['sign'];
    $params['sign'] = null;
    return $this->verify($this->getSignContent($params), $sign, $rsaPublicKeyFilePath, $signType);
  }
  function verify($data, $sign, $rsaPublicKeyFilePath, $signType = 'RSA') {
    if($this->checkEmpty($this->alipayPublicKey)){
      $pubKey= $this->alipayrsaPublicKey;
      $res = "-----BEGIN PUBLIC KEY-----\n" .
        wordwrap($pubKey, 64, "\n", true) .
        "\n-----END PUBLIC KEY-----";
    }else {
      //读取公钥文件
      $pubKey = file_get_contents($rsaPublicKeyFilePath);
      //转换为openssl格式密钥
      $res = openssl_get_publickey($pubKey);
    }
    ($res) or die('支付宝RSA公钥错误。请检查公钥文件格式是否正确');  
    //调用openssl内置方法验签,返回bool值
    $result = FALSE;
    if ("RSA2" == $signType) {
      $result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256)===1);;
    } else {
      $result = (openssl_verify($data, base64_decode($sign), $res)===1);
    }
    if(!$this->checkEmpty($this->alipayPublicKey)) {
      //释放资源
      openssl_free_key($res);
    }
    return $result;
  }
/** 
   *  在使用本方法前,必须初始化AopClient且传入公私钥参数。
   *  公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。
   **/
  public function rsaDecrypt($data, $rsaPrivateKeyPem = null, $charset = null) {
    if($this->checkEmpty($this->rsaPrivateKeyFilePath)){
      //读字符串
      $priKey=$this->rsaPrivateKey;
      $res = "-----BEGIN RSA PRIVATE KEY-----\n" .
        wordwrap($priKey, 64, "\n", true) .
        "\n-----END RSA PRIVATE KEY-----";
    }else {
      $priKey = file_get_contents($this->rsaPrivateKeyFilePath);
      $res = openssl_get_privatekey($priKey);
    }
    ($res) or die('您使用的私钥格式错误,请检查RSA私钥配置'); 
    //转换为openssl格式密钥
    $decodes = explode(',', $data);
    $strnull = "";
    $dcyCont = "";
    foreach ($decodes as $n => $decode) {
      if (!openssl_private_decrypt($decode, $dcyCont, $res)) {
        echo "<br/>" . openssl_error_string() . "<br/>";
      }
      $strnull .= $dcyCont;
    }
    return $strnull;
  }
function verify($data, $sign, $rsaPublicKeyFilePath, $signType = 'RSA') {
    if($this->checkEmpty($this->alipayPublicKey)){
      $pubKey= $this->alipayrsaPublicKey;
      $res = "-----BEGIN PUBLIC KEY-----\n" .
        wordwrap($pubKey, 64, "\n", true) .
        "\n-----END PUBLIC KEY-----";
    }else {
      //读取公钥文件
      $pubKey = file_get_contents($rsaPublicKeyFilePath);
      //转换为openssl格式密钥
      $res = openssl_get_publickey($pubKey);
    }
    ($res) or die('支付宝RSA公钥错误。请检查公钥文件格式是否正确');  
    //调用openssl内置方法验签,返回bool值
    $result = FALSE;
    if ("RSA2" == $signType) {
      $result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256)===1);;
    } else {
      $result = (openssl_verify($data, base64_decode($sign), $res)===1);
    }
    if(!$this->checkEmpty($this->alipayPublicKey)) {
      //释放资源
      openssl_free_key($res);
    }
    return $result;
  }

上面的代码分别对应的流程中的 验签、解密、校验,通过一行一行执行,定位分析问题,最终定位到的为

$result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256)===1);;
if (!openssl_private_decrypt($decode, $dcyCont, $res)) {
        echo "<br/>" . openssl_error_string() . "<br/>";
      }

也就是说openssl遇到的问题,这也和我在调试过程中的报错信息展示一致

3.论坛&搜索

通过查询官方论坛和找到支付宝的官方客服,得到的都只有一句话

你好,解密方式:填充方式是AES/CBC/PKCS5Padding;偏移量全是0

那么怎么能用PHP实现AES/CBC/PKCS5Padding呢?

需要用到下面的类

class MagicCrypt {
    private $iv = "0102030405060708";//密钥偏移量IV,可自定义
    private $encryptKey = "自定义16位长度key";//AESkey,可自定义
    //加密
    public function encrypt($encryptStr) {
        $localIV = $this->iv;
        $encryptKey = $this->encryptKey;
        //Open module
        $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, $localIV);
        //print "module = $module <br/>" ;
        mcrypt_generic_init($module, $encryptKey, $localIV);
        //Padding
        $block = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
        $pad = $block - (strlen($encryptStr) % $block); //Compute how many characters need to pad
        $encryptStr .= str_repeat(chr($pad), $pad); // After pad, the str length must be equal to block or its integer multiples
        //encrypt
        $encrypted = mcrypt_generic($module, $encryptStr);
        //Close
        mcrypt_generic_deinit($module);
        mcrypt_module_close($module);
        return base64_encode($encrypted);
    }
    //解密
    public function decrypt($encryptStr) {
        $localIV = $this->iv;
        $encryptKey = $this->encryptKey;
        //Open module
        $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, $localIV);
        //print "module = $module <br/>" ;
        mcrypt_generic_init($module, $encryptKey, $localIV);
        $encryptedData = base64_decode($encryptStr);
        $encryptedData = mdecrypt_generic($module, $encryptedData);
        return $encryptedData;
    }
}

但是,这个类中的mcrypt_module_open对应的扩展,在PHP7以上的版本就废弃了,所以这个方法也是不能用的。 : (

解决方案

疑问1:微信小程序是小程序,支付宝小程序也是小程序,他们之间能不能有什么关联?

疑问2:我刚做完微信小程序的用户信息获取,也是采用前端获取加密数据传给服务端,服务端解密的方式,那这之间有没有什么关联?

带着这些疑问,我重新看了下微信小程序提供的解析demo

class WXBizDataCrypt
{
    private $appid;
    private $sessionKey;
    /**
     * 构造函数
     * @param $sessionKey string 用户在小程序登录后获取的会话密钥
     * @param $appid string 小程序的appid
     */
    public function __construct( $appid, $sessionKey)
    {
        $this->sessionKey = $sessionKey;
        $this->appid = $appid;
    }
    /**
     * 检验数据的真实性,并且获取解密后的明文.
     * @param $encryptedData string 加密的用户数据
     * @param $iv string 与用户数据一同返回的初始向量
     * @param $data string 解密后的原文
     *
     * @return int 成功0,失败返回对应的错误码
     */
    public function decryptData( $encryptedData, $iv, &$data )
    {
        if (strlen($this->sessionKey) != 24) {
            return ErrorCode::$IllegalAesKey;
        }
        $aesKey=base64_decode($this->sessionKey);
        if (strlen($iv) != 24) {
            return ErrorCode::$IllegalIv;
        }
        $aesIV=base64_decode($iv);
        $aesCipher=base64_decode($encryptedData);
        $result=openssl_decrypt( $aesCipher, "AES-128-CBC", $aesKey, 1, $aesIV);
        $dataObj=json_decode( $result );
        if( $dataObj  == NULL )
        {
            return ErrorCode::$IllegalBuffer;
        }
        if( $dataObj->watermark->appid != $this->appid )
        {
            return ErrorCode::$IllegalBuffer;
        }
        $data = $result;
        return ErrorCode::$OK;
    }
}

相同点:

  • 都采用aes密钥解密
  • 解决方案中都存在偏移量iv
    不同点:
  • 微信的aes密钥为从登录code中拿到的sessionKey,支付宝为管理后台随机生成
  • 微信小程序校验解密得到的数据中的watermark水印

由此可以得出一个大胆的想法,并由此得到一个修改之后的方法

public function decryptData( $encryptedData )
    {
        $key = '支付宝生成的aesKey';
        $aesKey=base64_decode($key);
        $iv = 0;
        $aesIV=base64_decode($iv);
        $aesCipher=base64_decode($encryptedData);
        $result=openssl_decrypt( $aesCipher, "AES-128-CBC", $aesKey, 1, $aesIV);
        return $result;
    }

经过亲测有效,得到的数据为

{"code":"10000","msg":"Success","mobile":"用户支付宝绑定的手机号"}

返回的结果和官方文档中的一致。

到此,支付宝获取用户敏感信息的PHP后台解密问题,已经解决。

参考资料

相关文章
ly~
|
3月前
|
存储 供应链 小程序
除了微信小程序,PHP 还可以用于开发哪些类型的小程序?
除了微信小程序,PHP 还可用于开发多种类型的小程序,包括支付宝小程序、百度智能小程序、抖音小程序、企业内部小程序及行业特定小程序。在电商、生活服务、资讯、工具、娱乐、营销等领域,PHP 能有效管理商品信息、订单处理、支付接口、内容抓取、复杂计算、游戏数据、活动规则等多种业务。同时,在企业内部,PHP 可提升工作效率,实现审批流程、文件共享、生产计划等功能;在医疗和教育等行业,PHP 能管理患者信息、在线问诊、课程资源、成绩查询等重要数据。
ly~
84 6
|
6月前
|
存储 算法 安全
PHP AES加解密示例
PHP AES加解密示例
117 0
PHP AES加解密示例
|
6月前
|
存储 小程序 前端开发
【微信小程序 - 工作实战分享】1.微信小程序发送手机短信验证码(阿里云)
【微信小程序 - 工作实战分享】1.微信小程序发送手机短信验证码(阿里云)
523 0
|
2月前
|
小程序 JavaScript API
微信小程序开发之:保存图片到手机,使用uni-app 开发小程序;还有微信原生保存图片到手机
这篇文章介绍了如何在uni-app和微信小程序中实现将图片保存到用户手机相册的功能。
952 0
微信小程序开发之:保存图片到手机,使用uni-app 开发小程序;还有微信原生保存图片到手机
|
2月前
|
小程序
如何将CCBUPT全能墙小程序添加到手机桌面
如何将CCBUPT全能墙小程序添加到手机桌面
36 0
|
4月前
|
小程序 安全 Java
|
4月前
|
存储 小程序 JavaScript
|
4月前
|
存储 小程序 JavaScript
|
4月前
|
存储 小程序 关系型数据库
原生小程序 获取手机号并进行存储到mysql数据库
原生小程序 获取手机号并进行存储到mysql数据库
|
5月前
|
小程序 前端开发
手机租房房源小程序模板源码
手机租房房源小程序模板源码
148 4