添加网站文件

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