去年我们团队接了一个需求:为公司内部及合作方提供一套电子合同签署平台,替代原来邮件往返打印盖章的流程。项目从需求调研到上线迭代,前后跑了差不多五个月,技术栈选的是.NET 8加C#,前端用Vue,PDF签章基于iText7和自建的CA证书体系。整个过程踩了不少坑,也积累了一些可以复用的设计经验,这篇文章把这些内容整理出来,供准备做类似系统的开发者参考。先看一张系统整体架构的示意图。

一、电子合同系统的核心组成与整体架构设计
动手写代码之前,必须先把电子合同的法律效力和技术边界搞清楚。一套完整的电子合同系统通常包含四个核心模块:合同模板与文档生成、实名身份认证、数字签章、以及签署流程管理。很多人把电子签章等同于图片盖章,这其实是最大的误区。法律上有效的电子签名要求签署行为可追溯、签署内容不可篡改、签署人身份可确认,这就意味着单纯往PDF上贴一张公章图片是没有任何法律效力的。
我们的架构分层设计如下:接入层负责API网关与鉴权,业务层处理合同起草、审批、流转,签名层封装证书管理与PDF签章能力,最底层是存储与存证服务。签名层是整个系统的核心,它对外只暴露两个方法:对文档计算摘要并签名,以及对已签署文档做完整性校验。这种封装方式的好处是,后续无论是接入第三方CA还是自建证书链,业务层代码都不需要变动。
数据库设计上有几张关键表:合同主表存储合同元信息与当前状态,签署方表记录每个签署人的身份信息、认证方式和签署状态,签署记录表则保存每一次签署操作的时间戳、IP、证书指纹等证据链数据。这些数据看起来冗余,但在发生纠纷时就是关键的存证材料,建议在设计阶段就预留完整。
二、PDF数字签章的实现原理与C#代码实践
PDF签章的技术核心是数字签名。简单说,签署时系统会对PDF文件字节流计算哈希摘要,再用签署人的私钥对这个摘要进行RSA或SM2加密,签名结果连同证书信息一起写入PDF的签名域。校验时用公钥解密签名并与重新计算的摘要比对,一旦文件被改动哪怕一个字节,摘要就不匹配,签名立即失效。
下面是一段简化后的签章核心代码,基于iText7实现,展示了如何加载证书并对指定位置盖章:
using iText.Kernel.Pdf;
using iText.Signatures;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
public byte[] SignPdf(byte[] pdfBytes, string pfxPath, string pfxPassword,
int pageNumber, float x, float y, string reason)
{
// 加载PKCS12格式的签署证书
var cert = new Pkcs12Store(new FileStream(pfxPath, FileMode.Open),
pfxPassword.ToCharArray());
string alias = null;
foreach (var a in cert.Aliases)
{
if (cert.IsKeyEntry(a)) { alias = a; break; }
}
var entry = cert.GetKey(alias);
var chain = new X509CertificateEntry[entry.CertificateChain.Length];
for (int i = 0; i < chain.Length; i++)
{
// 构建证书链
chain[i] = new X509CertificateEntry(
DotNetUtilities.FromX509Certificate(
new System.Security.Cryptography.X509Certificates.X509Certificate2(
entry.CertificateChain[i].Certificate.GetEncoded())));
}
using var reader = new PdfReader(new MemoryStream(pdfBytes));
using var output = new MemoryStream();
var stamper = PdfSigner.Sign(reader, output,
new StampingProperties().UseAppendMode());
// 设置签章外观:位置、图片、签名原因
var appearance = stamper.GetSignatureAppearance()
.SetReason(reason)
.SetPageNumber(pageNumber)
.SetPosition(x, y);
// 外部签名容器:摘要算法为SHA256
IExternalSignature signature = new PrivateKeySignature(
entry.Key, DigestAlgorithms.SHA256);
stamper.SignDetached(appearance, signature, chain, null, null, null, 0,
CryptoStandard.CMS);
return output.ToArray();
}这里有几个实践要点值得展开。第一,一定要使用追加模式(UseAppendMode),这样后续签署人在已签署的文档上继续盖章时,前一手签名不会被破坏,这是多方签署场景的基础。第二,签名摘要算法建议用SHA256及以上,如果客户有国密要求,可以换成SM3摘要加SM2签名,需要引入BouncyCastle的国密实现。第三,时间戳服务不能省,通过TSA服务器给签名加上可信时间,可以证明签署发生的确切时间,防止签署人事后否认。
另一个容易忽略的细节是签章图片的处理。公章图片必须是带透明通道的PNG,并且要按PDF坐标系的DPI换算实际尺寸,否则盖出来的章会变形或模糊。我们一开始没注意DPI换算,测试时章盖出来比实际公章大了一圈,被业务方当场退回。
三、签署流程状态机设计与并发场景处理
合同签署流程本质上是一个状态机:草稿、待签署、部分签署、全部签署完成、已归档,中间还可能有作废和拒签分支。一开始我们用字符串字段加if-else判断状态流转,很快就变得不可维护,后来重构成显式的状态机模式,每种流转规则集中定义,非法流转直接拦截。
public enum ContractState
{
Draft, // 草稿
PendingSign, // 待签署
PartialSigned,// 部分签署
Completed, // 全部完成
Rejected, // 已拒签
Archived // 已归档
}
public class ContractStateMachine
{
private static readonly Dictionary<ContractState, ContractState[]> Transitions =
new()
{
[ContractState.Draft] = new[] { ContractState.PendingSign, ContractState.Archived },
[ContractState.PendingSign] = new[] { ContractState.PartialSigned, ContractState.Rejected },
[ContractState.PartialSigned] = new[] { ContractState.Completed, ContractState.Rejected },
[ContractState.Completed] = new[] { ContractState.Archived },
[ContractState.Rejected] = new[] { ContractState.Archived },
[ContractState.Archived] = Array.Empty<ContractState>()
};
public void Transition(Contract contract, ContractState target)
{
if (!Transitions[contract.State].Contains(target))
throw new InvalidOperationException(
$"不允许从 {contract.State} 流转到 {target}");
contract.State = target;
}
}并发是这套系统最容易出问题的地方。典型场景是两个签署人同时打开同一份合同并点击签署,如果不加控制,后提交的签署可能基于过期版本的文档,导致前一手的签名被意外覆盖。我们的解决方案是在签署事务中对合同记录加乐观锁:合同表带版本号字段,签署提交时先比对版本号,不一致就拒绝并要求签署人刷新文档重签。同时对签署操作按合同ID做分布式锁,保证同一份合同的签署动作在数据库层面串行化。
文件存储方面,原始模板、中间版本、最终定稿要分开保存,并且每次文档变更都生成新的存储对象而不是覆盖旧文件。这样做既满足了存证要求,也方便在出现问题时回溯任意历史版本。我们采用对象存储加数据库记录文件哈希的方式,定时任务会校验存储文件与记录哈希的一致性,一旦发现不一致立即告警。
四、身份认证、防篡改校验与上线后的经验教训
签署是否有效,很大程度上取决于签署人身份是否真实。我们集成了三种认证方式:手机号三要素校验用于低风险场景,人脸识别用于个人签署,企业签署则要求对公打款或电子营业执照授权。认证通过后签发的签署令牌与具体的合同和签署位绑定,防止令牌被挪用到其他合同上。这一点很重要,早期版本我们的令牌只绑定用户,测试时发现可以用A合同的令牌签B合同,立刻做了修正。
防篡改方面,除了PDF自身的数字签名,系统还提供独立的验签接口,任何一方随时可以上传合同文件,系统重新计算哈希并校验证书链与时间戳,返回签署完整性的详细报告。这份报告本身也会做数字签名存档,形成完整的证据闭环。
总结几条上线后总结的教训。证书私钥必须放在加密机或密钥管理服务中,绝不能以PFX文件形式散落在应用服务器上;签章接口要做幂等设计,网络重试导致的重复签署请求在真实环境里出现频率远超预期;PDF兼容性测试要覆盖WPS、福昕、Adobe Reader等多款阅读器,不同阅读器对签名的呈现差异很大;最后,日志与证据数据的保留周期建议至少五年,与合同纠纷诉讼时效对齐。把这些细节处理到位,一套自研的电子合同系统完全可以达到商用标准。