添加网站文件

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,17 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasySDKKernel", "EasySDKKernel\EasySDKKernel.csproj", "{71E029F7-98A5-40E8-8491-E75DD680E9CF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{71E029F7-98A5-40E8-8491-E75DD680E9CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71E029F7-98A5-40E8-8491-E75DD680E9CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71E029F7-98A5-40E8-8491-E75DD680E9CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71E029F7-98A5-40E8-8491-E75DD680E9CF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<PackOnBuild>true</PackOnBuild>
<PackageId>AlipayEasySDK.Kernel</PackageId>
<Authors>antopen</Authors>
<Owners>antopen</Owners>
<Description>Alipay Easy SDK for .NET allows you to enjoy a minimalist programming experience and quickly access the various high-frequency capabilities of the Alipay Open Platform.</Description>
<NeutralLanguage>zh</NeutralLanguage>
<PackageLicenseUrl>https://github.com/alipay/alipay-easysdk/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/alipay/alipay-easysdk</PackageProjectUrl>
<Summary>Alipay Easy SDK for .NET allows you to enjoy a minimalist programming experience and quickly access the various high-frequency capabilities of the Alipay Open Platform.</Summary>
<Title>Kernel for Alipay Easy SDK</Title>
<PackageVersion>1.0.5</PackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.6.7" />
<PackageReference Include="Tea" Version="1.0.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,65 @@
using System;
using System.Text;
namespace Alipay.EasySDK.Kernel
{
/// <summary>
/// 支付宝开放平台网关交互常用常量
/// </summary>
public static class AlipayConstants
{
/// <summary>
/// Config配置参数Key值
/// </summary>
public const string PROTOCOL_CONFIG_KEY = "protocol";
public const string HOST_CONFIG_KEY = "gatewayHost";
public const string ALIPAY_CERT_PATH_CONFIG_KEY = "alipayCertPath";
public const string MERCHANT_CERT_PATH_CONFIG_KEY = "merchantCertPath";
public const string ALIPAY_ROOT_CERT_PATH_CONFIG_KEY = "alipayRootCertPath";
public const string SIGN_TYPE_CONFIG_KEY = "signType";
public const string NOTIFY_URL_CONFIG_KEY = "notifyUrl";
/// <summary>
/// 与网关HTTP交互中涉及到的字段值
/// </summary>
public const string BIZ_CONTENT_FIELD = "biz_content";
public const string ALIPAY_CERT_SN_FIELD = "alipay_cert_sn";
public const string SIGN_FIELD = "sign";
public const string SIGN_TYPE_FIELD = "sign_type";
public const string BODY_FIELD = "http_body";
public const string NOTIFY_URL_FIELD = "notify_url";
public const string METHOD_FIELD = "method";
public const string RESPONSE_SUFFIX = "_response";
public const string ERROR_RESPONSE = "error_response";
/// <summary>
/// 默认字符集编码EasySDK统一固定使用UTF-8编码无需用户感知编码用户面对的总是String而不是bytes
/// </summary>
public readonly static Encoding DEFAULT_CHARSET = Encoding.UTF8;
/// <summary>
/// 默认的签名算法EasySDK统一固定使用RSA2签名算法即SHA_256_WITH_RSA但此参数依然需要用户指定以便用户感知因为在开放平台接口签名配置界面中需要选择同样的算法
/// </summary>
public const string RSA2 = "RSA2";
/// <summary>
/// RSA2对应的真实签名算法名称
/// </summary>
public const string SHA_256_WITH_RSA = "SHA256WithRSA";
/// <summary>
/// RSA2对应的真实非对称加密算法名称
/// </summary>
public const string RSA = "RSA";
/// <summary>
/// 申请生成的重定向网页的请求类型GET表示生成URL
/// </summary>
public const string GET = "GET";
/// <summary>
/// 申请生成的重定向网页的请求类型POST表示生成form表单
/// </summary>
public const string POST = "POST";
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.IO;
using Org.BouncyCastle.X509;
using Alipay.EasySDK.Kernel.Util;
using System.Linq;
namespace Alipay.EasySDK.Kernel
{
/// <summary>
/// 证书模式运行时环境
/// </summary>
public class CertEnvironment
{
/// <summary>
/// 支付宝根证书内容
/// </summary>
public string RootCertContent { get; set; }
/// <summary>
/// 支付宝根证书序列号
/// </summary>
public string RootCertSN { get; set; }
/// <summary>
/// 商户应用公钥证书序列号
/// </summary>
public string MerchantCertSN { get; set; }
/// <summary>
/// 缓存的不同支付宝公钥证书序列号对应的支付宝公钥
/// </summary>
private readonly Dictionary<string, string> CachedAlipayPublicKey = new Dictionary<string, string>();
/// <summary>
/// 构造证书运行环境
/// </summary>
/// <param name="merchantCertPath">商户公钥证书路径</param>
/// <param name="alipayCertPath">支付宝公钥证书路径</param>
/// <param name="alipayRootCertPath">支付宝根证书路径</param>
public CertEnvironment(string merchantCertPath, string alipayCertPath, string alipayRootCertPath)
{
if (string.IsNullOrEmpty(merchantCertPath) || string.IsNullOrEmpty(alipayCertPath) || string.IsNullOrEmpty(alipayCertPath))
{
throw new Exception("证书参数merchantCertPath、alipayCertPath或alipayRootCertPath设置不完整。");
}
this.RootCertContent = File.ReadAllText(alipayRootCertPath);
this.RootCertSN = AntCertificationUtil.GetRootCertSN(RootCertContent);
X509Certificate merchantCert = AntCertificationUtil.ParseCert(File.ReadAllText(merchantCertPath));
this.MerchantCertSN = AntCertificationUtil.GetCertSN(merchantCert);
X509Certificate alipayCert = AntCertificationUtil.ParseCert(File.ReadAllText(alipayCertPath));
string alipayCertSN = AntCertificationUtil.GetCertSN(alipayCert);
string alipayPublicKey = AntCertificationUtil.ExtractPemPublicKeyFromCert(alipayCert);
CachedAlipayPublicKey[alipayCertSN] = alipayPublicKey;
}
public string GetAlipayPublicKey(string sn)
{
//如果没有指定sn则默认取缓存中的第一个值
if (string.IsNullOrEmpty(sn))
{
return CachedAlipayPublicKey.Values.FirstOrDefault();
}
if (CachedAlipayPublicKey.ContainsKey(sn))
{
return CachedAlipayPublicKey[sn];
}
else
{
//网关在支付宝公钥证书变更前,一定会确认通知到商户并在商户做出反馈后,才会更新该商户的支付宝公钥证书
//TODO: 后续可以考虑加入自动升级支付宝公钥证书逻辑,注意并发更新冲突问题
throw new Exception("支付宝公钥证书[" + sn + "]已过期,请重新下载最新支付宝公钥证书并替换原证书文件");
}
}
}
}

View File

@@ -0,0 +1,521 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Alipay.EasySDK.Kernel.Util;
using Tea;
namespace Alipay.EasySDK.Kernel
{
/// <summary>
/// Tea DSL编排所需实现的原子方法
/// </summary>
public class Client
{
/// <summary>
/// 构造成本较高的一些参数缓存在上下文中
/// </summary>
private readonly Context context;
/// <summary>
/// 注入的可选额外文本参数集合
/// </summary>
private readonly Dictionary<string, string> optionalTextParams = new Dictionary<string, string>();
/// <summary>
/// 注入的可选业务参数集合
/// </summary>
private readonly Dictionary<string, object> optionalBizParams = new Dictionary<string, object>();
/// <summary>
/// 构造函数
/// </summary>
/// <param name="context">上下文对象</param>
public Client(Context context)
{
this.context = context;
}
/// <summary>
/// 注入额外文本参数
/// </summary>
/// <param name="key">参数名称</param>
/// <param name="value">参数的值</param>
/// <returns>本客户端本身,便于链路调用</returns>
public Client InjectTextParam(String key, String value)
{
optionalTextParams.Add(key, value);
return this;
}
/// <summary>
/// 注入额外业务参数
/// </summary>
/// <param name="key">参数名称</param>
/// <param name="value">参数的值</param>
/// <returns>本客户端本身,便于链式调用</returns>
public Client InjectBizParam(String key, Object value)
{
optionalBizParams.Add(key, value);
return this;
}
/// <summary>
/// 获取Config中的配置项
/// </summary>
/// <param name="key">配置项的名称</param>
/// <returns>配置项的值</returns>
public string GetConfig(string key)
{
return context.GetConfig(key);
}
/// <summary>
/// 是否是证书模式
/// </summary>
/// <returns>truefalse不是</returns>
public bool IsCertMode()
{
return context.CertEnvironment != null;
}
/// <summary>
/// 获取时间戳格式yyyy-MM-dd HH:mm:ss
/// </summary>
/// <returns>当前时间戳</returns>
public string GetTimestamp()
{
return DateTime.UtcNow.AddHours(8).ToString("yyyy-MM-dd HH:mm:ss");
}
/// <summary>
/// 计算签名注意要去除key或value为null的键值对
/// </summary>
/// <param name="systemParams">系统参数集合</param>
/// <param name="bizParams">业务参数集合</param>
/// <param name="textParams">其他额外文本参数集合</param>
/// <param name="privateKey">私钥</param>
/// <returns>签名值的Base64串</returns>
public string Sign(Dictionary<string, string> systemParams, Dictionary<string, object> bizParams,
Dictionary<string, string> textParams, string privateKey)
{
IDictionary<string, string> sortedMap = GetSortedMap(systemParams, bizParams, textParams);
StringBuilder content = new StringBuilder();
foreach (var pair in sortedMap)
{
if (!string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
{
content.Append(pair.Key).Append("=").Append(pair.Value).Append("&");
}
}
if (content.Length > 0)
{
//去除尾巴上的&
content.Remove(content.Length - 1, 1);
}
return Signer.Sign(content.ToString(), privateKey);
}
private IDictionary<string, string> GetSortedMap(Dictionary<string, string> systemParams,
Dictionary<string, object> bizParams, Dictionary<string, string> textParams)
{
AddOtherParams(textParams, bizParams);
IDictionary<string, string> sortedMap = new SortedDictionary<string, string>(systemParams, StringComparer.Ordinal);
if (bizParams != null && bizParams.Count != 0)
{
sortedMap.Add(AlipayConstants.BIZ_CONTENT_FIELD, JsonUtil.ToJsonString(bizParams));
}
if (textParams != null)
{
foreach (var pair in textParams)
{
sortedMap.Add(pair.Key, pair.Value);
}
}
SetNotifyUrl(sortedMap);
return sortedMap;
}
private void SetNotifyUrl(IDictionary<string, string> paramters)
{
if (GetConfig(AlipayConstants.NOTIFY_URL_CONFIG_KEY) != null && !paramters.ContainsKey(AlipayConstants.NOTIFY_URL_FIELD))
{
paramters.Add(AlipayConstants.NOTIFY_URL_FIELD, GetConfig(AlipayConstants.NOTIFY_URL_CONFIG_KEY));
}
}
/// <summary>
/// 获取商户应用公钥证书序列号,从证书模式运行时环境对象中直接读取
/// </summary>
/// <returns>商户应用公钥证书序列号</returns>
public string GetMerchantCertSN()
{
if (context.CertEnvironment == null)
{
return null;
}
return context.CertEnvironment.MerchantCertSN;
}
/// <summary>
/// 获取支付宝根证书序列号,从证书模式运行时环境对象中直接读取
/// </summary>
/// <returns>支付宝根证书序列号</returns>
public string GetAlipayRootCertSN()
{
if (context.CertEnvironment == null)
{
return null;
}
return context.CertEnvironment.RootCertSN;
}
/// <summary>
/// 将业务参数和其他额外文本参数按www-form-urlencoded格式转换成HTTP Body中的字节数组注意要做URL Encode
/// </summary>
/// <param name="bizParams">业务参数</param>
/// <returns>HTTP Body中的字节数组</returns>
public byte[] ToUrlEncodedRequestBody(Dictionary<string, object> bizParams)
{
IDictionary<string, string> sortedMap = GetSortedMap(new Dictionary<string, string>(), bizParams, null);
return AlipayConstants.DEFAULT_CHARSET.GetBytes(BuildQueryString(sortedMap));
}
private string BuildQueryString(IDictionary<string, string> sortedMap)
{
StringBuilder content = new StringBuilder();
int index = 0;
foreach (var pair in sortedMap)
{
if (!string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
{
content.Append(index == 0 ? "" : "&")
.Append(pair.Key)
.Append("=")
.Append(HttpUtility.UrlEncode(pair.Value, AlipayConstants.DEFAULT_CHARSET));
index++;
}
}
return content.ToString();
}
/// <summary>
/// 生成随机分界符用于multipart格式的HTTP请求Body的多个字段间的分隔
/// </summary>
/// <returns>随机分界符</returns>
public string GetRandomBoundary()
{
return DateTime.Now.Ticks.ToString("X");
}
/// <summary>
/// 字符串拼接
/// </summary>
/// <param name="a">字符串a</param>
/// <param name="b">字符串b</param>
/// <returns>字符串a和b拼接后的字符串</returns>
public string ConcatStr(string a, string b)
{
return a + b;
}
/// <summary>
/// 将其他额外文本参数和文件参数按multipart/form-data格式转换成HTTP Body中的字节数组流
/// </summary>
/// <param name="textParams">其他额外文本参数</param>
/// <param name="fileParams">业务文件参数</param>
/// <param name="boundary">HTTP Body中multipart格式的分隔符</param>
/// <returns>Multipart格式的字节流</returns>
public Stream ToMultipartRequestBody(Dictionary<string, string> textParams, Dictionary<string, string> fileParams, string boundary)
{
MemoryStream stream = new MemoryStream();
//补充其他额外参数
AddOtherParams(textParams, null);
foreach (var pair in textParams)
{
if (!string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
{
MultipartUtil.WriteToStream(stream, MultipartUtil.GetEntryBoundary(boundary));
MultipartUtil.WriteToStream(stream, MultipartUtil.GetTextEntry(pair.Key, pair.Value));
}
}
//组装文件参数
foreach (var pair in fileParams)
{
if (!string.IsNullOrEmpty(pair.Key) && pair.Value != null)
{
MultipartUtil.WriteToStream(stream, MultipartUtil.GetEntryBoundary(boundary));
MultipartUtil.WriteToStream(stream, MultipartUtil.GetFileEntry(pair.Key, pair.Value));
MultipartUtil.WriteToStream(stream, File.ReadAllBytes(pair.Value));
}
}
//添加结束标记
MultipartUtil.WriteToStream(stream, MultipartUtil.GetEndBoundary(boundary));
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
/// <summary>
/// 将网关响应发序列化成Map同时将API的接口名称和响应原文插入到响应Map的method和body字段中
/// </summary>
/// <param name="response">HTTP响应</param>
/// <param name="method">调用的OpenAPI的接口名称</param>
/// <returns>响应反序列化的Map</returns>
public Dictionary<string, object> ReadAsJson(TeaResponse response, string method)
{
string responseBody = TeaCore.GetResponseBody(response);
Dictionary<string, object> dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(responseBody);
dictionary.Add(AlipayConstants.BODY_FIELD, responseBody);
dictionary.Add(AlipayConstants.METHOD_FIELD, method);
return DictionaryUtil.ObjToDictionary(dictionary);
}
/// <summary>
/// 适配Tea DSL自动生成的代码
/// </summary>
public async Task<Dictionary<string, object>> ReadAsJsonAsync(TeaResponse response, string method)
{
return ReadAsJson(response, method);
}
/// <summary>
/// 从响应Map中提取支付宝公钥证书序列号
/// </summary>
/// <param name="respMap">响应Map</param>
/// <returns>支付宝公钥证书序列号</returns>
public string GetAlipayCertSN(Dictionary<string, object> respMap)
{
return (string)respMap[AlipayConstants.ALIPAY_CERT_SN_FIELD];
}
/// <summary>
/// 获取支付宝公钥,从证书运行时环境对象中直接读取
/// 如果缓存的用户指定的支付宝公钥证书的序列号与网关响应中携带的支付宝公钥证书序列号不一致,需要报错给出提示或自动更新支付宝公钥证书
/// </summary>
/// <param name="alipayCertSN">网关响应中携带的支付宝公钥证书序列号</param>
/// <returns>支付宝公钥</returns>
public string ExtractAlipayPublicKey(string alipayCertSN)
{
if (context.CertEnvironment == null)
{
return null;
}
return context.CertEnvironment.GetAlipayPublicKey(alipayCertSN);
}
/// <summary>
/// 验证签名
/// </summary>
/// <param name="respMap">响应Map可以从中提取出sign和body</param>
/// <param name="alipayPublicKey">支付宝公钥</param>
/// <returns>true验签通过false验签不通过</returns>
public bool Verify(Dictionary<string, object> respMap, string alipayPublicKey)
{
string sign = (string)respMap[AlipayConstants.SIGN_FIELD];
string content = SignContentExtractor.GetSignSourceData((string)respMap[AlipayConstants.BODY_FIELD],
(string)respMap[AlipayConstants.METHOD_FIELD]);
return Signer.Verify(content, sign, alipayPublicKey);
}
/// <summary>
/// 从响应Map中提取返回值对象的Map并将响应原文插入到body字段中
/// </summary>
/// <param name="respMap">响应Map</param>
/// <returns>返回值对象Map</returns>
public Dictionary<string, object> ToRespModel(Dictionary<string, object> respMap)
{
string methodName = (string)respMap[AlipayConstants.METHOD_FIELD];
string responseNodeName = methodName.Replace('.', '_') + AlipayConstants.RESPONSE_SUFFIX;
string errorNodeName = AlipayConstants.ERROR_RESPONSE;
//先找正常响应节点
foreach (var pair in respMap)
{
if (responseNodeName.Equals(pair.Key))
{
Dictionary<string, object> model = (Dictionary<string, object>)pair.Value;
model.Add(AlipayConstants.BODY_FIELD, respMap[AlipayConstants.BODY_FIELD]);
return model;
}
}
//再找异常响应节点
foreach (var pair in respMap)
{
if (errorNodeName.Equals(pair.Key))
{
Dictionary<string, object> model = (Dictionary<string, object>)pair.Value;
model.Add(AlipayConstants.BODY_FIELD, respMap[AlipayConstants.BODY_FIELD]);
return model;
}
}
throw new Exception("响应格式不符合预期,找不到" + responseNodeName + "节点");
}
/// <summary>
/// 生成页面类请求所需URL或Form表单
/// </summary>
/// <param name="method">GET或POST决定是生成URL还是Form表单</param>
/// <param name="systemParams">系统参数集合</param>
/// <param name="bizParams">业务参数集合</param>
/// <param name="textParams">其他额外文本参数集合</param>
/// <param name="sign">所有参数的签名值</param>
/// <returns>生成的URL字符串或表单</returns>
public string GeneratePage(string method, Dictionary<string, string> systemParams, Dictionary<string, object> bizParams,
Dictionary<string, string> textParams, string sign)
{
if (AlipayConstants.GET.Equals(method))
{
//采集并排序所有参数
IDictionary<string, string> sortedMap = GetSortedMap(systemParams, bizParams, textParams);
sortedMap.Add(AlipayConstants.SIGN_FIELD, sign);
//将所有参数置于URL中
return GetGatewayServerUrl() + "?" + BuildQueryString(sortedMap);
}
else if (AlipayConstants.POST.Equals(method))
{
//将系统参数、额外文本参数排序后置于URL中
IDictionary<string, string> urlParams = GetSortedMap(systemParams, null, textParams);
urlParams.Add(AlipayConstants.SIGN_FIELD, sign);
string actionUrl = GetGatewayServerUrl() + "?" + BuildQueryString(urlParams);
//将业务参数排序后置于form表单中
AddOtherParams(null, bizParams);
IDictionary<string, string> formParams = new SortedDictionary<string, string>()
{
{ AlipayConstants.BIZ_CONTENT_FIELD, JsonUtil.ToJsonString(bizParams)}
};
return PageUtil.BuildForm(actionUrl, formParams);
}
else
{
throw new Exception("_generatePage中method只支持传入GET或POST");
}
}
/// <summary>
/// 生成订单串
/// </summary>
/// <param name="systemParams">系统参数集合</param>
/// <param name="bizParams">业务参数集合</param>
/// <param name="textParams">其他文本参数集合</param>
/// <param name="sign">所有参数的签名值</param>
/// <returns>订单串</returns>
public string GenerateOrderString(Dictionary<string, string> systemParams, Dictionary<string, object> bizParams,
Dictionary<string, string> textParams, string sign)
{
//采集并排序所有参数
IDictionary<string, string> sortedMap = GetSortedMap(systemParams, bizParams, textParams);
sortedMap.Add(AlipayConstants.SIGN_FIELD, sign);
//将所有参数置于URL中
return BuildQueryString(sortedMap);
}
private string GetGatewayServerUrl()
{
return GetConfig(AlipayConstants.PROTOCOL_CONFIG_KEY) + "://" + GetConfig(AlipayConstants.HOST_CONFIG_KEY) + "/gateway.do";
}
/// <summary>
/// AES加密
/// </summary>
/// <param name="plainText">明文</param>
/// <param name="key">密钥</param>
/// <returns>密文</returns>
public string AesEncrypt(string plainText, string key)
{
return AES.Encrypt(plainText, key);
}
/// <summary>
/// AES解密
/// </summary>
/// <param name="chiperText">密文</param>
/// <param name="key">密钥</param>
/// <returns>明文</returns>
public string AesDecrypt(string chiperText, string key)
{
return AES.Decrypt(chiperText, key);
}
/// <summary>
/// 对支付类请求的异步通知的参数集合进行验签
/// </summary>
/// <param name="parameters">参数集合</param>
/// <param name="alipayPublicKey">支付宝公钥</param>
/// <returns>true验证成功false验证失败</returns>
public bool VerifyParams(Dictionary<string, string> parameters, string alipayPublicKey)
{
return Signer.VerifyParams(parameters, alipayPublicKey);
}
/// <summary>
/// 获取SDK版本信息
/// </summary>
/// <returns>SDK版本信息</returns>
public string GetSdkVersion()
{
return context.SdkVersion;
}
/// <summary>
/// 将随机顺序的Map转换为有序的Map
/// </summary>
/// <param name="input">随机顺序的Map</param>
/// <returns>有序的Map</returns>
public Dictionary<string, string> SortMap(Dictionary<string, string> input)
{
//GO语言的Map是随机顺序的每次访问顺序都不同才需排序
return input;
}
private void AddOtherParams(Dictionary<string, string> textParams, Dictionary<string, object> bizParams)
{
//为null表示此处不是扩展此类参数的时机
if (textParams != null)
{
foreach (var pair in optionalTextParams)
{
if (!textParams.ContainsKey(pair.Key))
{
textParams.Add(pair.Key, pair.Value);
}
}
SetNotifyUrl(textParams);
}
//为null表示此处不是扩展此类参数的时机
if (bizParams != null)
{
foreach (var pair in optionalBizParams)
{
if (!bizParams.ContainsKey(pair.Key))
{
bizParams.Add(pair.Key, pair.Value);
}
}
}
}
}
}

View File

@@ -0,0 +1,106 @@
using Tea;
namespace Alipay.EasySDK.Kernel
{
/// <summary>
/// 客户端配置参数模型
/// </summary>
public class Config : TeaModel
{
/// <summary>
/// 通信协议通常填写https
/// </summary>
[NameInMap("protocol")]
[Validation(Required = true)]
public string Protocol { get; set; } = "https";
/// <summary>
/// 网关域名
/// 线上为openapi.alipay.com
/// 沙箱为openapi.alipaydev.com
/// </summary>
[NameInMap("gatewayHost")]
[Validation(Required = true)]
public string GatewayHost { get; set; } = "openapi.alipay.com";
/// <summary>
/// AppId
/// </summary>
[NameInMap("appId")]
[Validation(Required = true)]
public string AppId { get; set; }
/// <summary>
/// 签名类型Alipay Easy SDK只推荐使用RSA2估此处固定填写RSA2
/// </summary>
[NameInMap("signType")]
[Validation(Required = true)]
public string SignType { get; set; } = "RSA2";
/// <summary>
/// 支付宝公钥
/// </summary>
[NameInMap("alipayPublicKey")]
[Validation(Required = true)]
public string AlipayPublicKey { get; set; }
/// <summary>
/// 应用私钥
/// </summary>
[NameInMap("merchantPrivateKey")]
[Validation(Required = true)]
public string MerchantPrivateKey { get; set; }
/// <summary>
/// 应用公钥证书文件路径
/// </summary>
[NameInMap("merchantCertPath")]
[Validation(Required = true)]
public string MerchantCertPath { get; set; }
/// <summary>
/// 支付宝公钥证书文件路径
/// </summary>
[NameInMap("alipayCertPath")]
[Validation(Required = true)]
public string AlipayCertPath { get; set; }
/// <summary>
/// 支付宝根证书文件路径
/// </summary>
[NameInMap("alipayRootCertPath")]
[Validation(Required = true)]
public string AlipayRootCertPath { get; set; }
/// <summary>
/// 异步通知回调地址(可选)
/// </summary>
[NameInMap("notifyUrl")]
[Validation(Required = true)]
public string NotifyUrl { get; set; }
/// <summary>
/// AES密钥可选
/// </summary>
[NameInMap("encryptKey")]
[Validation(Required = true)]
public string EncryptKey { get; set; }
/// <summary>
/// 代理地址可选例如http://127.0.0.1:8080
/// </summary>
[NameInMap("httpProxy")]
[Validation(Required = true)]
public string HttpProxy { get; set; }
/// <summary>
/// 忽略证书校验(可选)
/// </summary>
[NameInMap("ignoreSSL")]
[Validation(Required = true)]
public string IgnoreSSL { get; set; }
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using Tea;
using Alipay.EasySDK.Kernel.Util;
namespace Alipay.EasySDK.Kernel
{
public class Context
{
/// <summary>
/// 客户端配置参数
/// </summary>
private readonly Dictionary<string, object> config;
/// <summary>
/// 证书模式运行时环境
/// </summary>
public CertEnvironment CertEnvironment { get; }
/// <summary>
/// SDK版本号
/// </summary>
public string SdkVersion { get; set; }
public Context(Config config, string sdkVersion)
{
this.config = config.ToMap();
SdkVersion = sdkVersion;
ArgumentValidator.CheckArgument(AlipayConstants.RSA2.Equals(GetConfig(AlipayConstants.SIGN_TYPE_CONFIG_KEY)),
"Alipay Easy SDK只允许使用RSA2签名方式RSA签名方式由于安全性相比RSA2弱已不再推荐。");
if (!string.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));
}
}
public string GetConfig(string key)
{
return (string)config[key];
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Text;
using System.Security.Cryptography;
namespace Alipay.EasySDK.Kernel.Util
{
public class AES
{
/// <summary>
/// 128位全0初始向量
/// </summary>
private static readonly byte[] AES_IV = InitIV(16);
/// <summary>
/// AES加密
/// </summary>
/// <param name="plainText">明文</param>
/// <param name="key">对称密钥</param>
/// <returns>密文</returns>
public static string Encrypt(string plainText, string key)
{
try
{
byte[] keyBytes = Convert.FromBase64String(key);
byte[] plainBytes = AlipayConstants.DEFAULT_CHARSET.GetBytes(plainText); ;
RijndaelManaged rijndatel = new RijndaelManaged
{
Key = keyBytes,
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7,
IV = AES_IV
};
ICryptoTransform transform = rijndatel.CreateEncryptor(rijndatel.Key, rijndatel.IV);
byte[] cipherBytes = transform.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
return Convert.ToBase64String(cipherBytes);
}
catch (Exception e)
{
throw new Exception("AES加密失败plainText=" + plainText +
"keySize=" + key.Length + "" + e.Message, e);
}
}
/// <summary>
/// AES解密
/// </summary>
/// <param name="cipherText">密文</param>
/// <param name="key">对称密钥</param>
/// <returns>明文</returns>
public static string Decrypt(string cipherText, string key)
{
try
{
byte[] keyBytes = Convert.FromBase64String(key);
byte[] cipherBytes = Convert.FromBase64String(cipherText);
RijndaelManaged rijndatel = new RijndaelManaged
{
Key = keyBytes,
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7,
IV = AES_IV
};
ICryptoTransform transform = rijndatel.CreateDecryptor(rijndatel.Key, rijndatel.IV);
byte[] plainBytes = transform.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return AlipayConstants.DEFAULT_CHARSET.GetString(plainBytes);
}
catch (Exception e)
{
throw new Exception("AES解密失败ciphertext=" + cipherText +
"keySize=" + key.Length + "" + e.Message, e);
}
}
private static byte[] InitIV(int blockSize)
{
byte[] iv = new byte[blockSize];
for (int i = 0; i < blockSize; ++i)
{
iv[i] = 0x0;
}
return iv;
}
}
}

View File

@@ -0,0 +1,326 @@
using System.Collections.Generic;
using System;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using System.Security.Cryptography;
using System.Text;
using System.Linq;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// 证书相关工具类
/// </summary>
public static class AntCertificationUtil
{
/// <summary>
/// 提取根证书序列号
/// </summary>
/// <param name="rootCertContent">根证书文本</param>
/// <returns>根证书序列号</returns>
public static string GetRootCertSN(string rootCertContent)
{
string rootCertSN = "";
try
{
List<X509Certificate> x509Certificates = ReadPemCertChain(rootCertContent);
foreach (X509Certificate cert in x509Certificates)
{
//只提取与指定算法类型匹配的证书的序列号
if (cert.SigAlgOid.StartsWith("1.2.840.113549.1.1", StringComparison.Ordinal))
{
string certSN = GetCertSN(cert);
if (string.IsNullOrEmpty(rootCertSN))
{
rootCertSN = certSN;
}
else
{
rootCertSN = rootCertSN + "_" + certSN;
}
}
}
}
catch (Exception ex)
{
throw new Exception("提取根证书序列号失败。" + ex.Message);
}
return rootCertSN;
}
/// <summary>
/// 反序列化证书文本
/// </summary>
/// <param name="certContent">证书文本</param>
/// <returns>X509Certificate证书对象</returns>
public static X509Certificate ParseCert(string certContent)
{
return new X509CertificateParser().ReadCertificate(Encoding.UTF8.GetBytes(certContent));
}
/// <summary>
/// 计算指定证书的序列号
/// </summary>
/// <param name="cert">证书</param>
/// <returns>序列号</returns>
public static string GetCertSN(X509Certificate cert)
{
string issuerDN = cert.IssuerDN.ToString();
//提取出的证书的issuerDN本身是以CN开头的则无需逆序直接返回
if (issuerDN.StartsWith("CN", StringComparison.Ordinal))
{
return CalculateMd5(issuerDN + cert.SerialNumber);
}
List<string> attributes = issuerDN.Split(',').ToList();
attributes.Reverse();
return CalculateMd5(string.Join(",", attributes.ToArray()) + cert.SerialNumber);
}
/// <summary>
/// 校验证书链是否可信
/// </summary>
/// <param name="certContent">需要验证的目标证书或者证书链文本</param>
/// <param name="rootCertContent">可信根证书列表文本</param>
/// <returns>true证书可信false证书不可信</returns>
public static bool IsTrusted(string certContent, string rootCertContent)
{
List<X509Certificate> certs = ReadPemCertChain(certContent);
List<X509Certificate> rootCerts = ReadPemCertChain(rootCertContent);
return VerifyCertChain(certs, rootCerts);
}
/// <summary>
/// 从证书链文本反序列化证书链集合
/// </summary>
/// <param name="cert">证书链文本</param>
/// <returns>证书链集合</returns>
private static List<X509Certificate> ReadPemCertChain(string cert)
{
System.Collections.ICollection collection = new X509CertificateParser().ReadCertificates(Encoding.UTF8.GetBytes(cert));
List<X509Certificate> result = new List<X509Certificate>();
foreach (var each in collection)
{
result.Add((X509Certificate)each);
}
return result;
}
/// <summary>
/// 将证书链按照完整的签发顺序进行排序,排序后证书链为:[issuerA, subjectA]-[issuerA, subjectB]-[issuerB, subjectC]-[issuerC, subjectD]...
/// </summary>
/// <param name="certChain">未排序的证书链</param>
/// <returns>true排序成功false证书链不完整</returns>
private static bool SortCertChain(List<X509Certificate> certChain)
{
//主题和证书的映射
Dictionary<X509Name, X509Certificate> subject2CertMap = new Dictionary<X509Name, X509Certificate>();
//签发者和证书的映射
Dictionary<X509Name, X509Certificate> issuer2CertMap = new Dictionary<X509Name, X509Certificate>();
//是否包含自签名证书
bool hasSelfSignedCert = false;
foreach (X509Certificate cert in certChain)
{
if (IsSelfSigned(cert))
{
if (hasSelfSignedCert)
{
//同一条证书链中只能有一个自签名证书
return false;
}
hasSelfSignedCert = true;
}
subject2CertMap[cert.SubjectDN] = cert;
issuer2CertMap[cert.IssuerDN] = cert;
}
List<X509Certificate> orderedCertChain = new List<X509Certificate>();
X509Certificate current = certChain[0];
AddressingUp(subject2CertMap, orderedCertChain, current);
AddressingDown(issuer2CertMap, orderedCertChain, current);
//说明证书链不完整
if (certChain.Count != orderedCertChain.Count)
{
return false;
}
//用排序后的结果覆盖传入的证书链集合
for (int i = 0; i < orderedCertChain.Count; i++)
{
certChain[i] = orderedCertChain[i];
}
return true;
}
private static bool IsSelfSigned(X509Certificate cert)
{
return cert.SubjectDN.Equivalent(cert.IssuerDN);
}
/// <summary>
/// 向上构造证书链
/// </summary>
/// <param name="subject2CertMap">主题与证书的映射</param>
/// <param name="orderedCertChain">储存排序后的证书链集合</param>
/// <param name="current">当前需要插入排序后的证书链集合中的证书</param>
private static void AddressingUp(Dictionary<X509Name, X509Certificate> subject2CertMap,
List<X509Certificate> orderedCertChain, X509Certificate current)
{
orderedCertChain.Insert(0, current);
if (IsSelfSigned(current))
{
return;
}
if (!subject2CertMap.ContainsKey(current.IssuerDN))
{
return;
}
X509Certificate issuer = subject2CertMap[current.IssuerDN];
AddressingUp(subject2CertMap, orderedCertChain, issuer);
}
/// <summary>
/// 向下构造证书链
/// </summary>
/// <param name="issuer2CertMap">签发者和证书的映射</param>
/// <param name="certChain">储存排序后的证书链集合</param>
/// <param name="current">当前需要插入排序后的证书链集合中的证书</param>
private static void AddressingDown(Dictionary<X509Name, X509Certificate> issuer2CertMap,
List<X509Certificate> certChain, X509Certificate current)
{
if (!issuer2CertMap.ContainsKey(current.SubjectDN))
{
return;
}
X509Certificate subject = issuer2CertMap[current.SubjectDN];
if (IsSelfSigned(subject))
{
return;
}
certChain.Add(subject);
AddressingDown(issuer2CertMap, certChain, subject);
}
/// <summary>
/// 验证证书是否是信任证书库中的证书签发的
/// </summary>
/// <param name="cert">待验证证书</param>
/// <param name="rootCerts">可信根证书列表</param>
/// <returns>true验证通过false验证不通过</returns>
private static bool VerifyCert(X509Certificate cert, List<X509Certificate> rootCerts)
{
if (!cert.IsValidNow)
{
return false;
}
Dictionary<X509Name, X509Certificate> subject2CertMap = new Dictionary<X509Name, X509Certificate>();
foreach (X509Certificate root in rootCerts)
{
subject2CertMap[root.SubjectDN] = root;
}
X509Name issuerDN = cert.IssuerDN;
if (!subject2CertMap.ContainsKey(issuerDN))
{
return false;
}
X509Certificate issuer = subject2CertMap[issuerDN];
try
{
AsymmetricKeyParameter publicKey = issuer.GetPublicKey();
cert.Verify(publicKey);
}
catch (Exception ex)
{
Console.WriteLine("证书验证出现异常。" + ex.Message);
return false;
}
return true;
}
/// <summary>
/// 验证证书列表
/// </summary>
/// <param name="certs">待验证的证书列表</param>
/// <param name="rootCerts">可信根证书列表</param>
/// <returns>true验证通过false验证不通过</returns>
private static bool VerifyCertChain(List<X509Certificate> certs, List<X509Certificate> rootCerts)
{
//证书列表排序,形成排序后的证书链
bool sorted = SortCertChain(certs);
if (!sorted)
{
//不是完整的证书链
return false;
}
//先验证第一个证书是不是信任库中证书签发的
X509Certificate previous = certs[0];
bool firstOK = VerifyCert(previous, rootCerts);
if (!firstOK || certs.Count == 1)
{
return firstOK;
}
//验证证书链
for (int i = 1; i < certs.Count; i++)
{
try
{
X509Certificate cert = certs[i];
if (!cert.IsValidNow)
{
return false;
}
//用上级证书的公钥验证本证书是否是上级证书签发的
cert.Verify(previous.GetPublicKey());
previous = cert;
}
catch (Exception ex)
{
//证书链验证失败
Console.WriteLine("证书链验证失败。" + ex.Message);
return false;
}
}
return true;
}
private static string CalculateMd5(string input)
{
using (MD5 md5 = new MD5CryptoServiceProvider())
{
string result = "";
byte[] bytes = md5.ComputeHash(Encoding.GetEncoding("utf-8").GetBytes(input));
for (int i = 0; i < bytes.Length; i++)
{
result += bytes[i].ToString("x2");
}
return result;
}
}
/// <summary>
/// 从证书中提取公钥并转换为PEM编码
/// </summary>
/// <param name="input">证书</param>
/// <returns>PEM编码公钥</returns>
public static string ExtractPemPublicKeyFromCert(X509Certificate input)
{
SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(input.GetPublicKey());
return Convert.ToBase64String(subjectPublicKeyInfo.GetDerEncoded());
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// 参数校验类
/// </summary>
public static class ArgumentValidator
{
public static void CheckArgument(bool expression, string errorMessage)
{
if (!expression)
{
throw new Exception(errorMessage);
}
}
public static void CheckNotNull(object value, string errorMessage)
{
if (value == null)
{
throw new Exception(errorMessage);
}
}
public static void EnsureNull(object value, string errorMessage)
{
if (value != null)
{
throw new Exception(errorMessage);
}
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// 字典工具类
/// </summary>
public static class DictionaryUtil
{
/// <summary>
/// 将字典各层次Value中的JObject和JArray转换成C#标准库中的Dictionary和List
/// </summary>
/// <param name="dicObj">输入字典</param>
/// <returns>转换后的输出字典</returns>
public static Dictionary<string, object> ObjToDictionary(Dictionary<string, object> dicObj)
{
Dictionary<string, object> dic = new Dictionary<string, object>();
foreach (string key in dicObj.Keys)
{
if (dicObj[key] is JArray)
{
List<Dictionary<string, object>> dicObjList = ((JArray)dicObj[key]).ToObject<List<Dictionary<string, object>>>();
List<Dictionary<string, object>> dicList = new List<Dictionary<string, object>>();
foreach (Dictionary<string, object> objItem in dicObjList)
{
dicList.Add(ObjToDictionary(objItem));
}
dic.Add(key, dicList);
}
else if (dicObj[key] is JObject)
{
Dictionary<string, object> dicJObj = ((JObject)dicObj[key]).ToObject<Dictionary<string, object>>();
dic.Add(key, ObjToDictionary(dicJObj));
}
else
{
dic.Add(key, dicObj[key]);
}
}
return dic;
}
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
using Tea;
using Newtonsoft.Json;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// JSON工具类
/// </summary>
public class JsonUtil
{
/// <summary>
/// 将字典集合转换为Json字符串转换过程中对于TeaModel使用标注的字段名称而不是字段的变量名
/// </summary>
/// <param name="input">字典集合</param>
/// <returns>Json字符串</returns>
public static string ToJsonString(IDictionary<string, object> input)
{
IDictionary<string, object> result = new Dictionary<string, object>();
foreach (var pair in input)
{
if (pair.Value is TeaModel)
{
result.Add(pair.Key, GetTeaModelMap((TeaModel)pair.Value));
}
else
{
result.Add(pair.Key, pair.Value);
}
}
return JsonConvert.SerializeObject(result);
}
private static IDictionary<string, object> GetTeaModelMap(TeaModel teaModel)
{
IDictionary<string, object> result = new Dictionary<string, object>();
IDictionary<string, object> teaModelMap = teaModel.ToMap();
foreach (var pair in teaModelMap)
{
if (pair.Value is TeaModel)
{
result.Add(pair.Key, GetTeaModelMap((TeaModel)pair.Value));
}
else
{
result.Add(pair.Key, pair.Value);
}
}
return result;
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Text;
using System.IO;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// HTTP multipart/form-data格式相关工具类
/// </summary>
public static class MultipartUtil
{
/// <summary>
/// 获取Multipart分界符
/// </summary>
/// <param name="boundary">用作分界的随机字符串</param>
/// <returns>Multipart分界符</returns>
public static byte[] GetEntryBoundary(string boundary)
{
return Encoding.UTF8.GetBytes("\r\n--" + boundary + "\r\n");
}
/// <summary>
/// 获取Multipart结束标记
/// </summary>
/// <param name="boundary">用作分界的随机字符串</param>
/// <returns>Multipart结束标记</returns>
public static byte[] GetEndBoundary(string boundary)
{
return Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n");
}
/// <summary>
/// 获取Multipart中的文本参数结构
/// </summary>
/// <param name="fieldName">字段名称</param>
/// <param name="fieldValue">字段值</param>
/// <returns>文本参数结构</returns>
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 AlipayConstants.DEFAULT_CHARSET.GetBytes(entry);
}
/// <summary>
/// 获取Multipart中的文件参数结构不含文件内容只有文件元数据
/// </summary>
/// <param name="fieldName">字段名称</param>
/// <param name="filePath">文件路径</param>
/// <returns>文件参数结构(不含文件内容)</returns>
public static byte[] GetFileEntry(String fieldName, String filePath)
{
ArgumentValidator.CheckArgument(File.Exists(filePath),
Path.GetFullPath(filePath) + "文件不存在");
ArgumentValidator.CheckArgument(Path.GetFileName(filePath).Contains("."),
"文件名必须带上正确的扩展名");
String entry = "Content-Disposition:form-data;name=\""
+ fieldName
+ "\";filename=\""
+ Path.GetFileName(filePath)
+ "\"\r\nContent-Type:application/octet-stream"
+ "\r\n\r\n";
return AlipayConstants.DEFAULT_CHARSET.GetBytes(entry);
}
/// <summary>
/// 往指定流中写入整个字节数组
/// </summary>
/// <param name="stream">流</param>
/// <param name="content">字节数组</param>
public static void WriteToStream(Stream stream, byte[] content)
{
stream.Write(content, 0, content.Length);
}
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// 生成页面信息辅助类
/// </summary>
public static class PageUtil
{
/// <summary>
/// 生成表单
/// </summary>
/// <param name="actionUrl">表单提交链接</param>
/// <param name="parameters">表单参数</param>
/// <returns>表单字符串</returns>
public static string BuildForm(string actionUrl, IDictionary<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(IDictionary<string, string> parameters)
{
if (parameters == null || parameters.Count == 0)
{
return "";
}
StringBuilder stringBuilder = new StringBuilder();
foreach (var pair in parameters)
{
if (pair.Key == null || pair.Value == null)
{
continue;
}
stringBuilder.Append(BuildHiddenField(pair.Key, pair.Value));
}
return stringBuilder.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,31 @@
using System.Reflection;
using Tea;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// 响应检查工具类
/// </summary>
public class ResponseChecker
{
public const string SUB_CODE_FIELD_NAME = "SubCode";
/// <summary>
/// 判断一个请求返回的响应是否成功
/// </summary>
/// <param name="response">响应对象</param>
/// <returns>true成功false失败</returns>
public static bool Success(TeaModel response)
{
PropertyInfo propertyInfo = response.GetType().GetProperty(SUB_CODE_FIELD_NAME);
if (propertyInfo == null)
{
//没有SubCode属性的响应对象通常是那些无需跟网关远程通信的API只要本地执行完成都视为成功
return true;
}
string subCode = (string)propertyInfo.GetValue(response);
return string.IsNullOrEmpty(subCode);
}
}
}

View File

@@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// 待验签原文提取器
/// 注此处不可使用JSON反序列化工具进行提取会破坏原有格式对于签名而言差个空格都会验签不通过
/// </summary>
public class SignContentExtractor
{
/// <summary>
/// 左大括号
/// </summary>
public const char LEFT_BRACE = '{';
/// <summary>
/// 右大括号
/// </summary>
public const char RIGHT_BRACE = '}';
/// <summary>
/// 双引号
/// </summary>
public const char DOUBLE_QUOTES = '"';
/// <summary>
/// 获取待验签的原文
/// </summary>
/// <param name="body">网关的整体响应字符串</param>
/// <param name="method">本次调用的OpenAPI接口名称</param>
/// <returns>待验签的原文</returns>
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, StringComparison.Ordinal);
int indexOfErrorRoot = body.IndexOf(errorRootNode, StringComparison.Ordinal);
string result = null;
if (indexOfRootNode > 0)
{
result = ParseSignSourceData(body, rootNode, indexOfRootNode);
}
else if (indexOfErrorRoot > 0)
{
result = ParseSignSourceData(body, errorRootNode, indexOfErrorRoot);
}
return result;
}
private static string ParseSignSourceData(string body, string rootNode, int indexOfRootNode)
{
int signDataStartIndex = indexOfRootNode + rootNode.Length + 2;
int indexOfSign = body.IndexOf("\"" + AlipayConstants.SIGN_FIELD + "\"", StringComparison.Ordinal);
if (indexOfSign < 0)
{
return null;
}
SignSourceData signSourceData = ExtractSignContent(body, signDataStartIndex);
//如果提取的待验签原始内容后还有rootNode
if (body.LastIndexOf(rootNode, StringComparison.Ordinal) > signSourceData.EndIndex)
{
throw new Exception("检测到响应报文中有重复的" + rootNode + ",验签失败。");
}
return signSourceData.SourceData;
}
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()
{
SourceData = str.Substring(beginIndex, endIndex - beginIndex),
BeginIndex = beginIndex,
EndIndex = endIndex
};
}
private static int ExtractBeginPosition(string responseString, int begin)
{
int beginPosition = begin;
//找到第一个左大括号对应响应的是JSON对象的情况普通调用OpenAPI响应明文
//或者双引号对应响应的是JSON字符串的情况加密调用OpenAPI响应Base64串作为待验签内容的起点
while (beginPosition < responseString.Length
&& responseString[beginPosition] != LEFT_BRACE
&& responseString[beginPosition] != DOUBLE_QUOTES)
{
++beginPosition;
}
return beginPosition;
}
private static int ExtractEndPosition(string responseString, int beginPosition)
{
//提取明文验签内容终点
if (responseString[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[index] == DOUBLE_QUOTES && index != beginPosition)
{
return index + 1;
}
}
//如果没有找到第2个双引号说明验签内容片段提取失败直接尝试选取剩余整个响应字符串进行验签
return responseString.Length;
}
private static int ExtractJsonObjectEndPosition(string responseString, int beginPosition)
{
//记录当前尚未发现配对闭合的大括号
LinkedList<char> braces = new LinkedList<char>();
//记录当前字符是否在双引号中
bool inQuotes = false;
//记录当前字符前面连续的转义字符个数
int consecutiveEscapeCount = 0;
//从待验签字符的起点开始遍历后续字符串,找出待验签字符串的终止点,终点即是与起点{配对的}
for (int index = beginPosition; index < responseString.Length; ++index)
{
//提取当前字符
char currentChar = responseString[index];
//如果当前字符是"且前面有偶数个转义标记0也是偶数
if (currentChar == DOUBLE_QUOTES && consecutiveEscapeCount % 2 == 0)
{
//是否在引号中的状态取反
inQuotes = !inQuotes;
}
//如果当前字符是{且不在引号中
else if (currentChar == LEFT_BRACE && !inQuotes)
{
//将该{加入未闭合括号中
braces.AddLast(LEFT_BRACE);
}
//如果当前字符是}且不在引号中
else if (currentChar == RIGHT_BRACE && !inQuotes)
{
//弹出一个未闭合括号
braces.RemoveLast();
//如果弹出后,未闭合括号为空,说明已经找到终点
if (braces.Count == 0)
{
return index + 1;
}
}
//如果当前字符是转义字符
if (currentChar == '\\')
{
//连续转义字符个数+1
++consecutiveEscapeCount;
}
else
{
//连续转义字符个数置0
consecutiveEscapeCount = 0;
}
}
//如果没有找到配对的闭合括号,说明验签内容片段提取失败,直接尝试选取剩余整个响应字符串进行验签
return responseString.Length;
}
/// <summary>
/// 从响应字符串中提取到的待验签原始内容
/// </summary>
public class SignSourceData
{
/// <summary>
/// 待验签原始内容
/// </summary>
public string SourceData { get; set; }
/// <summary>
/// 待验签原始内容在响应字符串中的起始位置
/// </summary>
public int BeginIndex { get; set; }
/// <summary>
/// 待验签原始内容在响应字符串中的结束位置
/// </summary>
public int EndIndex { get; set; }
}
}
}

View File

@@ -0,0 +1,260 @@
using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Collections.Generic;
namespace Alipay.EasySDK.Kernel.Util
{
/// <summary>
/// SHA256WithRSA签名器
/// </summary>
public class Signer
{
/// <summary>
/// 计算签名
/// </summary>
/// <param name="content">待签名的内容</param>
/// <param name="privateKeyPem">私钥</param>
/// <returns>签名值的Base64串</returns>
public static string Sign(string content, string privateKeyPem)
{
try
{
using (RSACryptoServiceProvider rsaService = BuildRSAServiceProvider(Convert.FromBase64String(privateKeyPem)))
{
byte[] data = AlipayConstants.DEFAULT_CHARSET.GetBytes(content);
byte[] sign = rsaService.SignData(data, "SHA256");
return Convert.ToBase64String(sign);
}
}
catch (Exception e)
{
string errorMessage = "签名遭遇异常content=" + content + " privateKeySize=" + privateKeyPem.Length + " reason=" + e.Message;
Console.WriteLine(errorMessage);
throw new Exception(errorMessage, e);
}
}
/// <summary>
/// 验证签名
/// </summary>
/// <param name="content">待验签的内容</param>
/// <param name="sign">签名值的Base64串</param>
/// <param name="publicKeyPem">支付宝公钥</param>
/// <returns>true验证成功false验证失败</returns>
public static bool Verify(string content, string sign, string publicKeyPem)
{
try
{
using (RSACryptoServiceProvider rsaService = new RSACryptoServiceProvider())
{
rsaService.PersistKeyInCsp = false;
rsaService.ImportParameters(ConvertFromPemPublicKey(publicKeyPem));
return rsaService.VerifyData(AlipayConstants.DEFAULT_CHARSET.GetBytes(content),
"SHA256", Convert.FromBase64String(sign));
}
}
catch (Exception e)
{
string errorMessage = "验签遭遇异常content=" + content + " sign=" + sign +
" publicKey=" + publicKeyPem + " reason=" + e.Message;
Console.WriteLine(errorMessage);
throw new Exception(errorMessage, e);
}
}
/// <summary>
/// 对参数集合进行验签
/// </summary>
/// <param name="parameters">参数集合</param>
/// <param name="publicKeyPem">支付宝公钥</param>
/// <returns>true验证成功false验证失败</returns>
public static bool VerifyParams(Dictionary<string, string> parameters, string publicKeyPem)
{
string sign = parameters[AlipayConstants.SIGN_FIELD];
parameters.Remove(AlipayConstants.SIGN_FIELD);
parameters.Remove(AlipayConstants.SIGN_TYPE_FIELD);
string content = GetSignContent(parameters);
return Verify(content, sign, publicKeyPem);
}
private static string GetSignContent(IDictionary<string, string> parameters)
{
// 把字典按Key的字母顺序排序
IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters, StringComparer.Ordinal);
IEnumerator<KeyValuePair<string, string>> iterator = sortedParams.GetEnumerator();
// 把所有参数名和参数值串在一起
StringBuilder query = new StringBuilder("");
while (iterator.MoveNext())
{
string key = iterator.Current.Key;
string value = iterator.Current.Value;
query.Append(key).Append("=").Append(value).Append("&");
}
string content = query.ToString().Substring(0, query.Length - 1);
return content;
}
private static RSAParameters ConvertFromPemPublicKey(string pemPublickKey)
{
if (string.IsNullOrEmpty(pemPublickKey))
{
throw new Exception("PEM格式公钥不可为空。");
}
//移除干扰文本
pemPublickKey = pemPublickKey.Replace("-----BEGIN PUBLIC KEY-----", "")
.Replace("-----END PUBLIC KEY-----", "").Replace("\n", "").Replace("\r", "");
byte[] keyData = Convert.FromBase64String(pemPublickKey);
bool keySize1024 = (keyData.Length == 162);
bool keySize2048 = (keyData.Length == 294);
if (!(keySize1024 || keySize2048))
{
throw new Exception("公钥长度只支持1024和2048。");
}
byte[] pemModulus = (keySize1024 ? new byte[128] : new byte[256]);
byte[] pemPublicExponent = new byte[3];
Array.Copy(keyData, (keySize1024 ? 29 : 33), pemModulus, 0, (keySize1024 ? 128 : 256));
Array.Copy(keyData, (keySize1024 ? 159 : 291), pemPublicExponent, 0, 3);
RSAParameters para = new RSAParameters
{
Modulus = pemModulus,
Exponent = pemPublicExponent
};
return para;
}
private static RSACryptoServiceProvider BuildRSAServiceProvider(byte[] privateKey)
{
byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;
byte bt = 0;
ushort twobytes = 0;
int elems = 0;
//set up stream to decode the asn.1 encoded RSA private key
//wrap Memory Stream with BinaryReader for easy reading
using (BinaryReader binaryReader = new BinaryReader(new MemoryStream(privateKey)))
{
twobytes = binaryReader.ReadUInt16();
//data read as little endian order (actual data order for Sequence is 30 81)
if (twobytes == 0x8130)
{
//advance 1 byte
binaryReader.ReadByte();
}
else if (twobytes == 0x8230)
{
//advance 2 bytes
binaryReader.ReadInt16();
}
else
{
return null;
}
twobytes = binaryReader.ReadUInt16();
//version number
if (twobytes != 0x0102)
{
return null;
}
bt = binaryReader.ReadByte();
if (bt != 0x00)
{
return null;
}
//all private key components are Integer sequences
elems = GetIntegerSize(binaryReader);
MODULUS = binaryReader.ReadBytes(elems);
elems = GetIntegerSize(binaryReader);
E = binaryReader.ReadBytes(elems);
elems = GetIntegerSize(binaryReader);
D = binaryReader.ReadBytes(elems);
elems = GetIntegerSize(binaryReader);
P = binaryReader.ReadBytes(elems);
elems = GetIntegerSize(binaryReader);
Q = binaryReader.ReadBytes(elems);
elems = GetIntegerSize(binaryReader);
DP = binaryReader.ReadBytes(elems);
elems = GetIntegerSize(binaryReader);
DQ = binaryReader.ReadBytes(elems);
elems = GetIntegerSize(binaryReader);
IQ = binaryReader.ReadBytes(elems);
//create RSACryptoServiceProvider instance and initialize with public key
RSACryptoServiceProvider rsaService = new RSACryptoServiceProvider();
RSAParameters rsaParams = new RSAParameters
{
Modulus = MODULUS,
Exponent = E,
D = D,
P = P,
Q = Q,
DP = DP,
DQ = DQ,
InverseQ = IQ
};
rsaService.ImportParameters(rsaParams);
return rsaService;
}
}
private static int GetIntegerSize(BinaryReader binaryReader)
{
byte bt = 0;
byte lowbyte = 0x00;
byte highbyte = 0x00;
int count = 0;
bt = binaryReader.ReadByte();
//expect integer
if (bt != 0x02)
{
return 0;
}
bt = binaryReader.ReadByte();
if (bt == 0x81)
{
//data size in next byte
count = binaryReader.ReadByte();
}
else if (bt == 0x82)
{
//data size in next 2 bytes
highbyte = binaryReader.ReadByte();
lowbyte = binaryReader.ReadByte();
byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
count = BitConverter.ToInt32(modint, 0);
}
else
{
//we already have the data size
count = bt;
}
while (binaryReader.ReadByte() == 0x00)
{ //remove high order zeros in data
count -= 1;
}
//last ReadByte wasn't a removed zero, so back up a byte
binaryReader.BaseStream.Seek(-1, SeekOrigin.Current);
return count;
}
}
}

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);
}
}