资讯详情

C#软件授权实战:基于RSA签名与机器码的离线License校验方案

📅 2026/9/16 11:10:33 | 华诺云谱 👁 阅读
C#软件授权实战:基于RSA签名与机器码的离线License校验方案
简介一套基于C#语言与.NET 6.0框架的软件授权License应用示例面向需要为客户端软件加入正版授权与使用期限控制的开发者。示例围绕授权管理完整链路展开演示了如何读取电脑硬件信息、通过非对称加密完成签名与验证、并实现授权文件的生成和读取可帮助开发者快速搭建可复用的授权验证模块。压缩包共172个文件以C#源码、DLL依赖库和EXE可执行程序为主另含JSON配置、工程文件及说明文档整体仅1.04MB结构紧凑便于查阅与移植。目前已有626人学习下载适合具备一定C#基础、希望借助成熟案例落地软件授权机制的开发人员参考。通过学习该工程还能深入理解非对称加密在离线授权场景中的实际应用以及授权文件从生成、分发到校验的完整生命周期。1. C# 软件 Licence 实例到底在解决什么问题很多 C# 开发者在交付软件时才碰 License客户要求按年付费、按模块收费或者限制只能在某几台机器上运行。常见做法是写一个机器码读取、比对注册表里的注册状态弹个框提示“授权已过期”但这套方案最大問題不是代码量而是它把授权状态放在了用户可控的位置。C# 程序集可以反编译注册表项可以被修改系统时间可以被回拨甚至换个盘符就能绕过路径判断。真正可交付的 C# Licence 系统核心不在加密而在“签名”用私钥签发授权内容用公钥在客户端验证授权文件只承载数据程序只做签名校验。这篇按 C# 上位机、行业软件和内部工具三个常见场景拆出一套离线、可复现、不依赖商业加密锁的 Licence 实现从协议设计到签发端和校验端代码最后给出验证手法和常见坑位。2. 先定协议RSA 签名、机器码和许可证文件格式2.1 为什么用 RSA 签名而不是把密钥写死在程序里做 License 第一个要决定的事是“授权信息如何防伪造”。把序列号做成对称加密字符串然后程序里放着解密密钥这种方案等于把保险柜密码写在保险柜门上。C# 程序用 dnSpy 之类工具可以直接看到字符串常量和 IL 代码对称密钥藏不住。正确方向是采用非对称签名签发端持有 RSA 私钥负责为授权信息签名客户端只嵌入 RSA 公钥验证签名即可。公钥无法反推私钥伪造 License 文件的成本从“读懂代码”变成“破解 RSA”这个量级差异就是整个方案的底气。RSA 密钥长度建议 4096 位虽然比 2048 位慢几十毫秒但签发和校验都不是高频操作用这点性能换更长余量值得。签名方案用 SHA256 与 PKCS1 v1.5 填充兼容性比 PSS 更好后续跨语言对接比如用 Python 或 Go 写签发工具也省事。注意客户端只放公钥千万不要图省事把私钥也嵌进安装包。私钥一旦泄露所有历史 License 全部失效。2.2 机器码取什么、怎么组合、格式怎么归一化机器码的作用是绑定授权到指定电脑。最怕的不是取不到硬件信息而是取到的信息在不同机器上重复或者同一台机器重装系统后机器码变化导致客户骂娘。我在实际项目里用过几类信息来源各有取舍机器码来源稳定性唯一性采集成本重装系统影响MachineGuid注册表高中低读取注册表单项即可不变同一系统镜像下相同ProcessorIdWMI高中高虚拟机下可能为空中需 System.Management不变主板序列号WMI高高但部分品牌机为空中不变DiskSerialNumberWMI中中改动分区表可能变化中可能变化常见做法是选两个来源组合后做 SHA256 哈希再转为大写十六进制字符串。比如 MachineGuid 加 ProcessorId前者保证重装系统后不漂移后者补足唯一性。组合后 32 个字符展示和复制都比一长串数字友好。特别注意字符串归一化全部转大写、去除中划线、去掉换行和首尾空格避免因为格式差异让同一台机器验出两个机器码。2.3 .lic 文件结构约定与大小限制License 文件本身是纯文本结构定成三段拼接中间用竖线分隔。第一段是 Base64 编码的 JSON 载荷包含授权对象第二段是 Base64 编码的 RSA 签名。这样做的好处是用记事本打开能看到一小段疑似乱码不会诱导客户手动改内容同时程序解析又很简单。ewo... (Base64 JSON) | qV3f... (Base64 RSA Signature)JSON 载荷字段这样设计后续扩展时不破坏旧文件格式{ App: PlcMonitor, Customer: 某自动化公司, MachineCode: 7F2A..., ExpireDate: 2026-12-31, Modules: [Motion, Vision], IssueDate: 2025-01-01 }RSA-4096 签名长度固定为 512 字节Base64 编码后约 684 字符加上 JSON 载荷一个 .lic 文件通常不到 2KB。请求体太小无所谓关键是解析时不要把整个文件读进内存后无脑 Split要校验竖线数量数量不是 2 就直接判失败避免畸形文件触发异常。3. 签发端用 C# 命令行工具生成 License 文件3.1 环境与密钥准备签发端工具建议做成独立命令行程序不集成在正式软件里。目标框架选 net6.0-windows 或 net8.0-windows方便用 WMI 和注册表 API。项目添加 System.Management 包.NET Framework 时代是直接引用读取硬件信息。密钥准备分两步第一步用 OpenSSL 生成 RSA 私钥和公钥或直接用 C# 代码生成 PEM。我习惯把私钥 PEM 放在独立目录签发工具通过环境变量或启动参数指定路径。生成并导出 PEM 的 C# 代码using System.Security.Cryptography; // 生成 4096 位 RSA 密钥对 using var rsa RSA.Create(4096); var privateKey rsa.ExportRSAPrivateKeyPem(); // .NET 5 内置 var publicKey rsa.ExportRSAPublicKeyPem(); File.WriteAllText(private_key.pem, privateKey); File.WriteAllText(public_key.pem, publicKey); Console.WriteLine(public key:\n publicKey);逻辑说明ExportRSAPrivateKeyPem输出 PKCS#1 格式的 PEMImportFromPem可原样导回。公钥文件要复制到客户端工程里作为嵌入资源或常量字符串。签发端机器不需要公钥客户端机器不需要私钥两个文件分开存放。3.2 读取机器码并生成签名的核心代码签发工具需要客户提供的机器码但实战里经常遇到客户不会复制机器码的场景所以签发端最好也内置读取机器码的逻辑远程指导时直接让客户运行一条命令输出。下面这段是签发端的核心入口using System.Security.Cryptography; using System.Text; using System.Text.Json; var privateKeyPem File.ReadAllText(args[0]); // 第一个参数私钥路径 var machineCode args[1]; // 第二个参数目标机器码 var expireDays int.Parse(args[2]); // 第三个参数授权天数 // 1. 构造授权载荷 var licenseInfo new LicenseInfo { App PlcMonitor, Customer args.Length 3 ? args[3] : Default, MachineCode machineCode.ToUpperInvariant(), ExpireDate DateTime.Now.AddDays(expireDays).ToString(yyyy-MM-dd), Modules args.Skip(4).ToArray() }; string payloadJson JsonSerializer.Serialize(licenseInfo); string payload Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson)); // 2. 对 Base64 载荷做签名 using var rsa RSA.Create(); rsa.ImportFromPem(privateKeyPem); byte[] payloadBytes Encoding.UTF8.GetBytes(payload); byte[] signature rsa.SignData(payloadBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // 3. 写出 .lic 文件 File.WriteAllText(license.lic, payload | Convert.ToBase64String(signature)); Console.WriteLine(License 已生成: license.lic); class LicenseInfo { public string App { get; set; } public string Customer { get; set; } public string MachineCode { get; set; } public string ExpireDate { get; set; } public string[] Modules { get; set; } }参数说明expireDays用相对天数而不是绝对日期签发时自动计算减少人工写错日期的概率MachineCode转大写再写进 JSON保证和客户端校验时归一化结果一致Modules是可选项按功能代号传入例如输入Motion Vision就让该 License 同时启用两个模块。签名时是对 Base64 后的字符串签名不是对原始 JSON 签名客户端验证时同样先 Base64 解码再核对两侧顺序必须一致。3.3 签发参数表有效期、功能位、备注签发工具的命令行参数需要一套约定避免同一个功能每次使用都要翻文档。我固定用这套参数次序简单场景只填前三个参数位置参数名必填示例说明args[0]私钥路径是private_key.pem建议运行时输入不推荐写死在程序里args[1]机器码是7F2A9C...客户机器输出的 32 位大写字符串args[2]授权天数是3650 表示永久授权内部存 9999-12-31args[3]客户名称否某自动化公司写入 JSON便于溯源args[4..]功能模块否Motion Vision不传则所有模块可用签发时多一个心眼把客户名称、签发日期、机器码记录到一个本地脚本里方便日后排查“哪个客户的 License 是哪一批发出去的”。很多 C# 软件做软件著作权登记时也要提供授权和版本说明这份签发记录就是现成的证明材料。4. 校验端在 C# 软件启动流程里做一次完整验证4.1 校验函数的输入输出设计客户端校验最忌讳把验证逻辑散落在窗体加载事件里然后到处引用一个bool _isLicensed标志。校验要收敛成一个独立静态类LicenseGuard输入是 .lic 文件路径和公钥 PEM输出是一个结果对象包含是否通过、错误码和载荷信息。错误码设计成我自己的这套排查问题时省时间错误码含义出现场景Success校验通过正常情况ErrorFileNotFound找不到 .lic 文件首次运行未激活ErrorFormat文件格式错误.lic 文件被文本编辑器破坏ErrorSignature签名不匹配文件被篡改或公钥不匹配ErrorMachine机器码不匹配License 拷到另一台机器ErrorExpired授权过期当前日期超过有效日期4.2 ValidateLicense 完整代码与参数说明下面这段是可直接搬到上位机工程里的校验实现目标框架 .NET 6注意ImportFromPem在 .NET 5 之前不可用老工程需改用Rsa.FromXmlString或引入 BouncyCastlepublic static class LicenseGuard { public static (LicenseStatus Status, LicenseInfo Info, string Message) Validate( string licPath, string publicKeyPem, string currentMachineCode, bool enableClockSkewCheck true) { if (!File.Exists(licPath)) return (LicenseStatus.ErrorFileNotFound, null, 缺少 License 文件); // 1. 拆分载荷与签名 string[] parts File.ReadAllText(licPath).Split(|); if (parts.Length ! 2 || string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1])) return (LicenseStatus.ErrorFormat, null, License 文件格式错误); string payloadBase64 parts[0]; string signatureBase64 parts[1]; // 2. 验签签名通过内容才可信 using var rsa RSA.Create(); rsa.ImportFromPem(publicKeyPem); byte[] payloadBytes Encoding.UTF8.GetBytes(payloadBase64); byte[] signatureBytes Convert.FromBase64String(signatureBase64); bool valid rsa.VerifyData( payloadBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); if (!valid) return (LicenseStatus.ErrorSignature, null, 签名验证失败); // 3. 反序列化并逐项检查 LicenseInfo info; try { string json Encoding.UTF8.GetString(Convert.FromBase64String(payloadBase64)); info JsonSerializer.DeserializeLicenseInfo(json); } catch { return (LicenseStatus.ErrorFormat, null, 授权内容解析失败); } // 机器码归一化后比较 if (!string.Equals(info.MachineCode.Trim().ToUpperInvariant(), currentMachineCode.Trim().ToUpperInvariant(), StringComparison.Ordinal)) return (LicenseStatus.ErrorMachine, null, 授权未绑定当前设备); // 有效期解析使用不变区域设置避免本机日期格式干扰 if (!DateTime.TryParseExact(info.ExpireDate, yyyy-MM-dd, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime expire)) return (LicenseStatus.ErrorFormat, null, 授权日期格式错误); if (expire DateTime.Now.Date) return (LicenseStatus.ErrorExpired, null, 授权已过期); return (LicenseStatus.Success, info, 验证通过); } } public enum LicenseStatus { Success, ErrorFileNotFound, ErrorFormat, ErrorSignature, ErrorMachine, ErrorExpired }逻辑说明验签在反序列化之前签名不过直接返回后面的所有步骤都不执行这是安全底线。机器码比较放在有效期之前因为客户最常犯的错误是把 A 机器的 License 放到 B 机器上先报机器码错误更友好。日期比较统一用DateTime.Now.Date忽略时分秒避免授权最后一天晚上 23:59 被误判。enableClockSkewCheck参数在基础版里先预留默认不开启用来做时间回拨防护后面第 5 章展开讲。4.3 集成到上位机 / 桌面应用的三个位置校验逻辑写好后集成位置和方式决定整个方案是否好用。我一般在三个层面同时做第一层是启动入口。WinForms 在Program.Main里启动Application.Run前校验校验失败直接弹提示并退出或者进入降级模式。WPF 放在App.InitializeComponent之后。第二层是核心业务模块的定时校验。上位机场景常见做法是程序运行期间每隔几十分钟重新读取一次 .lic 文件并校验防止客户打开软件后再手动替换授权文件。WPF 可以用DispatcherTimerWinForms 用System.Windows.Forms.Timer间隔不要太频繁30 到 60 分钟一次足够。第三层是 C# 特性打标。给核心程序集加一个自定义 Attribute例如[LicensedModule(Motion)]启动时扫描程序集特性对照 LicenseInfo.Modules 判断功能是否启用。这样新加一个功能模块时只要打特性不需要改动校验代码。后台定时校验的代码框架var timer new System.Threading.Timer(_ { var result LicenseGuard.Validate(licensePath, pubKey, machineCode); if (result.Status ! LicenseStatus.Success) { // 不能在这里直接弹窗UI 线程之外操作会抛异常 Application.Current.Dispatcher.Invoke(() { MessageBox.Show(授权已失效软件即将退出); Application.Current.Shutdown(); }); } }, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));注意在上位机里集成时Application.Current.Dispatcher只适用于 WPFWinForms 要改成Control.CheckForIllegalCrossThreadCalls true;或用BeginInvoke切回 UI 线程。C# 里跨线程操作 UI 控件卡顿是高频问题这里如果直接在新线程里MessageBox.Show会行为异常必须显式切线程。5. 验证手法与进阶给自己做一轮“破解测试”5.1 五分钟模拟篡改测试License 代码写完第一件事不是交给客户而是先自己尝试各种破坏手段确保每个分支都真正生效。我自己的验收流程如下打开 .lic 文件改一个字符保存后再启动软件预期是 ErrorSignature把同一份 .lic 放到另一台机器预期是 ErrorMachine把系统日期手动改到一年后预期是 ErrorExpired。前三条都能正确拦截才算达到基本可用。除了手改文件还要测试异常文件格式空文件、只有一行的文件、超过 10MB 的垃圾文件。常见坑是File.ReadAllText一次性读取超大文件导致内存暴涨所以在读取前先检查文件大小超过 1MB 直接返回格式错误var fileInfo new FileInfo(licPath); if (fileInfo.Length 1024 * 1024) return (LicenseStatus.ErrorFormat, null, License 文件异常);5.2 时钟回拨的消耗策略时间回拨是离线 License 最头疼的绕过手段客户把系统日期调回授权期内就能无限续期。基础校验加上一段“上次运行时间记录”就能防住大部分情况。做法是软件正常运行时定期把当前日期写入本地隐藏文件校验时读取该文件里的日期如果当前日期比记录日期提前超过一天判定为时间回拨。string lastRunPath Path.Combine(AppContext.BaseDirectory, .lastrun); DateTime lastDate DateTime.MinValue; if (File.Exists(lastRunPath) DateTime.TryParseExact( File.ReadAllText(lastRunPath).Trim(), yyyy-MM-dd, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)) { lastDate parsed; } if (DateTime.Now.Date lastDate.AddDays(-1)) return (LicenseStatus.ErrorExpired, null, 检测到系统时间异常); File.WriteAllText(lastRunPath, DateTime.Now.ToString(yyyy-MM-dd));注意这个记录文件不能只存一个日期否则删除文件就能绕过。我一般会把文件属性设为隐藏再叠加一个简单的 HMAC密钥从公钥派生。但要说清楚这种防护上限不高懂技术的用户仍能定位代码绕过它的价值在于拦住 90% 愿意改时间但不愿分析程序的普通使用者。对高价值软件最终防线是硬件加密锁或联网验证超出本文范围。5.3 常见失败现象对照表我把这几年客户反馈最多的现象整理成一张排查表开发时对照自查能省下大量来回沟通的时间现象可能原因排查方法刚签发的 License 报签名不匹配签发端使用的密钥与客户端公钥不是同一对比对签发端导出公钥与客户端常量是否一致同一台机器重装系统后机器码变化机器码来源包含 MachineGuid 时部分系统镜像会变换成 ProcessorId 主板序列号组合License 在笔记本上时而有效时而失效笔记本睡眠唤醒后 WMI 读取部分硬件信息为空机器码生成时加空值兜底某字段为空就跳过授权未到期但提示过期日期解析使用了本地区域设置客户电脑日期格式为 yyyy/MM/dd统一用 TryParseExact yyyy-MM-dd InvariantCulture程序启动后一段时间才弹授权错误定时校验线程未切回 UI 线程检查 Dispatcher.Invoke 或 BaeginInvoke最后的提醒是License 只是商业化的第一道门它决定“谁能在哪台机器上用到什么时候”决定不了软件是否被反编译修改。C# 程序发布前至少加一层混淆配合签名验证把能挡的路径都挡上剩下的事情交给服务质量和法律条款。高级一点的玩法是把 License 文件内容与软件主程序集哈希绑定校验时计算主程序集 SHA256与 License 里存的程序集指纹比对这样客户想替换主程序文件绕过校验也会失效。这个方法实现成本不高对防止自带破解补丁的“热心网友”有实际效果。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。