IJPay微信企业付款到零钱协议不正确 No appropriate protocol

工作 / 2022-02-22

IJPay微信企业付款到零钱协议不正确 No appropriate protocol

问题发现

项目中使用了微信企业付款到零钱的功能,因为自己去封装过于麻烦,就使用了LJpay,原本使用的是jdk11的环境去运行一切正常,但是因为云托管没有固定的IP所以转移到了公司服务器,公司服务器的环境是jdk8,所以我把环境换到了jdk8之后报了如下错误

Servlet.service() for servlet [dispatcherServlet] in context with path [/server] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: cn.hutool.core.io.IORuntimeException: SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)] with root cause

javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

问题研究

经过在各搜索引擎搜索,网上也有出现相同错误的,原因很直接,jdk的版本原因,这也是和我的猜想一样,因为只有环境变了代码没有做出改变。很多答案都是让我去把useSSL给禁用了了,更有甚者 让我去\jdk安装包目录下的\jre\lib\security中的java.security文件,将对应的SSLv3删除了,还有它后缀的两个一样的算法一起删除保存再重启服务,很明显这不现实,因为服务器里面跑着另一个服务。

解决方案

然后我再去查找LJpay相关的这个问题的话题只找到了微信退款协议不正确。随后我去看LJpay的源码。发现 transfers 在AbstractHttpDelegate里面的post是写死的 SSLSocketFactoryBuilder.TLSv1 的协议,想到两个解决方法,一个删除环境安装包的配置(否决了),还有一个是禁用了useSSL,发现作者写的LJpay的WxpayApi里面提供了自定义协议的方法transfersByProtocol。

    /**
     * 企业付款到零钱
     *
     * @param params   请求参数
     * @param certFile 证书文件的 InputStream
     * @param certPass 证书密码
     * @return {@link String} 请求返回的结果
     */
    public static String transfers(Map<String, String> params, InputStream certFile, String certPass) {
        return execution(getReqUrl(WxApiType.TRANSFER, null, false), params, certFile, certPass);
    }

    /**
     * 企业付款到零钱
     *
     * @param params   请求参数
     * @param certFile 证书文件的 InputStream
     * @param certPass 证书密码
     * @param protocol 协议
     * @return {@link String} 请求返回的结果
     */
    public static String transfersByProtocol(Map<String, String> params, InputStream certFile, String certPass, String protocol) {
        return executionByProtocol(getReqUrl(WxApiType.TRANSFER, null, false), params, certFile, certPass, protocol);
    }

适配优化

但是 我发现我这里就使用到了企业付款到零钱这个功能,而且我个人觉得LJpay封装了太多,为了减少打包的重量,我决定重新实现LJpay企业付款到零钱这个功能。

        // 自己的业务代码
        String refundStr = post(WxPayApi.getReqUrl(WxApiType.TRANSFER
                , null, false), WxPayKit.toXml(params), stream, wxPayV3Bean.getMchId());


// 实际调用AbstractHttpDelegate类中的方法
    public String post(String url, String data, InputStream certFile, String certPass) {
        try {
            return HttpRequest
                    .post(url)
                    .setSSLSocketFactory(SSLSocketFactoryBuilder.create()
                            .setProtocol("")
                            .setKeyManagers(this.getKeyManager(certPass, null, certFile))
                            .setSecureRandom(new SecureRandom())
                            .build())
                    .body(data)
                    .execute()
                    .body();
        } catch (Exception var7) {
            throw new RuntimeException(var7);
        }
    }

    private KeyManager[] getKeyManager(String certPass, String certPath, InputStream certFile) throws Exception {
        KeyStore clientStore = KeyStore.getInstance("PKCS12");
        if (certFile != null) {
            clientStore.load(certFile, certPass.toCharArray());
        } else {
            clientStore.load(new FileInputStream(certPath), certPass.toCharArray());
        }
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(clientStore, certPass.toCharArray());
        return kmf.getKeyManagers();
    }

// 重写生成模板类,因为很多方法都抽象的继承了,为了方便直接调用。

/**
 * @Description:
 * 重写的这个类主要有两个功能
 *    1. 通过Builder创建Map。
 *    2. 排序并参与字符拼接的参数组,来实现签名算法
 *    3. 构造签名Map加入参数sign
 * @author: zwy
 * @date: 2022年02月22日 15:23
 */
@Builder
@AllArgsConstructor
@Getter
@Setter
public class TransferModel {
    private String mch_appid;
    private String mchid;
    private String device_info;
    private String nonce_str;
    private String sign;
    private String partner_trade_no;
    private String openid;
    private String check_name;
    private String re_user_name;
    private String amount;
    private String desc;
    private String spbill_create_ip;

    //************************继承BaseModel实现类****************************************
    /**
     * 将建构的 builder 转为 Map
     *
     * @return 转化后的 Map
     */
    public Map<String, String> toMap() {
        String[] fieldNames = getFiledNames(this);
        HashMap<String, String> map = new HashMap<String, String>(fieldNames.length);
        for (String name : fieldNames) {
            String value = (String) getFieldValueByName(name, this);
            if (StrUtil.isNotEmpty(value)) {
                map.put(name, value);
            }
        }
        return map;
    }

    /**
     * 根据属性名获取属性值
     *
     * @param fieldName 属性名称
     * @param obj       对象
     * @return 返回对应属性的值
     */
    public Object getFieldValueByName(String fieldName, Object obj) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = new StringBuffer().append("get")
                    .append(firstLetter)
                    .append(fieldName.substring(1))
                    .toString();
            Method method = obj.getClass().getMethod(getter);
            return method.invoke(obj);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取属性名数组
     *
     * @param obj 对象
     * @return 返回对象属性名数组
     */
    public String[] getFiledNames(Object obj) {
        Field[] fields = obj.getClass().getDeclaredFields();
        String[] fieldNames = new String[fields.length];
        for (int i = 0; i < fields.length; i++) {
            fieldNames[i] = fields[i].getName();
        }
        return fieldNames;
    }



    //************************ 排序并参与字符拼接的参数组 ****************************************

    /*形参:
    params – 需要排序并参与字符拼接的参数组
    connStr – 连接符号
    encode – 是否进行URLEncoder
    返回值:
    拼接后字符串*/
    public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) {
        List<String> keys = new ArrayList<>(params.keySet());
        Collections.sort(keys);
        StringBuilder content = new StringBuilder();
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);
            // 拼接时,不包括最后一个&字符
            if (i == keys.size() - 1) {
                if (quotes) {
                    content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"');
                } else {
                    content.append(key).append("=").append(encode ? urlEncode(value) : value);
                }
            } else {
                if (quotes) {
                    content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr);
                } else {
                    content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr);
                }
            }
        }
        return content.toString();
    }

    /**
     * URL 编码
     *
     * @param src 需要编码的字符串
     * @return 编码后的字符串
     */
    public static String urlEncode(String src) {
        try {
            return URLEncoder.encode(src, CharsetUtil.UTF_8).replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }


    //************************ 构造签名Map ****************************************

    /**
     * 构建签名 Map
     *
     * @param partnerKey   API KEY
     * @return 构建签名后的 Map
     */
    public Map<String, String> createSign(String partnerKey) {
        return buildSign(toMap(), partnerKey);
    }
    /**
     * 构建签名
     *
     * @param params     需要签名的参数
     * @param partnerKey 密钥
     * @return 签名后的 Map
     */
    public static Map<String, String> buildSign(Map<String, String> params, String partnerKey) {
        String sign = createSign(params, partnerKey);
        params.put("sign", sign);
        return params;
    }

    /**
     * 生成签名
     *
     * @param params 需要签名的参数
     * @param partnerKey 密钥
     * @return 签名后的数据
     */
    public static String createSign(Map<String, String> params, String partnerKey) {
        // 生成签名前先去除sign
        params.remove("sign");
        String tempStr = createLinkString(params, "&", false, false);
        String stringSignTemp = tempStr + "&key=" + partnerKey;
        return SecureUtil.md5(stringSignTemp).toUpperCase();
    }
}