You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于C#/.NET Core+实现AS2服务端接收AS2消息的开发咨询

C#/.NET Core 原生实现AS2服务端相关说明

公开资料匮乏的核心原因

除AS2协议推出时间较早外,还有三个核心原因导致相关参考内容少:

  • AS2属于EDI/B2B交易的垂直领域协议,商用场景长期被BizTalk、MuleSoft、商业EDI SDK等成熟付费方案垄断,开源社区从零自研的案例基数本身就极小
  • 协议逻辑深度绑定S/MIME、CMS加密签名规范,细节兼容性坑极多,多数自研实现都是针对自身对接方的定制化开发,适配不同厂商的非标扩展后代码通用性差,很少有人公开完整可运行的版本
  • 生产环境的AS2实现涉及企业交易证书、报文敏感信息,企业很少会把内部生产级的实现代码对外公开,网络上能找到的大多是零散的发送端片段,缺少接收端签名校验、MDN返回的完整逻辑

前置证书配置要求

  • 服务端需要准备两类证书,无需购买专门的AS2证书,普通X509证书即可满足要求:
    1. HTTPS传输层证书:和普通WebAPI使用的SSL证书要求一致,用于HTTPS链路加密
    2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 15:21:25