• API v3版微信支付(二)----请求签名、证书和回调报文解密


    请求签名是微信用来验证请求的合法性的,签名是放在请求头中的编码串。

    签名生成

    商户可以按照下述步骤生成请求的签名。

    微信支付API v3 key要求商户对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付API v3将会拒绝处理请求,并返回401 Unauthorized

    准备

    商户需要拥有一个微信支付商户号,并通过超级管理员账号登录商户平台,获取商户API证书。商户API证书 的压缩包中包含了签名必需的私钥和商户证书。

    构造签名串

    我们希望商户的技术开发人员按照当前文档约定的规则构造签名串。微信支付会使用同样的方式构造签名串。如果商户构造签名串的方式错误,将导致签名验证不通过。下面先说明签名串的具体格式。

    签名串一共有五行,每一行为一个参数。行尾以  (换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以 结束,也需要附加一个

    HTTP请求方法
    
    URL
    
    请求时间戳
    
    请求随机串
    
    请求报文主体
    

    我们通过在命令行中调用"获取微信支付平台证书"接口,一步一步向开发者介绍如何进行请求签名。按照接口文档,获取商户平台证书的URL为 https://api.mch.weixin.qq.com/v3/certificates请求方法为GET,没有查询参数。

    第一步,获取HTTP请求的方法(GET,POSTPUT)等

    GET

    第二步,获取请求的绝对URL,并去除域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串。

    /v3/certificates

    第三步,获取发起请求时的系统当前时间戳,即格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数,作为请求时间戳。微信支付会拒绝处理很久之前发起的请求,请商户保持自身系统的时间准确。

    $ date +%s 1554208460

    第四步,生成一个请求随机串(我们推荐生成随机数算法如下:调用随机数函数生成,将得到的值转换为字符串) 。这里,我们使用命令行直接生成一个。

    $ hexdump -n 16 -e '4/4 "%08X" 1 " "' /dev/random
    593BEC0C930BF1AFEB40B4A08C8FB242

    第五步,获取请求中的请求报文主体(request body)。

    • 请求方法为GET时,报文主体为空。
    • 当请求方法为POST或PUT时,请使用真实发送的JSON报文。
    • 图片上传API,请使用meta对应的JSON报文。

    对于下载证书的接口来说,请求报文主体是一个空串。

    第六步,按照前述规则,构造的请求签名串为:

    GET
     
    /v3/certificates
    1554208460
    593BEC0C930BF1AFEB40B4A08C8FB242

    计算签名值

    绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。

    下面我们使用命令行演示如何生成签名。

    
    $ echo -n -e 
    "GET
    /v3/certificates
    1554208460
    593BEC0C930BF1AFEB40B4A08C8FB242
    
    " 
      | openssl dgst -sha256 -sign apiclient_key.pem 
      | openssl base64 -A
      uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==
                    

    设置HTTP头

    微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization认证类型和签名信息两个部分组成。

    下面我们使用命令行演示如何生成签名。

    Authorization: 认证类型 签名信息

    具体组成为:

    1.认证类型,目前为WECHATPAY2-SHA256-RSA2048

    2.签名信息

      • 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
      • 商户API证书serial_no,用于声明所使用的证书
      • 请求随机串nonce_str
      • 时间戳timestamp
      • 签名值signature

    注:以上五项签名信息,无顺序要求。

    Authorization 头的示例如下:(注意,示例因为排版可能存在换行,实际数据应在一行)

    Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930B

    最终我们可以组一个包含了签名的HTTP请求了。

    $ curl https://api.mch.weixin.qq.com/v3/certificates -H 'Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"'

    下面看一下具体代码实现:

    生成签名
     /**
         * 生成签名
         *
         * @param method
         * @param url
         * @param body
         * @return
         * @throws UnsupportedEncodingException
         * @throws SignatureException
         * @throws NoSuchAlgorithmException
         * @throws InvalidKeyException
         */
        public static String getSign(String method, HttpUrl url, String body) throws UnsupportedEncodingException, SignatureException, NoSuchAlgorithmException, InvalidKeyException {
            String schema = "WECHATPAY2-SHA256-RSA2048";
            String nonceStr = "c5ac7061fccab6bf3e254dcf98995b8c";
            long timestamp = System.currentTimeMillis() / 1000;
            String message = buildMessage(method, url, timestamp, nonceStr, body);
            String signature = sign(message.getBytes("utf-8"));
    
            return "mchid="" + CommonParameters.mchId + "","
                    + "nonce_str="" + nonceStr + "","
                    + "timestamp="" + timestamp + "","
                    + "serial_no="" + CommonParameters.mchSerialNo + "","
                    + "signature="" + signature + """;
        }
    
        static String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnsupportedEncodingException {
            PrivateKey merchantPrivateKey = PemUtil
                    .loadPrivateKey(new ByteArrayInputStream(CommonParameters.privateKey.getBytes("utf-8")));
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(merchantPrivateKey);
            sign.update(message);
    
            return Base64.getEncoder().encodeToString(sign.sign());
        }
    
        static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
            String canonicalUrl = url.encodedPath();
            if (url.encodedQuery() != null) {
                canonicalUrl += "?" + url.encodedQuery();
            }
    
            return method + "
    "
                    + canonicalUrl + "
    "
                    + timestamp + "
    "
                    + nonceStr + "
    "
                    + body + "
    ";
        }

    证书和回调报文解密

    为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。本章节详细介绍了加密报文的格式,以及如何进行解密。


    加密报文格式

    AES-GCM是一种NIST标准的认证加密算法, 是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。它最广泛的应用是在TLS中。

    证书和回调报文使用的加密密钥为是一种NIST标准的APIv3密钥

    对于加密的数据,我们使用了一个独立的JSON对象来表示。为了方便阅读,示例做了Pretty格式化,并加入了注释。

    {
    "original_type": "transaction", // 加密前的对象类型
    "algorithm":"AEAD_AES_256_GCM", // 加密算法
    
    // Base64编码后的密文
    "ciphertext": "...", 
    // 加密使用的随机串初始化向量)
    "nonce": "...", 
    // 附加数据包(可能为空)
    "associated_data": "" 
    }
    加密的随机串,跟签名时使用的随机串没有任何关系,是不一样的。
    解密代码实现(resource是请求的返回值)
                AesUtil aesUtil = new AesUtil(CommonParameters.apiV3Key.getBytes("utf-8"));
                String string = aesUtil.decryptToString(resource.getAssociated_data().getBytes("utf-8"), resource.getNonce().getBytes("utf-8"), resource.getCiphertext());

    获取平台证书并解密

    /**
         * 获取平台证书
         */
        public static X509Certificate getCertificates() throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
            CloseableHttpClient httpClient = CommonUtils.httpClient();
            //请求URL
            HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
            httpGet.setHeader("Accept", "application/json");
            //生成签名
            httpGet.setHeader("Authorization ", SignUtils.getSign("GET", HttpUrl.parse("https://api.mch.weixin.qq.com/v3/certificates"), ""));
            httpGet.setHeader("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
            //完成签名并执行请求
            CloseableHttpResponse response = httpClient.execute(httpGet);
            X509Certificate x509Certificate = null;
            try {
                int statusCode = response.getStatusLine().getStatusCode();
                if (statusCode == 200) { //处理成功
    //                System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
                    CertificateVo certificateVo = JSONObject.parseObject(EntityUtils.toString(response.getEntity()), CertificateVo.class);
                    for (Certificates certificates : certificateVo.getData()) {
                        if (format.parse(certificates.getEffective_time()).before(new Date()) && format.parse(certificates.getExpire_time()).after(new Date())) {
                            EncryptCertificate encrypt_certificate = certificates.getEncrypt_certificate();
                            //解密
                            AesUtil aesUtil = new AesUtil(CommonParameters.apiV3Key.getBytes("utf-8"));
                            String pulicKey = aesUtil.decryptToString(encrypt_certificate.getAssociated_data().getBytes("utf-8"), encrypt_certificate.getNonce().getBytes("utf-8"), encrypt_certificate.getCiphertext());
                            
                   //获取平台证书
    final CertificateFactory cf = CertificateFactory.getInstance("X509"); ByteArrayInputStream inputStream = new ByteArrayInputStream(pulicKey.getBytes(StandardCharsets.UTF_8)); x509Certificate = (X509Certificate) cf.generateCertificate(inputStream); } } return x509Certificate; } else if (statusCode == 204) { //处理成功,无返回Body System.out.println("success"); return x509Certificate; } else { System.out.println("failed,resp code = " + statusCode + ",return body = " + EntityUtils.toString(response.getEntity())); return x509Certificate; } } catch (GeneralSecurityException | ParseException e) { e.printStackTrace(); return null; } finally { response.close(); CommonUtils.after(httpClient); } }
     
  • 相关阅读:
    Django Cookie Session和自定义分页
    ORM版学员管理系统3
    ORM版学员管理系统2
    ORM版学员管理系统1
    Django 基础 ORM系统
    Django 基础 模板系统
    Django 基础 视图系统
    property 与 attribute 的区别?
    SQL数据库相关
    观察者模式-猫叫了,老鼠跑了,主人醒了...
  • 原文地址:https://www.cnblogs.com/wiliamzhao/p/14885312.html
Copyright © 2020-2023  润新知