易宝支付SDK自有业务适应性修改

最近在对接易宝支付,但是易宝支付的文档指定使用他们的SDK进行所有的操作。但一般的系统,相关参数为了安全考虑,都是自己从配置中心或者应用中心获取的。所以,我们可以继承他们的 AbstractClient 类,模仿他们的RequestClient3 类,自己去实现一下,然后自行配置加入日志组件,这样,可以更准确的定位问题和看到报文等信息。以下是,我自己实现的类。

关于如何使用:

还是使用他们的YeeRequest请求类,初始化类的时候,把AppKey和分配的私钥初始化进去,即可。

import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.yeepay.g3.sdk.yop.client.*;
import com.yeepay.g3.sdk.yop.config.AppSdkConfig;
import com.yeepay.g3.sdk.yop.encrypt.CertTypeEnum;
import com.yeepay.g3.sdk.yop.encrypt.DigestAlgEnum;
import com.yeepay.g3.sdk.yop.encrypt.DigitalSignatureDTO;
import com.yeepay.g3.sdk.yop.exception.YopClientException;
import com.yeepay.g3.sdk.yop.http.Headers;
import com.yeepay.g3.sdk.yop.http.HttpUtils;
import com.yeepay.g3.sdk.yop.unmarshaller.JacksonJsonMarshaller;
import com.yeepay.g3.sdk.yop.utils.*;
import com.yeepay.g3.sdk.yop.utils.checksum.CRC64Utils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
import java.net.URLEncoder;
import java.util.concurrent.TimeUnit;

public class YeePayRequestClient extends AbstractClient {

    private static final Logger logger = LoggerFactory.getLogger(YeePayRequestClient.class);

    private static CloseableHttpClient httpClient;
    private static final Set<String> defaultHeadersToSign = Sets.newHashSet();
    private static final Joiner headerJoiner = Joiner.on('\n');
    private static final Joiner signedHeaderStringJoiner = Joiner.on(';');
    private static final String EXPIRED_SECONDS = "1800";

    static {
        // TODO: 2018/11/8 以下两行为请求参数配置
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(InternalConfig.READ_TIMEOUT)
                .setConnectTimeout(InternalConfig.CONNECT_TIMEOUT).build();
        httpClient = HttpClientBuilder.create().setMaxConnTotal(InternalConfig.MAX_CONN_TOTAL)
                .setMaxConnPerRoute(InternalConfig.MAX_CONN_PER_ROUTE)
                .setSSLSocketFactory(InternalConfig.TRUST_ALL_CERTS ? getTrustedAllSSLConnectionSocketFactory() : null)
                .setDefaultRequestConfig(requestConfig).evictExpiredConnections()
                .evictIdleConnections(5000L, TimeUnit.MILLISECONDS).setRetryHandler(new YopHttpRequestRetryHandler()).build();
    }

    public static YopResponse postRsa(String apiUri, YopRequest request) throws IOException {
        String contentUrl = richRequest(apiUri, request);
        sign(apiUri, request);

        RequestBuilder requestBuilder = RequestBuilder.post().setUri(contentUrl);
        for (Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
            requestBuilder.addHeader(entry.getKey(), entry.getValue());
        }
        for (Map.Entry<String, String> entry : request.getParams().entries()) {
            requestBuilder.addParameter(entry.getKey(), URLEncoder.encode(entry.getValue(), YopConstants.ENCODING));
        }

        HttpUriRequest httpPost = requestBuilder.build();
        YopResponse response = fetchContentByApacheHttpClient(httpPost);
        handleRsaResult(response, request.getAppSdkConfig());
        return response;
    }

    private static void sign(String apiUri, YopRequest request) {
        String appKey = request.getAppSdkConfig().getAppKey();
        String timestamp = DateUtils.formatCompressedIso8601Timestamp(System.currentTimeMillis());

        Map<String, String> headers = request.getHeaders();
        if (!headers.containsKey(Headers.YOP_REQUEST_ID)) {
            headers.put(Headers.YOP_REQUEST_ID, getUUID());
            headers.put(Headers.YOP_SESSION_ID, SESSION_ID);
        }
        headers.put(Headers.YOP_DATE, timestamp);
        if (request.hasFiles()) {
            try {
                request.addHeader(Headers.YOP_HASH_CRC64ECMA, CRC64Utils.calculateMultiPartFileCrc64ecma(request.getMultipartFiles()));
            } catch (IOException ex) {
                logger.error("IOException occurred when generate crc64ecma.", ex);
                throw new YopClientException("IOException occurred when generate crc64ecma.", ex);
            }
        }

        String authString = InternalConfig.PROTOCOL_VERSION + "/" + appKey + "/" + timestamp + "/" + EXPIRED_SECONDS;

        Set<String> headersToSignSet = new HashSet<String>();
        headersToSignSet.add(Headers.YOP_REQUEST_ID);
        headersToSignSet.add(Headers.YOP_DATE);

        headers.put(Headers.YOP_APP_KEY, appKey);
        headersToSignSet.add(Headers.YOP_APP_KEY);

        String canonicalURI = HttpUtils.getCanonicalURIPath(apiUri);
        String canonicalQueryString = HttpUtils.getCanonicalQueryString(request.getParams(), true);
        SortedMap<String, String> headersToSign = getHeadersToSign(headers, headersToSignSet);
        String canonicalHeader = getCanonicalHeaders(headersToSign);
        String signedHeaders = "";
        if (headersToSignSet != null) {
            signedHeaders = signedHeaderStringJoiner.join(headersToSign.keySet());
            signedHeaders = signedHeaders.trim().toLowerCase();
        }

        String canonicalRequest = authString + "\n" + "POST" + "\n" + canonicalURI + "\n" + canonicalQueryString + "\n" + canonicalHeader;

        PrivateKey isvPrivateKey;
        if (StringUtils.length(request.getSecretKey()) > 128) {
            try {
                isvPrivateKey = RSAKeyUtils.string2PrivateKey(request.getSecretKey());
            } catch (NoSuchAlgorithmException e) {
                throw Exceptions.unchecked(e);
            } catch (InvalidKeySpecException e) {
                throw Exceptions.unchecked(e);
            }
        } else {
            isvPrivateKey = request.getAppSdkConfig().getDefaultIsvPrivateKey();
        }
        if (null == isvPrivateKey) {
            throw new YopClientException("Can't init ISV private key!");
        }

        DigitalSignatureDTO digitalSignatureDTO = new DigitalSignatureDTO();
        digitalSignatureDTO.setPlainText(canonicalRequest);
        digitalSignatureDTO.setCertType(CertTypeEnum.RSA2048);
        digitalSignatureDTO.setDigestAlg(DigestAlgEnum.SHA256);
        digitalSignatureDTO = DigitalEnvelopeUtils.sign(digitalSignatureDTO, isvPrivateKey);
        if (logger.isDebugEnabled()) {
            logger.debug("canonicalRequest:" + canonicalRequest);
            logger.debug("signature:" + digitalSignatureDTO.getSignature());
        }

        headers.put(Headers.AUTHORIZATION, "YOP-RSA2048-SHA256 " + InternalConfig.PROTOCOL_VERSION + "/" + appKey + "/" + timestamp + "/" + EXPIRED_SECONDS + "/" + signedHeaders + "/" + digitalSignatureDTO.getSignature());
    }


    /**
     * 对业务结果签名进行校验
     */
    public static boolean verifySignature(String result, String expectedSign, AppSdkConfig appSdkConfig) {
        String trimmedBizResult = result.replaceAll("[ \t\n]", "");

        StringBuilder sb = new StringBuilder();
        sb.append(StringUtils.trimToEmpty(trimmedBizResult));

        DigitalSignatureDTO digitalSignatureDTO = new DigitalSignatureDTO();
        digitalSignatureDTO.setCertType(CertTypeEnum.RSA2048);
        digitalSignatureDTO.setSignature(expectedSign);
        digitalSignatureDTO.setPlainText(sb.toString());

        try {
            DigitalEnvelopeUtils.verify(digitalSignatureDTO, appSdkConfig.getDefaultYopPublicKey());
        } catch (Exception e) {
            logger.error("error verify sign", e);
            return false;
        }
        return true;
    }

    private static String getCanonicalHeaders(SortedMap<String, String> headers) {
        if (headers.isEmpty()) {
            return "";
        }

        List<String> headerStrings = Lists.newArrayList();
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String key = entry.getKey();
            if (key == null) {
                continue;
            }
            String value = entry.getValue();
            if (value == null) {
                value = "";
            }
            headerStrings.add(HttpUtils.normalize(key.trim().toLowerCase()) + ':' + HttpUtils.normalize(value.trim()));
        }
        Collections.sort(headerStrings);

        return headerJoiner.join(headerStrings);
    }

    private static SortedMap<String, String> getHeadersToSign(Map<String, String> headers, Set<String> headersToSign) {
        SortedMap<String, String> ret = Maps.newTreeMap();
        if (headersToSign != null) {
            Set<String> tempSet = Sets.newHashSet();
            for (String header : headersToSign) {
                tempSet.add(header.trim().toLowerCase());
            }
            headersToSign = tempSet;
        }
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String key = entry.getKey();
            if (entry.getValue() != null && !entry.getValue().isEmpty()) {
                if ((headersToSign == null && isDefaultHeaderToSign(key))
                        || (headersToSign != null && headersToSign.contains(key.toLowerCase())
                        && !Headers.AUTHORIZATION.equalsIgnoreCase(key))) {
                    ret.put(key, entry.getValue());
                }
            }
        }
        return ret;
    }

    private static void handleRsaResult(YopResponse response, AppSdkConfig appSdkConfig) {
        String stringResult = response.getStringResult();
        if (StringUtils.isNotBlank(stringResult)) {
            response.setResult(JacksonJsonMarshaller.unmarshal(stringResult, Object.class));
        }

        String sign = response.getSign();
        if (StringUtils.isNotBlank(sign)) {
            response.setValidSign(verifySignature(stringResult, sign, appSdkConfig));
        }
    }

    private static boolean isDefaultHeaderToSign(String header) {
        header = header.trim().toLowerCase();
        return header.startsWith(Headers.YOP_PREFIX) || defaultHeadersToSign.contains(header);
    }

    protected static YopResponse fetchContentByApacheHttpClient(HttpUriRequest request) throws IOException {

        CloseableHttpResponse remoteResponse = httpClient.execute(request);
        Object var2 = null;

        YopResponse var7;
        try {
            int statusCode = remoteResponse.getStatusLine().getStatusCode();
            if (statusCode >= 400) {
                throw new YopClientException(Integer.toString(statusCode));
            }

            String content = EntityUtils.toString(remoteResponse.getEntity());
            YopResponse response = (YopResponse)JacksonJsonMarshaller.unmarshal(content, YopResponse.class);
            Header requestIdHeader = remoteResponse.getFirstHeader("x-yop-request-id");
            if (null != requestIdHeader) {
                response.setRequestId(requestIdHeader.getValue());
            }

            var7 = response;
        } finally {
            HttpClientUtils.closeQuietly(remoteResponse);
        }

        return var7;
    }

    private static SSLConnectionSocketFactory getTrustedAllSSLConnectionSocketFactory() {
        logger.warn("[yop-sdk]已设置信任所有证书。仅供内测使用,请勿在生产环境配置。");
        SSLConnectionSocketFactory sslConnectionSocketFactory = null;

        try {
            SSLContextBuilder builder = new SSLContextBuilder();
            builder.loadTrustMaterial((KeyStore)null, new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    return true;
                }
            });
            sslConnectionSocketFactory = new SSLConnectionSocketFactory(builder.build());
        } catch (Exception var2) {
            logger.error("error when get trust-all-certs request factory,will return normal request factory instead", var2);
        }

        return sslConnectionSocketFactory;
    }
}

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据