对接微信支付APIv3版本签名验签学习

对接微信支付APIv3版本签名验签学习

学习整理了一下对接微信支付接口,不是使用SDK版本的,纯java开发。

微信官方参考网站:
https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-generation.html

按照微信官方接口说明进行开发

一. 前提:申请商户号和证书

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

二. 签名

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

具体组成为:

  1. 认证类型,目前为WECHATPAY2-SHA256-RSA2048
  2. 签名信息
    • 发起请求的商户(包括直连商户、服务商或渠道商)的商户号mchid
    • 商户API证书序列号serial_no,用于声明所使用的证书 tip:(商户P12证书的序列号)
    • 请求随机串nonce_str tip:(随机数,UUID或者雪花ID都可以)
    • 时间戳timestamp tip:(System.currentTimeMillis() / 1000)
    • 签名值signature tip:(SHA256签名)

提示

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

1、针对签名部分

下面方法是构造签名串和使用SHA256进行签名

    /**
     * @param requestType HTTP请求方法类型  GET\POST\PUT\DELETE
     * @param url        URL
     * @param timestamp  请求时间戳
     * @param nonceStr   请求随机串
     * @param body       请求报文主体 部分get请求是没有body请求体的
     */
    public static String getSignContent(String requestType, String url, long timestamp, String nonceStr, String body) {
        StringBuilder signContent = new StringBuilder();
        signContent.append(requestType).append("\n");
        signContent.append(url.substring(url.indexOf("/v3"))).append("\n");
        signContent.append(timestamp).append("\n");
        signContent.append(nonceStr).append("\n");
        if (StringUtils.isNotEmpty(body)) {
            signContent.append(body).append("\n");
        } else {
            signContent.append("\n");
        }
        return signContent.toString();
    }


    /**
     * 签名 
     * @param message -> getSignContent方法返回的内容
     * @param filePath -> 商户P12证书的路径
     * @param pwd -> 商户P12证书的密码
     * 签名串最后是返回Base64的
     */
    public static String sha256sign(String message, String filePath, String pwd) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(getPrivateKey(filePath, pwd));
        signature.update(message.getBytes(StringUtils.DEFAULT_CHARSET));
        return new String(Base64.encode(signature.sign()), StringUtils.DEFAULT_CHARSET);
    }
2、Authorization: 认证类型 签名信息

按照上面说的要求,把5部分的内容进行拼接,设置请求头中 Authorization 部分

		StringBuilder authorization = new StringBuilder();
        authorization.append("WECHATPAY2-SHA256-RSA2048 ");
        authorization.append("mchid=\"" + 商户号 + "\",");
        authorization.append("nonce_str=\"" + nonceStr + "\",");
        authorization.append("timestamp=\"" + timestamp + "\",");
        authorization.append("serial_no=\"" + getSerialno(keyPath, pwd) + "\",");
        authorization.append("signature=\"" + signature + "\"");
3、如果是post、put操作,需要设置请求体
//上面签名方法中的body内容放到httpEntity中
HttpEntity httpEntity = = new StringEntity(body, "UTF-8");
4、发送Post请求 PUT也一样(new HttpPut)
    /**
     * http post请求
     *
     * @param url           请求地址
     * @param httpEntity    请求内容
     * @param authorization 授权信息
     * @param headerList    请求头集合
     * @param pwd           P12证书密码
     * @param filePath      P12证书路径
     */
    public static JSONObject httpPost(String url, HttpEntity httpEntity, String authorization, String pwd, String filePath) throws Exception {
        
        InputStream inputStream = null;
        CloseableHttpClient httpClient = createSSLClientDefault();
        JSONObject jsonObj = new JSONObject();
        try {
            HttpPost httpPost = new HttpPost(url);
            // 请求头
            httpPost.addHeader("Content-Type", "application/json; charset=utf-8");
            httpPost.addHeader("Authorization", authorization);
            httpPost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727)");
            httpPost.addHeader("Accept", "application/json");
            
            // 请求体
            if (httpEntity != null) {
                httpPost.setEntity(httpEntity);
            }
            // 请求配置
            RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(15000)
                .setConnectTimeout(15000)
                .setConnectionRequestTimeout(15000)
                .build();
            
            httpPost.setConfig(requestConfig);

            HttpResponse response = httpClient.execute(httpPost);
            
            int statusCode = response.getStatusLine().getStatusCode();
            // 获取响应码
            jsonObj.put("statusCode", statusCode);
            Header[] headers = response.getAllHeaders();
            HttpEntity entity = response.getEntity();
            if (null != entity) {
                inputStream = entity.getContent();
                String responseJson = new String(IoUtils.read(inputStream, 1024), "utf-8");
                // 验签
                if (verify(headers, responseJson, merchantId, pwd, filePath)) {
                    // 获取响应内容
                    jsonObj.put("detail", responseJson);
                } else {
                    System.out.println("验签失败");
                    System.out.println(responseJson);
                }
            }
        } finally {
            IOUtils.closeQuietly(inputStream);
            httpClient.close();
        }
        
        return jsonObj;
    }
5、发送Get类请求 Delete也一样(new HttpDelete)
    public static JSONObject httpGet(String url, String authorization, String pwd, String filePath) throws Exception {
        
        InputStream inputStream = null;
        CloseableHttpClient httpClient = createSSLClientDefault();
        JSONObject jsonObj = new JSONObject();
        
        try {
            HttpGet httpGet = new HttpGet(url);
            
            httpGet.addHeader("Content-Type", "application/json; charset=utf-8");
            httpGet.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727)");
            httpGet.addHeader("Accept", "application/json");
            httpGet.addHeader("Authorization", authorization);

            RequestConfig requestConfig;
            requestConfig = RequestConfig.custom()
                .setSocketTimeout(15000)
                .setConnectTimeout(15000)
                .setConnectionRequestTimeout(15000)
                .build();
            httpGet.setConfig(requestConfig);
            
            HttpResponse response = httpClient.execute(httpGet);

            StatusLine statusLine = response.getStatusLine();
            int statusCode = statusLine.getStatusCode();

            jsonObj.put("statusCode", statusCode);
            
            Header[] headers = response.getAllHeaders();
            HttpEntity entity = response.getEntity();
            if (null != entity) {
                inputStream = entity.getContent();
                String responseJson = new String(IoUtils.read(inputStream, 1024), StringUtils.DEFAULT_CHARSET);
                
                if (verify(headers, responseJson, merchantId, pwd, filePath)) {
                    jsonObj.put("detail", responseJson);
                } else {
                    System.out.println("验签失败");
                    System.out.println(responseJson);
                }
            }
        } finally {
            IOUtils.closeQuietly(inputStream);
            httpClient.close();
        }
        
        return jsonObj;
    }

三.验签 使用平台证书,而不是商户证书

验签使用微信支付平台证书中的公钥验签。目前,微信支付平台证书仅提供 API 下载

下载平台证书对应的微信API地址:

https://pay.weixin.qq.com/docs/merchant/apis/platform-certificate/api-v3-get-certificates/get.html

在验证签名前,微信要求先检查 HTTP 头 Wechatpay-Serial 的内容是否跟商户当前所持有的微信支付平台证书的序列号一致。若不一致,请重新获取证书。否则,签名的私钥和证书不匹配,将验证失败。

//忽略getter、setter方法
public class EncryptCert {

    /**
     * 证书序列号
     */
    private String serialNo;
    /**
     * 证书生效时间
     */
    private String effectiveTime;
    /**
     * 证书失效时间
     */
    private String expireTime;
    /**
     * 证书使用的算法
     */
    private String algorithm;
    /**
     * 证书加密使用的随机串初始化向量
     */
    private String nonce;
    /**
     * 附加数据包(可能为空)
     */
    private String associatedData;
    /**
     * Base64编码后的密文
     */
    private String ciphertext;
    /**
     * 
     */
    private PublicKey publicKey;
    
}

所以需要调用下载证书接口,获取最新的证书,比对序列号,然后进行验签。

    /**
     * 验签
     *
     * @param headers 响应的请求头数组
     * @param message 响应
     * @param merchantId 商户号
     * @param pwd P12证书密码
     * @param filePath P12证书路径
     */
    pubilc static boolean verify(Header[] headers, String message, String merchantId, String pwd, String filePath) throws Exception {
        
        String timestamp = null;
        String nonce = null;
        String signature = null;
        String serial = null;
        
        for (Header header : headers) {
            String name = header.getName();
            String value = header.getValue();
            timestamp = name.equals("Wechatpay-Timestamp") ? value : timestamp;
            nonce = name.equals("Wechatpay-Nonce") ? value : nonce;
            signature = name.equals("Wechatpay-Signature") ? value : signature;
            serial = name.equals("Wechatpay-Serial") ? value : serial;
        }
        
        // 下载并比较序列号进行验签
        EncryptCert encryptCert = downloadCert(merchantId, filePath);
        
        // 当匹配到对应的服务商证书后,说明可以验签了
        if (serial.equalsIgnoreCase(encryptCert.getSerialNo())) {
            String signStr = timestamp + "\n" + nonce + "\n" + message + "\n";
            Signature instance = Signature.getInstance("SHA256withRSA");
            instance.initVerify(encryptCert.getPublicKey());
            instance.update(signStr.getBytes(StandardCharsets.UTF_8));
            boolean verify = instance.verify(Base64.decode(signature));
            if (verify) {
                System.out.println("验签成功!");
            } else {
                System.out.println("验签失败!!!");
            }
            return verify;
        }
        System.out.println("验签失败!!!");
        return false;
    }



    /**
     * 获取微信平台证书
     *
     * @return
     * @throws Exception
     */
    public static EncryptCert downloadCert(String merchantId, String filePath) throws CodeException {
        
        try {
            long timestamp = System.currentTimeMillis() / 1000;
            String nonceStr = UUID.randomUUID().toString();
            String signature = sha256sign(getSignContent("GET", downloadCertUrl, timestamp, nonceStr, ""), filePath, distributorID);
            String authorization = getAuthorization(timestamp, nonceStr, signature, merchantId, filePath, merchantId);
            
            // 通过Get请求获取证书内容,这块和Gget代码一致,只是没有验签方法
            JSONObject respJson = httpGetNotVer(downloadCertUrl, authorization, distributorID, filePath);
            System.out.println(respJson.toJSONString());
            
            if (respJson.isEmpty()) {
                throw new RunTimeException("公钥证书下载失败");
            }
            if (!"200".equals(StringUtils.trim(respJson.getString("statusCode")))) {
                throw new RunTimeException("公钥证书下载失败");
            }
            
            // 解析证书信息
            JSONObject detailJson = JSONObject.parseObject(StringUtils.trim(respJson.getString("detail")));
            JSONArray dataArray = JSONObject.parseArray(StringUtils.trim(detailJson.getString("data")));
            if (dataArray.isEmpty()) {
                throw new RunTimeException("公钥证书下载失败");
            }
            
            // 解析商户的所有证书
            List<EncryptCert> certList = getCertList(dataArray);
            // 获取商户最新的证书
            EncryptCert cert = getLastCert(certList);
            
            // 解密内容
            String decrypt = decryptToString(cert.getAssociatedData().getBytes(),
                    cert.getNonce().getBytes(), cert.getCiphertext());
            System.out.println("ciphertext解密内容:");
            System.out.println(decrypt);
            
            InputStream in = new ByteArrayInputStream(decrypt.getBytes("UTF-8"));
cert.setPublicKey(CertificateFactory.getInstance("X.509").generateCertificate(in).getPublicKey());
            return cert;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RunTimeException("公钥证书下载失败");
        }
    }



    /**
     * 解析渠道商下所有证书
     * @param dataArray 响应数据集
     */
    private static List<EncryptCert> getCertList(JSONArray dataArray) {
        int size = dataArray.size();
        List<EncryptCert> list = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            JSONObject certJson = JSONObject.parseObject(dataArray.get(i).toString());
            EncryptCert cert = new EncryptCert();
            cert.setSerialNo(certJson.getString("serial_no"));
            cert.setEffectiveTime(certJson.getString("effective_time"));
            cert.setExpireTime(certJson.getString("expire_time"));

            JSONObject encryptCertJson = JSONObject.parseObject(certJson.getString("encrypt_certificate"));
            cert.setAlgorithm(encryptCertJson.getString("algorithm"));
            cert.setNonce(encryptCertJson.getString("nonce"));
            cert.setAssociatedData(encryptCertJson.getString("associated_data"));
            cert.setCiphertext(encryptCertJson.getString("ciphertext"));
            list.add(cert);
        }
        return list;
    }

    /**
     * 获取最新的证书
     * @param certList 证书集
     */
    private static EncryptCert getLastCert(List<EncryptCert> certList) {
        int size = certList.size();
        if (size == 1) {
            return certList.get(0);
        }
        int index = 0;
        String effectiveTime = certList.get(0).getEffectiveTime();
        for (int i = 1; i < size; i++) {
            // 取值规则:生效时间最大的
            if (effectiveTime.compareTo(certList.get(i).getEffectiveTime()) < 0) {
                effectiveTime = certList.get(i).getEffectiveTime();
                index = i;
            }
        }
        return certList.get(index);
    }

    /**
     * 微信对密文进行解密
     * @param associatedData
     * @param nonce
     * @param ciphertext
     */
    public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws Exception {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(BankAT511B.apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            return new String(cipher.doFinal(Base64.decode(ciphertext)), StandardCharsets.UTF_8);
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }

四.总结

以上就是对微信支付接入后,签名、验签、解密的所有内容,demo就不放出来了,上面所表述的基本就是核心部分,

总体来看:

1、获取商户号
2、获取商户对应的私钥
3、签名,有格式要求,请求头中设置
4、涉及加密内容,需要借助平台公钥进行加密
5、发送对应的POST/GET/PUT/DELETE 请求
6、下载平台证书进行验签(这个公钥也是加解密用的)
7、解密数据

纯手打内容,如果涉及部分不正确,或者代码粘贴下来有问题,自行阅读处理一下就行,请大家理解哈~

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值