添加网站文件

This commit is contained in:
2025-12-22 13:59:40 +08:00
commit 117aaf83d1
19468 changed files with 2111999 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alipay.sdk</groupId>
<artifactId>easysdk-kernel</artifactId>
<version>1.0.8</version>
<name>Alipay Easy SDK Kernel</name>
<url>https://open.alipay.com</url>
<description>Alipay Easy SDK for Java
allows you to enjoy a minimalist programming experience
and quickly access the various high-frequency capabilities of the Alipay Open Platform.
</description>
<repositories>
<repository>
<id>mvnrepository</id>
<name>mvnrepository</name>
<url>http://www.mvnrepository.com/</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea</artifactId>
<version>1.0.7</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.64</version>
</dependency>
</dependencies>
<distributionManagement>
<snapshotRepository>
<id>sonatype-nexus-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>sonatype-nexus-staging</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<licenses>
<license>
<name>MIT License</name>
<url>http://www.opensource.org/licenses/mit-license.php</url>
</license>
</licenses>
<scm>
<connection>scm:git:git@github.com:alipay/alipay-easysdk.git</connection>
<developerConnection>scm:git:ssh://github.com:alipay/alipay-easysdk.git</developerConnection>
<url>http://github.com/alipay/alipay-easysdk/tree/master/java</url>
</scm>
<developers>
<developer>
<id>antopen</id>
<name>antopen</name>
<email>antopen@aliyun.com</email>
</developer>
</developers>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.2</version>
<configuration>
</configuration>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
<executions>
<execution>
<id>default-deploy</id>
<phase>deploy</phase>
<goals>
<goal>deploy</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-scm-plugin</artifactId>
<version>1.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1.2</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<aggregate>true</aggregate>
<charset>UTF-8</charset>
<encoding>UTF-8</encoding>
<docencoding>UTF-8</docencoding>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,75 @@
/**
* Alipay.com Inc. Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* 支付宝开放平台网关交互常用常量
*
* @author zhongyu
* @version $Id: AlipayConstants.java, v 0.1 2020年01月02日 7:53 PM zhongyu Exp $
*/
public final class AlipayConstants {
/**
* Config配置参数Key值
*/
public static final String PROTOCOL_CONFIG_KEY = "protocol";
public static final String HOST_CONFIG_KEY = "gatewayHost";
public static final String ALIPAY_CERT_PATH_CONFIG_KEY = "alipayCertPath";
public static final String MERCHANT_CERT_PATH_CONFIG_KEY = "merchantCertPath";
public static final String ALIPAY_ROOT_CERT_PATH_CONFIG_KEY = "alipayRootCertPath";
public static final String SIGN_TYPE_CONFIG_KEY = "signType";
public static final String NOTIFY_URL_CONFIG_KEY = "notifyUrl";
public static final String SIGN_PROVIDER_CONFIG_KEY = "signProvider";
/**
* 与网关HTTP交互中涉及到的字段值
*/
public static final String BIZ_CONTENT_FIELD = "biz_content";
public static final String ALIPAY_CERT_SN_FIELD = "alipay_cert_sn";
public static final String SIGN_FIELD = "sign";
public static final String SIGN_TYPE_FIELD = "sign_type";
public static final String BODY_FIELD = "http_body";
public static final String NOTIFY_URL_FIELD = "notify_url";
public static final String METHOD_FIELD = "method";
public static final String RESPONSE_SUFFIX = "_response";
public static final String ERROR_RESPONSE = "error_response";
/**
* 默认字符集编码EasySDK统一固定使用UTF-8编码无需用户感知编码用户面对的总是String而不是bytes
*/
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
/**
* 默认的签名算法EasySDK统一固定使用RSA2签名算法即SHA_256_WITH_RSA但此参数依然需要用户指定以便用户感知因为在开放平台接口签名配置界面中需要选择同样的算法
*/
public static final String RSA2 = "RSA2";
/**
* RSA2对应的真实签名算法名称
*/
public static final String SHA_256_WITH_RSA = "SHA256WithRSA";
/**
* RSA2对应的真实非对称加密算法名称
*/
public static final String RSA = "RSA";
/**
* 申请生成的重定向网页的请求类型GET表示生成URL
*/
public static final String GET = "GET";
/**
* 申请生成的重定向网页的请求类型POST表示生成form表单
*/
public static final String POST = "POST";
/**
* 使用Aliyun KMS签名服务时签名提供方的名称
*/
public static final String AliyunKMS = "AliyunKMS";
}

View File

@@ -0,0 +1,82 @@
/**
* Alipay.com Inc. Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel;
import com.alipay.easysdk.kernel.util.AntCertificationUtil;
import com.google.common.base.Strings;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 证书模式运行时环境
*
* @author zhongyu
* @version $Id: CertEnvironment.java, v 0.1 2020年01月02日 5:21 PM zhongyu Exp $
*/
public class CertEnvironment {
/**
* 支付宝根证书内容
*/
private String rootCertContent;
/**
* 支付宝根证书序列号
*/
private String rootCertSN;
/**
* 商户应用公钥证书序列号
*/
private String merchantCertSN;
/**
* 缓存的不同支付宝公钥证书序列号对应的支付宝公钥
*/
private Map<String, String> cachedAlipayPublicKey = new ConcurrentHashMap<String, String>();
/**
* 构造证书运行环境
*
* @param merchantCertPath 商户公钥证书路径
* @param alipayCertPath 支付宝公钥证书路径
* @param alipayRootCertPath 支付宝根证书路径
*/
public CertEnvironment(String merchantCertPath, String alipayCertPath, String alipayRootCertPath) {
if (Strings.isNullOrEmpty(merchantCertPath) || Strings.isNullOrEmpty(alipayCertPath) || Strings.isNullOrEmpty(alipayCertPath)) {
throw new RuntimeException("证书参数merchantCertPath、alipayCertPath或alipayRootCertPath设置不完整。");
}
this.rootCertContent = AntCertificationUtil.readCertContent(alipayRootCertPath);
this.rootCertSN = AntCertificationUtil.getRootCertSN(rootCertContent);
this.merchantCertSN = AntCertificationUtil.getCertSN(AntCertificationUtil.readCertContent((merchantCertPath)));
String alipayPublicCertContent = AntCertificationUtil.readCertContent(alipayCertPath);
cachedAlipayPublicKey.put(AntCertificationUtil.getCertSN(alipayPublicCertContent),
AntCertificationUtil.getCertPublicKey(alipayPublicCertContent));
}
public String getRootCertSN() {
return rootCertSN;
}
public String getMerchantCertSN() {
return merchantCertSN;
}
public String getAlipayPublicKey(String sn) {
//如果没有指定sn则默认取缓存中的第一个值
if (Strings.isNullOrEmpty(sn)) {
return cachedAlipayPublicKey.values().iterator().next();
}
if (cachedAlipayPublicKey.containsKey(sn)) {
return cachedAlipayPublicKey.get(sn);
} else {
//网关在支付宝公钥证书变更前,一定会确认通知到商户并在商户做出反馈后,才会更新该商户的支付宝公钥证书
//TODO: 后续可以考虑加入自动升级支付宝公钥证书逻辑,注意并发更新冲突问题
throw new RuntimeException("支付宝公钥证书[" + sn + "]已过期,请重新下载最新支付宝公钥证书并替换原证书文件");
}
}
}

View File

@@ -0,0 +1,476 @@
// This file is auto-generated, don't edit it. Thanks.
package com.alipay.easysdk.kernel;
import com.alipay.easysdk.kernel.util.AES;
import com.alipay.easysdk.kernel.util.JsonUtil;
import com.alipay.easysdk.kernel.util.MultipartUtil;
import com.alipay.easysdk.kernel.util.PageUtil;
import com.alipay.easysdk.kernel.util.SignContentExtractor;
import com.alipay.easysdk.kernel.util.Signer;
import com.aliyun.tea.TeaResponse;
import com.google.common.base.Strings;
import com.google.common.io.Files;
import com.google.gson.Gson;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.TreeMap;
public class Client {
/**
* 构造成本较高的一些参数缓存在上下文中
*/
private final Context context;
/**
* 注入的可选额外文本参数集合
*/
private final Map<String, String> optionalTextParams = new HashMap<>();
/**
* 注入的可选业务参数集合
*/
private final Map<String, Object> optionalBizParams = new HashMap<>();
/**
* 构造函数
*
* @param context 上下文对象
*/
public Client(Context context) {
this.context = context;
}
/**
* 注入额外文本参数
*
* @param key 参数名称
* @param value 参数的值
* @return 本客户端本身,便于链路调用
*/
public Client injectTextParam(String key, String value) {
optionalTextParams.put(key, value);
return this;
}
/**
* 注入额外业务参数
*
* @param key 业务参数名称
* @param value 业务参数的值
* @return 本客户端本身,便于链式调用
*/
public Client injectBizParam(String key, Object value) {
optionalBizParams.put(key, value);
return this;
}
/**
* 获取时间戳格式yyyy-MM-dd HH:mm:ss
*
* @return 当前时间戳
*/
public String getTimestamp() throws Exception {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("GMT+8"));
return df.format(new Date());
}
/**
* 获取Config中的配置项
*
* @param key 配置项的名称
* @return 配置项的值
*/
public String getConfig(String key) throws Exception {
return context.getConfig(key);
}
/**
* 获取SDK版本信息
*
* @return SDK版本信息
*/
public String getSdkVersion() throws Exception {
return context.getSdkVersion();
}
/**
* 将业务参数和其他额外文本参数按www-form-urlencoded格式转换成HTTP Body中的字节数组注意要做URL Encode
*
* @param bizParams 业务参数
* @return HTTP Body中的字节数组
*/
public byte[] toUrlEncodedRequestBody(java.util.Map<String, ?> bizParams) throws Exception {
Map<String, String> sortedMap = getSortedMap(Collections.<String, String>emptyMap(), bizParams, null);
return buildQueryString(sortedMap).getBytes(AlipayConstants.DEFAULT_CHARSET);
}
/**
* 将网关响应发序列化成Map同时将API的接口名称和响应原文插入到响应Map的method和body字段中
*
* @param response HTTP响应
* @param method 调用的OpenAPI的接口名称
* @return 响应反序列化的Map
*/
public java.util.Map<String, Object> readAsJson(TeaResponse response, String method) throws Exception {
String responseBody = response.getResponseBody();
Map map = new Gson().fromJson(responseBody, Map.class);
map.put(AlipayConstants.BODY_FIELD, responseBody);
map.put(AlipayConstants.METHOD_FIELD, method);
return map;
}
/**
* 从响应Map中提取返回值对象的Map并将响应原文插入到body字段中
*
* @param respMap 响应Map
* @return 返回值对象Map
*/
public java.util.Map<String, Object> toRespModel(java.util.Map<String, Object> respMap) throws Exception {
String methodName = (String) respMap.get(AlipayConstants.METHOD_FIELD);
String responseNodeName = methodName.replace('.', '_') + AlipayConstants.RESPONSE_SUFFIX;
String errorNodeName = AlipayConstants.ERROR_RESPONSE;
//先找正常响应节点
for (Entry<String, Object> pair : respMap.entrySet()) {
if (responseNodeName.equals(pair.getKey())) {
Map<String, Object> model = (Map<String, Object>) pair.getValue();
model.put(AlipayConstants.BODY_FIELD, respMap.get(AlipayConstants.BODY_FIELD));
return model;
}
}
//再找异常响应节点
for (Entry<String, Object> pair : respMap.entrySet()) {
if (errorNodeName.equals(pair.getKey())) {
Map<String, Object> model = (Map<String, Object>) pair.getValue();
model.put(AlipayConstants.BODY_FIELD, respMap.get(AlipayConstants.BODY_FIELD));
return model;
}
}
throw new RuntimeException("响应格式不符合预期,找不到" + responseNodeName + "" + errorNodeName + "节点");
}
/**
* 生成随机分界符用于multipart格式的HTTP请求Body的多个字段间的分隔
*
* @return 随机分界符
*/
public String getRandomBoundary() throws Exception {
return System.currentTimeMillis() + "";
}
/**
* 将其他额外文本参数和文件参数按multipart/form-data格式转换成HTTP Body中的字节数组流
*
* @param textParams 其他额外文本参数
* @param fileParams 业务文件参数
* @param boundary HTTP Body中multipart格式的分隔符
* @return Multipart格式的字节流
*/
public java.io.InputStream toMultipartRequestBody(java.util.Map<String, String> textParams,
java.util.Map<String, String> fileParams, String boundary) throws Exception {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
//补充其他额外参数
addOtherParams(textParams, null);
for (Entry<String, String> pair : textParams.entrySet()) {
if (!Strings.isNullOrEmpty(pair.getKey()) && !Strings.isNullOrEmpty(pair.getValue())) {
stream.write(MultipartUtil.getEntryBoundary(boundary));
stream.write(MultipartUtil.getTextEntry(pair.getKey(), pair.getValue()));
}
}
//组装文件参数
for (Entry<String, String> pair : fileParams.entrySet()) {
if (!Strings.isNullOrEmpty(pair.getKey()) && pair.getValue() != null) {
stream.write(MultipartUtil.getEntryBoundary(boundary));
stream.write(MultipartUtil.getFileEntry(pair.getKey(), pair.getValue()));
stream.write(Files.toByteArray(new File(pair.getValue())));
}
}
//添加结束标记
stream.write(MultipartUtil.getEndBoundary(boundary));
return new ByteArrayInputStream(stream.toByteArray());
}
private void addOtherParams(Map<String, String> textParams, Map<String, ?> bizParams) throws Exception {
//为null表示此处不是扩展此类参数的时机
if (textParams != null) {
for (Entry<String, String> pair : optionalTextParams.entrySet()) {
if (!textParams.containsKey(pair.getKey())) {
textParams.put(pair.getKey(), pair.getValue());
}
}
setNotifyUrl(textParams);
}
//为null表示此处不是扩展此类参数的时机
if (bizParams != null) {
for (Entry<String, Object> pair : optionalBizParams.entrySet()) {
if (!bizParams.containsKey(pair.getKey())) {
((Map<String, Object>) bizParams).put(pair.getKey(), pair.getValue());
}
}
}
}
/**
* 生成页面类请求所需URL或Form表单
*
* @param method GET或POST决定是生成URL还是Form表单
* @param systemParams 系统参数集合
* @param bizParams 业务参数集合
* @param textParams 其他额外文本参数集合
* @param sign 所有参数的签名值
* @return 生成的URL字符串或表单
*/
public String generatePage(String method, java.util.Map<String, String> systemParams, java.util.Map<String, ?> bizParams,
java.util.Map<String, String> textParams, String sign) throws Exception {
if (AlipayConstants.GET.equalsIgnoreCase(method)) {
//采集并排序所有参数
Map<String, String> sortedMap = getSortedMap(systemParams, bizParams, textParams);
sortedMap.put(AlipayConstants.SIGN_FIELD, sign);
//将所有参数置于URL中
return getGatewayServerUrl() + "?" + buildQueryString(sortedMap);
} else if (AlipayConstants.POST.equalsIgnoreCase(method)) {
//将系统参数、额外文本参数排序后置于URL中
Map<String, String> urlParams = getSortedMap(systemParams, null, textParams);
urlParams.put(AlipayConstants.SIGN_FIELD, sign);
String actionUrl = getGatewayServerUrl() + "?" + buildQueryString(urlParams);
//将业务参数置于form表单中
addOtherParams(null, bizParams);
Map<String, String> formParams = new TreeMap<>();
formParams.put(AlipayConstants.BIZ_CONTENT_FIELD, JsonUtil.toJsonString(bizParams));
return PageUtil.buildForm(actionUrl, formParams);
} else {
throw new RuntimeException("_generatePage中method只支持传入GET或POST");
}
}
/**
* 获取商户应用公钥证书序列号,从证书模式运行时环境对象中直接读取
*
* @return 商户应用公钥证书序列号
*/
public String getMerchantCertSN() throws Exception {
if (context.getCertEnvironment() == null) {
return null;
}
return context.getCertEnvironment().getMerchantCertSN();
}
/**
* 从响应Map中提取支付宝公钥证书序列号
*
* @param respMap 响应Map
* @return 支付宝公钥证书序列号
*/
public String getAlipayCertSN(java.util.Map<String, Object> respMap) throws Exception {
return (String) respMap.get(AlipayConstants.ALIPAY_CERT_SN_FIELD);
}
/**
* 获取支付宝根证书序列号,从证书模式运行时环境对象中直接读取
*
* @return 支付宝根证书序列号
*/
public String getAlipayRootCertSN() throws Exception {
if (context.getCertEnvironment() == null) {
return null;
}
return context.getCertEnvironment().getRootCertSN();
}
/**
* 是否是证书模式
*
* @return truefalse不是
*/
public Boolean isCertMode() throws Exception {
return context.getCertEnvironment() != null;
}
/**
* 获取支付宝公钥,从证书运行时环境对象中直接读取
* 如果缓存的用户指定的支付宝公钥证书的序列号与网关响应中携带的支付宝公钥证书序列号不一致,需要报错给出提示或自动更新支付宝公钥证书
*
* @param alipayCertSN 网关响应中携带的支付宝公钥证书序列号
* @return 支付宝公钥
*/
public String extractAlipayPublicKey(String alipayCertSN) throws Exception {
if (context.getCertEnvironment() == null) {
return null;
}
return context.getCertEnvironment().getAlipayPublicKey(alipayCertSN);
}
/**
* 验证签名
*
* @param respMap 响应Map可以从中提取出sign和body
* @param alipayPublicKey 支付宝公钥
* @return true验签通过false验签不通过
*/
public Boolean verify(java.util.Map<String, Object> respMap, String alipayPublicKey) throws Exception {
String sign = (String) respMap.get(AlipayConstants.SIGN_FIELD);
String content = SignContentExtractor.getSignSourceData((String) respMap.get(AlipayConstants.BODY_FIELD),
(String) respMap.get(AlipayConstants.METHOD_FIELD));
return Signer.verify(content, sign, alipayPublicKey);
}
/**
* 计算签名注意要去除key或value为null的键值对
*
* @param systemParams 系统参数集合
* @param bizParams 业务参数集合
* @param textParams 其他额外文本参数集合
* @param merchantPrivateKey 私钥
* @return 签名值的Base64串
*/
public String sign(java.util.Map<String, String> systemParams, java.util.Map<String, ?> bizParams,
java.util.Map<String, String> textParams, String merchantPrivateKey) throws Exception {
Map<String, String> sortedMap = getSortedMap(systemParams, bizParams, textParams);
StringBuilder content = new StringBuilder();
int index = 0;
for (Entry<String, String> pair : sortedMap.entrySet()) {
if (!Strings.isNullOrEmpty(pair.getKey()) && !Strings.isNullOrEmpty(pair.getValue())) {
content.append(index == 0 ? "" : "&").append(pair.getKey()).append("=").append(pair.getValue());
index++;
}
}
return context.getSigner().sign(content.toString(), merchantPrivateKey);
}
/**
* 将随机顺序的Map转换为有序的Map
*
* @param input 随机顺序的Map
* @return 有序的Map
*/
public java.util.Map<String, String> sortMap(java.util.Map<String, String> input) throws Exception {
//GO语言的Map是随机顺序的每次访问顺序都不同才需排序
return input;
}
/**
* AES加密
*
* @param plainText 明文
* @param key 密钥
* @return 密文
*/
public String aesEncrypt(String plainText, String key) throws Exception {
return AES.encrypt(plainText, key);
}
/**
* AES解密
*
* @param cipherText 密文
* @param key 密钥
* @return 明文
*/
public String aesDecrypt(String cipherText, String key) throws Exception {
return AES.decrypt(cipherText, key);
}
/**
* 生成订单串
*
* @param systemParams 系统参数集合
* @param bizParams 业务参数集合
* @param textParams 额外文本参数集合
* @param sign 所有参数的签名值
* @return 订单串
*/
public String generateOrderString(java.util.Map<String, String> systemParams, java.util.Map<String, Object> bizParams,
java.util.Map<String, String> textParams, String sign) throws Exception {
//采集并排序所有参数
Map<String, String> sortedMap = getSortedMap(systemParams, bizParams, textParams);
sortedMap.put(AlipayConstants.SIGN_FIELD, sign);
//将所有参数置于URL中
return buildQueryString(sortedMap);
}
/**
* 对支付类请求的异步通知的参数集合进行验签
*
* @param parameters 参数集合
* @param publicKey 支付宝公钥
* @return true验证成功false验证失败
*/
public Boolean verifyParams(java.util.Map<String, String> parameters, String publicKey) throws Exception {
return Signer.verifyParams(parameters, publicKey);
}
private Map<String, String> getSortedMap(Map<String, String> systemParams, Map<String, ?> bizParams,
Map<String, String> textParams) throws Exception {
addOtherParams(textParams, bizParams);
Map<String, String> sortedMap = new TreeMap<>(systemParams);
if (bizParams != null && !bizParams.isEmpty()) {
sortedMap.put(AlipayConstants.BIZ_CONTENT_FIELD, JsonUtil.toJsonString(bizParams));
}
if (textParams != null) {
sortedMap.putAll(textParams);
}
return sortedMap;
}
private void setNotifyUrl(Map<String, String> params) throws Exception {
if (getConfig(AlipayConstants.NOTIFY_URL_CONFIG_KEY) != null && !params.containsKey(AlipayConstants.NOTIFY_URL_FIELD)) {
params.put(AlipayConstants.NOTIFY_URL_FIELD, getConfig(AlipayConstants.NOTIFY_URL_CONFIG_KEY));
}
}
/**
* 字符串拼接
*
* @param a 字符串a
* @param b 字符串b
* @return 字符串a和b拼接后的字符串
*/
public String concatStr(String a, String b) {
return a + b;
}
private String buildQueryString(Map<String, String> sortedMap) throws UnsupportedEncodingException {
StringBuilder content = new StringBuilder();
int index = 0;
for (Entry<String, String> pair : sortedMap.entrySet()) {
if (!Strings.isNullOrEmpty(pair.getKey()) && !Strings.isNullOrEmpty(pair.getValue())) {
content.append(index == 0 ? "" : "&")
.append(pair.getKey())
.append("=")
.append(URLEncoder.encode(pair.getValue(), AlipayConstants.DEFAULT_CHARSET.name()));
index++;
}
}
return content.toString();
}
private String getGatewayServerUrl() throws Exception {
return getConfig(AlipayConstants.PROTOCOL_CONFIG_KEY) + "://" + getConfig(AlipayConstants.HOST_CONFIG_KEY) + "/gateway.do";
}
}

View File

@@ -0,0 +1,110 @@
/**
* Alipay.com Inc.
* Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel;
import com.aliyun.tea.NameInMap;
import com.aliyun.tea.TeaModel;
import com.aliyun.tea.Validation;
/**
* @author zhongyu
* @version : Config.java, v 0.1 2020年05月22日 4:25 下午 zhongyu Exp $
*/
public class Config extends TeaModel {
/**
* 通信协议通常填写https
*/
@NameInMap("protocol")
@Validation(required = true)
public String protocol;
/**
* 网关域名
* 线上为openapi.alipay.com
* 沙箱为openapi.alipaydev.com
*/
@NameInMap("gatewayHost")
@Validation(required = true)
public String gatewayHost;
/**
* AppId
*/
@NameInMap("appId")
@Validation(required = true)
public String appId;
/**
* 签名类型Alipay Easy SDK只推荐使用RSA2估此处固定填写RSA2
*/
@NameInMap("signType")
@Validation(required = true)
public String signType;
/**
* 支付宝公钥
*/
@NameInMap("alipayPublicKey")
public String alipayPublicKey;
/**
* 应用私钥
*/
@NameInMap("merchantPrivateKey")
@Validation(required = true)
public String merchantPrivateKey;
/**
* 应用公钥证书文件路径
*/
@NameInMap("merchantCertPath")
public String merchantCertPath;
/**
* 支付宝公钥证书文件路径
*/
@NameInMap("alipayCertPath")
public String alipayCertPath;
/**
* 支付宝根证书文件路径
*/
@NameInMap("alipayRootCertPath")
public String alipayRootCertPath;
/**
* 异步通知回调地址(可选)
*/
@NameInMap("notifyUrl")
public String notifyUrl;
/**
* AES密钥可选
*/
@NameInMap("encryptKey")
public String encryptKey;
/**
* 签名提供方的名称(可选)Aliyun KMS签名signProvider = "AliyunKMS"
*/
@NameInMap("signProvider")
public String signProvider;
/**
* 代理地址(可选)
* 例如http://127.0.0.1:8080
*/
@NameInMap("httpProxy")
public String httpProxy;
/**
* 忽略证书校验(可选)
*/
@NameInMap("ignoreSSL")
public boolean ignoreSSL;
}

View File

@@ -0,0 +1,81 @@
/**
* Alipay.com Inc.
* Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel;
import com.alipay.easysdk.kernel.util.Signer;
import com.aliyun.tea.TeaModel;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import java.util.Map;
/**
* @author zhongyu
* @version : Context.java, v 0.1 2020年05月24日 10:41 上午 zhongyu Exp $
*/
public class Context {
/**
* 客户端配置参数
*/
private final Map<String, Object> config;
/**
* SDK版本号
*/
private String sdkVersion;
/**
* 证书模式运行时环境
*/
private CertEnvironment certEnvironment;
/**
* SHA256WithRSA签名器
*/
private Signer signer;
public Context(Config options, String sdkVersion) throws Exception {
config = TeaModel.buildMap(options);
this.sdkVersion = sdkVersion;
Preconditions.checkArgument(AlipayConstants.RSA2.equals(getConfig(AlipayConstants.SIGN_TYPE_CONFIG_KEY)),
"Alipay Easy SDK只允许使用RSA2签名方式RSA签名方式由于安全性相比RSA2弱已不再推荐。");
if (!Strings.isNullOrEmpty(getConfig(AlipayConstants.ALIPAY_CERT_PATH_CONFIG_KEY))) {
certEnvironment = new CertEnvironment(
getConfig(AlipayConstants.MERCHANT_CERT_PATH_CONFIG_KEY),
getConfig(AlipayConstants.ALIPAY_CERT_PATH_CONFIG_KEY),
getConfig(AlipayConstants.ALIPAY_ROOT_CERT_PATH_CONFIG_KEY));
}
signer = new Signer();
}
public String getConfig(String key) {
if (String.valueOf(config.get(key)) == "null") {
return null;
} else {
return String.valueOf(config.get(key));
}
}
public String getSdkVersion() {
return sdkVersion;
}
public void setSdkVersion(String sdkVersion) {
this.sdkVersion = sdkVersion;
}
public CertEnvironment getCertEnvironment() {
return certEnvironment;
}
public Signer getSigner() {
return signer;
}
public void setSigner(Signer signer) {
this.signer = signer;
}
}

View File

@@ -0,0 +1,88 @@
/**
* Alipay.com Inc.
* Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel.util;
import com.alipay.easysdk.kernel.AlipayConstants;
import org.bouncycastle.util.encoders.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* 加密工具
*/
public class AES {
private static final String AES_ALG = "AES";
private static final String AES_CBC_PCK_ALG = "AES/CBC/PKCS5Padding";
private static final byte[] AES_IV = initIV();
/**
* AES加密
*
* @param plainText 明文
* @param key 对称密钥
* @return 密文
*/
public static String encrypt(String plainText, String key) {
try {
Cipher cipher = Cipher.getInstance(AES_CBC_PCK_ALG);
IvParameterSpec iv = new IvParameterSpec(AES_IV);
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(Base64.decode(key.getBytes()), AES_ALG), iv);
byte[] encryptBytes = cipher.doFinal(plainText.getBytes(AlipayConstants.DEFAULT_CHARSET));
return new String(Base64.encode(encryptBytes));
} catch (Exception e) {
throw new RuntimeException("AES加密失败plainText=" + plainText +
"keySize=" + key.length() + "" + e.getMessage(), e);
}
}
/**
* 密文
*
* @param cipherText 密文
* @param key 对称密钥
* @return 明文
*/
public static String decrypt(String cipherText, String key) {
try {
Cipher cipher = Cipher.getInstance(AES_CBC_PCK_ALG);
IvParameterSpec iv = new IvParameterSpec(AES_IV);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(Base64.decode(key.getBytes()), AES_ALG), iv);
byte[] cleanBytes = cipher.doFinal(Base64.decode(cipherText.getBytes()));
return new String(cleanBytes, AlipayConstants.DEFAULT_CHARSET);
} catch (Exception e) {
throw new RuntimeException("AES解密失败cipherText=" + cipherText +
"keySize=" + key.length() + "" + e.getMessage(), e);
}
}
/**
* 初始向量的方法全部为0
* 这里的写法适合于其它算法AES算法IV值一定是128位的(16字节)
*/
private static byte[] initIV() {
try {
Cipher cipher = Cipher.getInstance(AES_CBC_PCK_ALG);
int blockSize = cipher.getBlockSize();
byte[] iv = new byte[blockSize];
for (int i = 0; i < blockSize; ++i) {
iv[i] = 0;
}
return iv;
} catch (Exception e) {
int blockSize = 16;
byte[] iv = new byte[blockSize];
for (int i = 0; i < blockSize; ++i) {
iv[i] = 0;
}
return iv;
}
}
}

View File

@@ -0,0 +1,396 @@
package com.alipay.easysdk.kernel.util;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PublicKey;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 证书文件可信校验
*
* @author junying.wjy
* @version $Id: AntCertificationUtil.java, v 0.1 2019-07-29 下午04:46 junying.wjy Exp $
*/
public class AntCertificationUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(AntCertificationUtil.class);
private static BouncyCastleProvider provider;
static {
provider = new BouncyCastleProvider();
Security.addProvider(provider);
}
/**
* 验证证书是否可信
*
* @param certContent 需要验证的目标证书或者证书链
* @param rootCertContent 可信根证书列表
*/
public static boolean isTrusted(String certContent, String rootCertContent) {
X509Certificate[] certificates;
try {
certificates = readPemCertChain(certContent);
} catch (Exception e) {
LOGGER.error("读取证书失败", e);
throw new RuntimeException(e);
}
List<X509Certificate> rootCerts = new ArrayList<X509Certificate>();
try {
X509Certificate[] certs = readPemCertChain(rootCertContent);
rootCerts.addAll(Arrays.asList(certs));
} catch (Exception e) {
LOGGER.error("读取根证书失败", e);
throw new RuntimeException(e);
}
return verifyCertChain(certificates, rootCerts.toArray(new X509Certificate[rootCerts.size()]));
}
/**
* 验证证书是否是信任证书库中证书签发的
*
* @param cert 目标验证证书
* @param rootCerts 可信根证书列表
* @return 验证结果
*/
private static boolean verifyCert(X509Certificate cert, X509Certificate[] rootCerts) {
try {
cert.checkValidity();
} catch (CertificateExpiredException e) {
LOGGER.error("证书已经过期", e);
return false;
} catch (CertificateNotYetValidException e) {
LOGGER.error("证书未激活", e);
return false;
}
Map<Principal, X509Certificate> subjectMap = new HashMap<Principal, X509Certificate>();
for (X509Certificate root : rootCerts) {
subjectMap.put(root.getSubjectDN(), root);
}
Principal issuerDN = cert.getIssuerDN();
X509Certificate issuer = subjectMap.get(issuerDN);
if (issuer == null) {
LOGGER.error("证书链验证失败");
return false;
}
try {
PublicKey publicKey = issuer.getPublicKey();
verifySignature(publicKey, cert);
} catch (Exception e) {
LOGGER.error("证书链验证失败", e);
return false;
}
return true;
}
/**
* 验证证书链是否是信任证书库中证书签发的
*
* @param certs 目标验证证书列表
* @param rootCerts 可信根证书列表
* @return 验证结果
*/
private static boolean verifyCertChain(X509Certificate[] certs, X509Certificate[] rootCerts) {
boolean sorted = sortByDn(certs);
if (!sorted) {
LOGGER.error("证书链验证失败:不是完整的证书链");
return false;
}
//先验证第一个证书是不是信任库中证书签发的
X509Certificate prev = certs[0];
boolean firstOK = verifyCert(prev, rootCerts);
if (!firstOK || certs.length == 1) {
return firstOK;
}
//验证证书链
for (int i = 1; i < certs.length; i++) {
try {
X509Certificate cert = certs[i];
try {
cert.checkValidity();
} catch (CertificateExpiredException e) {
LOGGER.error("证书已经过期");
return false;
} catch (CertificateNotYetValidException e) {
LOGGER.error("证书未激活");
return false;
}
verifySignature(prev.getPublicKey(), cert);
prev = cert;
} catch (Exception e) {
LOGGER.error("证书链验证失败");
return false;
}
}
return true;
}
private static void verifySignature(PublicKey publicKey, X509Certificate cert)
throws NoSuchProviderException, CertificateException, NoSuchAlgorithmException, InvalidKeyException,
SignatureException {
cert.verify(publicKey, provider.getName());
}
/**
* 将证书链按照完整的签发顺序进行排序,排序后证书链为:[issuerA, subjectA]-[issuerA, subjectB]-[issuerB, subjectC]-[issuerC, subjectD]...
*
* @param certs 证书链
* @return true排序成功false证书链不完整
*/
private static boolean sortByDn(X509Certificate[] certs) {
//主题和证书的映射
Map<Principal, X509Certificate> subjectMap = new HashMap<Principal, X509Certificate>();
//签发者和证书的映射
Map<Principal, X509Certificate> issuerMap = new HashMap<Principal, X509Certificate>();
//是否包含自签名证书
boolean hasSelfSignedCert = false;
for (X509Certificate cert : certs) {
if (isSelfSigned(cert)) {
if (hasSelfSignedCert) {
return false;
}
hasSelfSignedCert = true;
}
Principal subjectDN = cert.getSubjectDN();
Principal issuerDN = cert.getIssuerDN();
subjectMap.put(subjectDN, cert);
issuerMap.put(issuerDN, cert);
}
List<X509Certificate> certChain = new ArrayList<X509Certificate>();
X509Certificate current = certs[0];
addressingUp(subjectMap, certChain, current);
addressingDown(issuerMap, certChain, current);
//说明证书链不完整
if (certs.length != certChain.size()) {
return false;
}
//将证书链复制到原先的数据
for (int i = 0; i < certChain.size(); i++) {
certs[i] = certChain.get(i);
}
return true;
}
/**
* 验证证书是否是自签发的
*
* @param cert 目标证书
* @return true自签发false不是自签发
*/
private static boolean isSelfSigned(X509Certificate cert) {
return cert.getSubjectDN().equals(cert.getIssuerDN());
}
/**
* 向上构造证书链
*
* @param subjectMap 主题和证书的映射
* @param certChain 证书链
* @param current 当前需要插入证书链的证书include
*/
private static void addressingUp(final Map<Principal, X509Certificate> subjectMap, List<X509Certificate> certChain,
final X509Certificate current) {
certChain.add(0, current);
if (isSelfSigned(current)) {
return;
}
Principal issuerDN = current.getIssuerDN();
X509Certificate issuer = subjectMap.get(issuerDN);
if (issuer == null) {
return;
}
addressingUp(subjectMap, certChain, issuer);
}
/**
* 向下构造证书链
*
* @param issuerMap 签发者和证书的映射
* @param certChain 证书链
* @param current 当前需要插入证书链的证书exclude
*/
private static void addressingDown(final Map<Principal, X509Certificate> issuerMap, List<X509Certificate> certChain,
final X509Certificate current) {
Principal subjectDN = current.getSubjectDN();
X509Certificate subject = issuerMap.get(subjectDN);
if (subject == null) {
return;
}
if (isSelfSigned(subject)) {
return;
}
certChain.add(subject);
addressingDown(issuerMap, certChain, subject);
}
private static X509Certificate[] readPemCertChain(String cert) throws CertificateException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(cert.getBytes());
CertificateFactory factory = CertificateFactory.getInstance("X.509", provider);
Collection<? extends Certificate> certificates = factory.generateCertificates(inputStream);
return certificates.toArray(new X509Certificate[certificates.size()]);
}
/**
* 获取支付宝根证书序列号
*
* @param rootCertContent 支付宝根证书内容
* @return 支付宝根证书序列号
*/
public static String getRootCertSN(String rootCertContent) {
String rootCertSN = null;
try {
X509Certificate[] x509Certificates = readPemCertChain(rootCertContent);
MessageDigest md = MessageDigest.getInstance("MD5");
for (X509Certificate c : x509Certificates) {
if (c.getSigAlgOID().startsWith("1.2.840.113549.1.1")) {
md.update((c.getIssuerX500Principal().getName() + c.getSerialNumber()).getBytes());
String certSN = new BigInteger(1, md.digest()).toString(16);
//BigInteger会把0省略掉需补全至32位
certSN = fillMD5(certSN);
if (Strings.isNullOrEmpty(rootCertSN)) {
rootCertSN = certSN;
} else {
rootCertSN = rootCertSN + "_" + certSN;
}
}
}
} catch (Exception e) {
LOGGER.error("提取根证书失败");
}
return rootCertSN;
}
/**
* 获取公钥证书序列号
*
* @param certContent 公钥证书内容
* @return 公钥证书序列号
*/
public static String getCertSN(String certContent) {
try {
InputStream inputStream = new ByteArrayInputStream(certContent.getBytes());
CertificateFactory factory = CertificateFactory.getInstance("X.509", "BC");
X509Certificate cert = (X509Certificate) factory.generateCertificate(inputStream);
return md5((cert.getIssuerX500Principal().getName() + cert.getSerialNumber()).getBytes());
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
private static String md5(byte[] bytes) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
String certSN = new BigInteger(1, md.digest()).toString(16);
//BigInteger会把0省略掉需补全至32位
certSN = fillMD5(certSN);
return certSN;
}
private static String fillMD5(String md5) {
return md5.length() == 32 ? md5 : fillMD5("0" + md5);
}
/**
* 提取公钥证书中的公钥
*
* @param certContent 公钥证书内容
* @return 公钥证书中的公钥
*/
public static String getCertPublicKey(String certContent) {
try {
InputStream inputStream = new ByteArrayInputStream(certContent.getBytes());
CertificateFactory factory = CertificateFactory.getInstance("X.509", "BC");
X509Certificate cert = (X509Certificate) factory.generateCertificate(inputStream);
return Base64.toBase64String(cert.getPublicKey().getEncoded());
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* 从文件中读取证书内容
*
* @param certPath 证书路径
* @return 证书内容
*/
public static String readCertContent(String certPath) {
if (existsInFileSystem(certPath)) {
return readFromFileSystem(certPath);
}
return readFromClassPath(certPath);
}
private static boolean existsInFileSystem(String certPath) {
try {
return new File(certPath).exists();
} catch (Throwable e) {
return false;
}
}
private static String readFromFileSystem(String certPath) {
try {
return new String(Files.toByteArray(new File(certPath)), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("从文件系统中读取[" + certPath + "]失败," + e.getMessage(), e);
}
}
private static String readFromClassPath(String certPath) {
try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(certPath)) {
return new String(ByteStreams.toByteArray(inputStream), StandardCharsets.UTF_8);
} catch (Exception e) {
String errorMessage = e.getMessage() == null ? "" : e.getMessage() + "";
if (certPath.startsWith("/")) {
errorMessage += "ClassPath路径不可以/开头,请去除后重试。";
}
throw new RuntimeException("读取[" + certPath + "]失败。" + errorMessage, e);
}
}
}

View File

@@ -0,0 +1,51 @@
/**
* Alipay.com Inc.
* Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel.util;
import com.aliyun.tea.TeaModel;
import com.google.gson.Gson;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
* JSON工具类
*
* @author zhongyu
* @version : JsonUtil.java, v 0.1 2020年02月18日 8:20 下午 zhongyu Exp $
*/
public class JsonUtil {
/**
* 将Map转换为Json字符串转换过程中对于TeaModel使用标注的字段名称而不是字段的变量名
*
* @param input 输入的Map
* @return Json字符串
*/
public static String toJsonString(Map<String, ?> input) {
Map<String, Object> result = new HashMap<>();
for (Entry<String, ?> pair : input.entrySet()) {
if (pair.getValue() instanceof TeaModel) {
result.put(pair.getKey(), getTeaModelMap((TeaModel) pair.getValue()));
} else {
result.put(pair.getKey(), pair.getValue());
}
}
return new Gson().toJson(result);
}
private static Map<String, Object> getTeaModelMap(TeaModel teaModel) {
Map<String, Object> result = new HashMap<>();
Map<String, Object> teaModelMap = teaModel.toMap();
for (Entry<String, Object> pair : teaModelMap.entrySet()) {
if (pair.getValue() instanceof TeaModel) {
result.put(pair.getKey(), getTeaModelMap((TeaModel) pair.getValue()));
} else {
result.put(pair.getKey(), pair.getValue());
}
}
return result;
}
}

View File

@@ -0,0 +1,77 @@
/**
* Alipay.com Inc.
* Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel.util;
import com.alipay.easysdk.kernel.AlipayConstants;
import com.google.common.base.Preconditions;
import java.io.File;
/**
* HTTP multipart/form-data格式相关工具类
*
* @author zhongyu
* @version : MulitpartUtil.java, v 0.1 2020年02月08日 11:26 上午 zhongyu Exp $
*/
public class MultipartUtil {
/**
* 获取Multipart分界符
*
* @param boundary 用作分界的随机字符串
* @return Multipart分界符
*/
public static byte[] getEntryBoundary(String boundary) {
return ("\r\n--" + boundary + "\r\n").getBytes();
}
/**
* 获取Multipart结束标记
*
* @param boundary 用作分界的随机字符串
* @return Multipart结束标记
*/
public static byte[] getEndBoundary(String boundary) {
return ("\r\n--" + boundary + "--\r\n").getBytes();
}
/**
* 获取Multipart中的文本参数结构
*
* @param fieldName 字段名称
* @param fieldValue 字段值
* @return 文本参数结构
*/
public static byte[] getTextEntry(String fieldName, String fieldValue) {
String entry = "Content-Disposition:form-data;name=\""
+ fieldName
+ "\"\r\nContent-Type:text/plain\r\n\r\n"
+ fieldValue;
return entry.getBytes(AlipayConstants.DEFAULT_CHARSET);
}
/**
* 获取Multipart中的文件参数结构不含文件内容只有文件元数据
*
* @param fieldName 字段名称
* @param filePath 文件路径
* @return 文件参数结构(不含文件内容)
*/
public static byte[] getFileEntry(String fieldName, String filePath) {
String entry = "Content-Disposition:form-data;name=\""
+ fieldName
+ "\";filename=\""
+ getFile(filePath).getName()
+ "\"\r\nContent-Type:application/octet-stream"
+ "\r\n\r\n";
return entry.getBytes(AlipayConstants.DEFAULT_CHARSET);
}
private static File getFile(String filePath) {
File file = new File(filePath);
Preconditions.checkArgument(file.exists(), file.getAbsolutePath() + "文件不存在");
Preconditions.checkArgument(file.getName().contains("."), "文件名必须带上正确的扩展名");
return file;
}
}

View File

@@ -0,0 +1,59 @@
/**
* Alipay.com Inc.
* Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel.util;
import java.util.Map;
import java.util.Map.Entry;
/**
* 生成页面信息辅助类
*
* @author zhongyu
* @version : PageUtil.java, v 0.1 2020年02月12日 3:11 下午 zhongyu Exp $
*/
public class PageUtil {
/**
* 生成表单
*
* @param actionUrl 表单提交链接
* @param parameters 表单参数
* @return 表单字符串
*/
public static String buildForm(String actionUrl, Map<String, String> parameters) {
return "<form name=\"punchout_form\" method=\"post\" action=\""
+ actionUrl
+ "\">\n"
+ buildHiddenFields(parameters)
+ "<input type=\"submit\" value=\"立即支付\" style=\"display:none\" >\n"
+ "</form>\n"
+ "<script>document.forms[0].submit();</script>";
}
private static String buildHiddenFields(Map<String, String> parameters) {
if (parameters == null || parameters.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder();
for (Entry<String, String> pair : parameters.entrySet()) {
// 除去参数中的空值
if (pair.getKey() == null || pair.getValue() == null) {
continue;
}
builder.append(buildHiddenField(pair.getKey(), pair.getValue()));
}
return builder.toString();
}
private static String buildHiddenField(String key, String value) {
StringBuilder builder = new StringBuilder(64);
builder.append("<input type=\"hidden\" name=\"");
builder.append(key);
builder.append("\" value=\"");
//转义双引号
String a = value.replace("\"", "&quot;");
builder.append(a).append("\">\n");
return builder.toString();
}
}

View File

@@ -0,0 +1,39 @@
/**
* Alipay.com Inc.
* Copyright (c) 2004-2020 All Rights Reserved.
*/
package com.alipay.easysdk.kernel.util;
import com.aliyun.tea.TeaModel;
import com.google.common.base.Strings;
import java.lang.reflect.Field;
/**
* 响应检查工具类
*
* @author zhongyu
* @version : ResponseChecker.java, v 0.1 2020年06月02日 10:42 上午 zhongyu Exp $
*/
public class ResponseChecker {
public static final String SUB_CODE_FIELD_NAME = "subCode";
/**
* 判断一个请求返回的响应是否成功
*
* @param response 响应对象
* @return true成功false失败
*/
public static boolean success(TeaModel response) {
try {
Field subCodeField = response.getClass().getField(SUB_CODE_FIELD_NAME);
subCodeField.setAccessible(true);
String subCode = (String) subCodeField.get(response);
return Strings.isNullOrEmpty(subCode);
} catch (NoSuchFieldException | IllegalAccessException e) {
//没有subCode字段的响应对象通常是那些无需跟网关远程通信的API只要本地执行完成都视为成功
return true;
}
}
}

View File

@@ -0,0 +1,203 @@
/**
* Alipay.com Inc. Copyright (c) 2004-2019 All Rights Reserved.
*/
package com.alipay.easysdk.kernel.util;
import com.alipay.easysdk.kernel.AlipayConstants;
import java.util.LinkedList;
/**
* 待验签原文提取器
* <p>
* 注此处不可使用JSON反序列化工具进行提取会破坏原有格式对于签名而言差个空格都会验签不通过
*
* @author zhongyu
* @version $Id: SignContentExtractor.java, v 0.1 2019年12月19日 9:07 PM zhongyu Exp $
*/
public class SignContentExtractor {
/**
* 左大括号
*/
public static final char LEFT_BRACE = '{';
/**
* 右大括号
*/
public static final char RIGHT_BRACE = '}';
/**
* 双引号
*/
public static final char DOUBLE_QUOTES = '"';
/**
* 获取待验签的原文
*
* @param body 网关的整体响应字符串
* @param method 本次调用的OpenAPI接口名称
* @return 待验签的原文
*/
public static String getSignSourceData(String body, String method) {
// 加签源串起点
String rootNode = method.replace('.', '_') + AlipayConstants.RESPONSE_SUFFIX;
String errorRootNode = AlipayConstants.ERROR_RESPONSE;
int indexOfRootNode = body.indexOf(rootNode);
int indexOfErrorRoot = body.indexOf(errorRootNode);
if (indexOfRootNode > 0) {
return parseSignSourceData(body, rootNode, indexOfRootNode);
} else if (indexOfErrorRoot > 0) {
return parseSignSourceData(body, errorRootNode, indexOfErrorRoot);
} else {
return null;
}
}
private static String parseSignSourceData(String body, String rootNode, int indexOfRootNode) {
//第一个字母 + 长度 + 冒号 + 引号
int signDataStartIndex = indexOfRootNode + rootNode.length() + 2;
int indexOfSign = body.indexOf("\"" + AlipayConstants.SIGN_FIELD + "\"");
if (indexOfSign < 0) {
return null;
}
SignSourceData signSourceData = extractSignContent(body, signDataStartIndex);
//如果提取的待验签原始内容后还有rootNode
if (body.lastIndexOf(rootNode) > signSourceData.getEndIndex()) {
throw new RuntimeException("检测到响应报文中有重复的" + rootNode + ",验签失败。");
}
return signSourceData.getSourceData();
}
private static SignSourceData extractSignContent(String str, int begin) {
if (str == null) {
return null;
}
int beginIndex = extractBeginPosition(str, begin);
if (beginIndex >= str.length()) {
return null;
}
int endIndex = extractEndPosition(str, beginIndex);
return new SignSourceData(str.substring(beginIndex, endIndex), beginIndex, endIndex);
}
private static int extractBeginPosition(String responseString, int begin) {
int beginPosition = begin;
//找到第一个左大括号对应响应的是JSON对象的情况普通调用OpenAPI响应明文
//或者双引号对应响应的是JSON字符串的情况加密调用OpenAPI响应Base64串作为待验签内容的起点
while (beginPosition < responseString.length()
&& responseString.charAt(beginPosition) != LEFT_BRACE
&& responseString.charAt(beginPosition) != DOUBLE_QUOTES) {
++beginPosition;
}
return beginPosition;
}
private static int extractEndPosition(String responseString, int beginPosition) {
//提取明文验签内容终点
if (responseString.charAt(beginPosition) == LEFT_BRACE) {
return extractJsonObjectEndPosition(responseString, beginPosition);
}
//提取密文验签内容终点
else {
return extractJsonBase64ValueEndPosition(responseString, beginPosition);
}
}
private static int extractJsonBase64ValueEndPosition(String responseString, int beginPosition) {
for (int index = beginPosition; index < responseString.length(); ++index) {
//找到第2个双引号作为终点由于中间全部是Base64编码的密文所以不会有干扰的特殊字符
if (responseString.charAt(index) == DOUBLE_QUOTES && index != beginPosition) {
return index + 1;
}
}
//如果没有找到第2个双引号说明验签内容片段提取失败直接尝试选取剩余整个响应字符串进行验签
return responseString.length();
}
private static int extractJsonObjectEndPosition(String responseString, int beginPosition) {
//记录当前尚未发现配对闭合的大括号
LinkedList<Character> braces = new LinkedList<Character>();
//记录当前字符是否在双引号中
boolean inQuotes = false;
//记录当前字符前面连续的转义字符个数
int consecutiveEscapeCount = 0;
//从待验签字符的起点开始遍历后续字符串,找出待验签字符串的终止点,终点即是与起点{配对的}
for (int index = beginPosition; index < responseString.length(); ++index) {
//提取当前字符
char currentChar = responseString.charAt(index);
//如果当前字符是"且前面有偶数个转义标记0也是偶数
if (currentChar == DOUBLE_QUOTES && consecutiveEscapeCount % 2 == 0) {
//是否在引号中的状态取反
inQuotes = !inQuotes;
}
//如果当前字符是{且不在引号中
else if (currentChar == LEFT_BRACE && !inQuotes) {
//将该{加入未闭合括号中
braces.push(LEFT_BRACE);
}
//如果当前字符是}且不在引号中
else if (currentChar == RIGHT_BRACE && !inQuotes) {
//弹出一个未闭合括号
braces.pop();
//如果弹出后,未闭合括号为空,说明已经找到终点
if (braces.isEmpty()) {
return index + 1;
}
}
//如果当前字符是转义字符
if (currentChar == '\\') {
//连续转义字符个数+1
++consecutiveEscapeCount;
} else {
//连续转义字符个数置0
consecutiveEscapeCount = 0;
}
}
//如果没有找到配对的闭合括号,说明验签内容片段提取失败,直接尝试选取剩余整个响应字符串进行验签
return responseString.length();
}
private static class SignSourceData {
/**
* 待验签原始内容
*/
private final String sourceData;
/**
* 待验签原始内容在响应字符串中的起始位置
*/
private final int beginIndex;
/**
* 待验签原始内容在响应字符串中的结束位置
*/
private final int endIndex;
SignSourceData(String sourceData, int beginIndex, int endIndex) {
this.sourceData = sourceData;
this.beginIndex = beginIndex;
this.endIndex = endIndex;
}
String getSourceData() {
return sourceData;
}
public int getBeginIndex() {
return beginIndex;
}
int getEndIndex() {
return endIndex;
}
}
}

View File

@@ -0,0 +1,115 @@
/**
* Alipay.com Inc. Copyright (c) 2004-2019 All Rights Reserved.
*/
package com.alipay.easysdk.kernel.util;
import com.alipay.easysdk.kernel.AlipayConstants;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* SHA256WithRSA签名器
*
* @author zhongyu
* @version $Id: Signer.java, v 0.1 2019年12月19日 9:10 PM zhongyu Exp $
*/
public class Signer {
private static final Logger LOGGER = LoggerFactory.getLogger(Signer.class);
public static String getSignCheckContent(Map<String, String> params) {
if (params == null) {
return null;
}
StringBuilder content = new StringBuilder();
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
content.append(i == 0 ? "" : "&").append(key).append("=").append(value);
}
return content.toString();
}
/**
* 验证签名
*
* @param content 待验签的内容
* @param sign 签名值的Base64串
* @param publicKeyPem 支付宝公钥
* @return true验证成功false验证失败
*/
public static boolean verify(String content, String sign, String publicKeyPem) {
try {
KeyFactory keyFactory = KeyFactory.getInstance(AlipayConstants.RSA);
byte[] encodedKey = publicKeyPem.getBytes();
encodedKey = Base64.decode(encodedKey);
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
Signature signature = Signature.getInstance(AlipayConstants.SHA_256_WITH_RSA);
signature.initVerify(publicKey);
signature.update(content.getBytes(AlipayConstants.DEFAULT_CHARSET));
return signature.verify(Base64.decode(sign.getBytes()));
} catch (Exception e) {
String errorMessage = "验签遭遇异常content=" + content + " sign=" + sign +
" publicKey=" + publicKeyPem + " reason=" + e.getMessage();
LOGGER.error(errorMessage, e);
throw new RuntimeException(errorMessage, e);
}
}
/**
* 计算签名
*
* @param content 待签名的内容
* @param privateKeyPem 私钥
* @return 签名值的Base64串
*/
public String sign(String content, String privateKeyPem) {
try {
byte[] encodedKey = privateKeyPem.getBytes();
encodedKey = Base64.decode(encodedKey);
PrivateKey privateKey = KeyFactory.getInstance(AlipayConstants.RSA).generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
Signature signature = Signature.getInstance(AlipayConstants.SHA_256_WITH_RSA);
signature.initSign(privateKey);
signature.update(content.getBytes(AlipayConstants.DEFAULT_CHARSET));
byte[] signed = signature.sign();
return new String(Base64.encode(signed));
} catch (Exception e) {
String errorMessage = "签名遭遇异常content=" + content + " privateKeySize=" + privateKeyPem.length() + " reason=" + e.getMessage();
LOGGER.error(errorMessage, e);
throw new RuntimeException(errorMessage, e);
}
}
/**
* 对参数集合进行验签
*
* @param parameters 参数集合
* @param publicKey 支付宝公钥
* @return true验证成功false验证失败
*/
public static boolean verifyParams(Map<String, String> parameters, String publicKey) {
String sign = parameters.get(AlipayConstants.SIGN_FIELD);
parameters.remove(AlipayConstants.SIGN_FIELD);
parameters.remove(AlipayConstants.SIGN_TYPE_FIELD);
String content = getSignCheckContent(parameters);
return verify(content, sign, publicKey);
}
}