基于C#/.NET Core+实现AS2服务端接收AS2消息的开发咨询
C#/.NET Core 原生实现AS2服务端相关说明
公开资料匮乏的核心原因
除AS2协议推出时间较早外,还有三个核心原因导致相关参考内容少:
- AS2属于EDI/B2B交易的垂直领域协议,商用场景长期被BizTalk、MuleSoft、商业EDI SDK等成熟付费方案垄断,开源社区从零自研的案例基数本身就极小
- 协议逻辑深度绑定S/MIME、CMS加密签名规范,细节兼容性坑极多,多数自研实现都是针对自身对接方的定制化开发,适配不同厂商的非标扩展后代码通用性差,很少有人公开完整可运行的版本
- 生产环境的AS2实现涉及企业交易证书、报文敏感信息,企业很少会把内部生产级的实现代码对外公开,网络上能找到的大多是零散的发送端片段,缺少接收端签名校验、MDN返回的完整逻辑
前置证书配置要求
- 服务端需要准备两类证书,无需购买专门的AS2证书,普通X509证书即可满足要求:
- HTTPS传输层证书:和普通WebAPI使用的SSL证书要求一致,用于HTTPS链路加密
- AS2业务层证书对:
- 己方私钥证书(
.pfx格式,带访问密码,存储在服务端安全目录,严禁外泄):用于解密对方用己方公钥加密的消息,以及给MDN回执做签名 - 对接方公钥证书(
.cer格式,存储在服务端信任证书目录):用于校验对方发来消息的签名,以及向对方发送消息时做内容加密
- 己方私钥证书(
- 算法兼容提示:主流AS2对接场景常用签名算法为
SHA256withRSA,常用加密算法为AES-256-CBC/3DES,提前和对接方确认支持的算法列表避免兼容问题。
核心功能实现示例
实现不需要依赖第三方商业AS2组件,仅需引入两个官方维护的免费包:用于CMS加解密签名的System.Security.Cryptography.Pkcs,以及用于解析多部分请求的Microsoft.AspNetCore.WebUtilities
1. HTTPS接收端点配置
直接基于ASP.NET Core原生接口实现,关闭默认模型绑定,直接读取原始请求流,避免框架自动处理请求导致报文字节被修改:
var builder = WebApplication.CreateBuilder(args); // 允许同步读取原始请求流 builder.Services.Configure<IISServerOptions>(opt => opt.AllowSynchronousIO = true); builder.Services.Configure<KestrelServerOptions>(opt => opt.AllowSynchronousIO = true); var app = builder.Build(); // AS2固定接收端点,对接方会将消息POST到该地址 app.MapPost("/as2/receive", async (HttpContext context) => { // 读取AS2协议要求的核心请求头 var as2Meta = new { MessageId = context.Request.Headers["Message-Id"].FirstOrDefault(), As2From = context.Request.Headers["AS2-From"].FirstOrDefault(), As2To = context.Request.Headers["AS2-To"].FirstOrDefault(), RequireMdnSign = context.Request.Headers["Disposition-Notification-Options"].ToString().Contains("signed-receipt-protocol"), MicAlgorithm = context.Request.Headers["Signed-Receipt-MICAlg"].FirstOrDefault() ?? "sha-256" }; // 读取原始请求字节,不要做任何编码转换 using var ms = new MemoryStream(); await context.Request.Body.CopyToAsync(ms); var rawBytes = ms.ToArray(); var requestContentType = context.Request.ContentType ?? string.Empty; // 执行签名校验、解密逻辑 var (signValid, decryptedXml, micValue) = ProcessAs2Payload(rawBytes, requestContentType, as2Meta.MicAlgorithm); // 生成MDN回执 var mdnContent = BuildSyncMdn(as2Meta, signValid, micValue); // 返回同步MDN context.Response.ContentType = "message/disposition-notification"; await context.Response.WriteAsync(mdnContent); }); app.Run();
2. 签名校验与解密逻辑
private (bool signValid, string decryptedXml, string micValue) ProcessAs2Payload(byte[] rawBytes, string contentType, string micAlg) { byte[] payloadToDecrypt = rawBytes; bool signPass = false; string mic = string.Empty; // 先处理带签名的multipart/signed格式报文 if (contentType.Contains("multipart/signed", StringComparison.OrdinalIgnoreCase)) { // 解析多部分内容:第一部分为业务报文,第二部分为签名值 var reader = new MultipartReader(contentType, new MemoryStream(rawBytes)); var firstSection = reader.ReadNextSectionAsync().Result; var businessPayload = firstSection!.Body.ReadAllBytes(); var signSection = reader.ReadNextSectionAsync().Result; var signBytes = signSection!.Body.ReadAllBytes(); // 加载对接方公钥校验签名,B2B场景大多无需校验CA证书链,仅验证公钥匹配即可 var partnerCert = new X509Certificate2(Path.Combine("certs", "partner-public.cer")); var signedCms = new SignedCms(new ContentInfo(businessPayload), true); signedCms.Decode(signBytes); try { signedCms.CheckSignature(new X509Certificate2Collection(partnerCert), false); signPass = true; // 计算MIC值,必须基于原始业务报文字节计算,禁止转字符串后重编码 using var hashAlg = micAlg == "sha-256" ? SHA256.Create() : SHA1.Create(); var hash = hashAlg.ComputeHash(businessPayload); mic = $"{Convert.ToBase64String(hash)}, {micAlg}"; payloadToDecrypt = businessPayload; } catch { signPass = false; } } // 签名校验通过后执行解密 string xmlContent = string.Empty; if (signPass) { var envelopedCms = new EnvelopedCms(); envelopedCms.Decode(payloadToDecrypt); // 加载己方私钥证书解密 var myCert = new X509Certificate2(Path.Combine("certs", "my-private.pfx"), "your-cert-password"); envelopedCms.Decrypt(new X509Certificate2Collection(myCert)); xmlContent = Encoding.UTF8.GetString(envelopedCms.ContentInfo.Content); } return (signPass, xmlContent, mic); }
3. MDN回执生成逻辑
private string BuildSyncMdn(dynamic as2Meta, bool processSuccess, string micValue) { var mdnBuilder = new StringBuilder(); mdnBuilder.AppendLine("Reporting-UA: SelfHosted-AS2-Server/1.0"); mdnBuilder.AppendLine($"Original-Message-ID: {as2Meta.MessageId}"); mdnBuilder.AppendLine($"Final-Recipient: rfc822; {as2Meta.As2To}"); mdnBuilder.AppendLine($"Original-Recipient: rfc822; {as2Meta.As2To}"); if (processSuccess) { mdnBuilder.AppendLine("Disposition: automatic-action/MDN-sent-automatically; processed"); if (!string.IsNullOrEmpty(micValue)) { mdnBuilder.AppendLine($"Received-Content-MIC: {micValue}"); } } else { mdnBuilder.AppendLine("Disposition: automatic-action/MDN-sent-automatically; processed/error: signature-check-failed"); } // 如果对接方要求签名MDN,可参考报文签名逻辑,用己方私钥对上述内容签名后返回即可 return mdnBuilder.ToString(); }
开发注意事项
- 所有AS2协议头名称必须严格遵循规范,例如
AS2-From、AS2-To、Message-Id,头信息缺失或拼写错误会直接被对接方判定为消息无效 - MIC值计算必须基于原始报文字节做哈希,禁止将字节转字符串后重新编码计算,否则会因为换行符、编码差异导致MIC不匹配,对接方会判定消息传输失败
- 如果对接方开启了AS2消息压缩,解密后得到的是zlib压缩字节流,需要先做解压再提取XML内容,压缩标识会在请求头
Content-Transfer-Encoding中标注 - 若对接方要求异步MDN,接收消息后先返回200状态码,后台单独将签名后的MDN内容POST到对方指定的
Receipt-Delivery-Option地址即可,无需在当前请求响应中返回MDN
内容的提问来源于stack exchange,提问作者imsan
相关产品推荐
相关产品推荐

