ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#软件Licence应用实例:基于RSA签名的授权系统实现

C#软件Licence应用实例:基于RSA签名的授权系统实现 简介针对需要在桌面软件中集成注册授权机制的.NET开发者这份由fengershishe整理的C#软件Licence应用实例提供了完整的授权管理实现参考。代码基于.NET 6.0围绕客户端授权与使用期限限制两个核心场景演示了通过读取电脑硬件信息生成本机标识、借助非对称加密对授权数据进行签名与校验、并以文件读写方式完成许可证持久化的整体思路适合有一定C#基础、正在设计离线License方案的工程技术人员。资源包共172个文件大小仅1.04MB包含24个C#源码文件、28个DLL库、配套的JSON配置、EXE可执行程序以及sln/csproj工程文件和密钥文件结构清晰便于在Visual Studio中直接打开查看多个子项目之间的配合关系。其中LicenceTool与LicenceTest等工程分别对应授权生成工具与验证测试入口能够帮助读者快速对照学习授权码生成、客户端校验和到期控制等关键环节。目前已有626人学习下载适合作为中高级C#开发者的动手参考资料。1. 从一次“软件被白嫖”说起C#软件Licence到底在解决什么做C#开发的同行应该都遇到过这个场景花几个月写完一套上位机或者行业管理软件发给客户试用结果客户在另一台电脑上拷了个副本就直接用起来了连试用期限制都没有。更头疼的是合同里写着按授权点数收费对方却在一台工控机上开了三个实例。这类问题说到底是软件本身没有Licence机制。C#软件Licence应用实例指的就是在.NET程序里做一套授权验证体系让软件只在被允许的机器上、在授权时间内、按授权功能运行。这套机制不是简单的“比对一下机器码”它要覆盖授权码生成、硬件指纹采集、签名防伪、离线与在线校验、防破解与容错等多个环节。适合谁看自己写工具软件出售的独立开发者、给工厂做上位机配套的工程师以及需要在企业内部系统里做模块级授权的团队。本文会从选型讲起逐步落到可复现的C#代码最后给出几个实际项目里容易踩的坑和处理手法。2. 先决定Licence的形态和校验级别再写第一行代码2.1 离线激活、在线激活还是混合模式写授权代码之前最先要定的是联网策略。很多C#桌面软件跑在车间、医院、实验室内网客户现场根本没有外网。这种情况下纯在线激活等于给自己找售后麻烦。常见做法是做成离线激活软件在本机生成机器指纹用户把指纹文件发给你或直接截图你用一个生成器产出授权文件用户导入后完成激活。离线方案的优点是部署简单、不依赖服务器缺点是无法实时吊销授权文件一旦泄露就可能被复用。在线激活则是软件启动时请求你的授权服务器比对设备信息后返回令牌适合SaaS化产品或需要按订阅收费的软件。混合模式比较实用首次激活必须联网之后进入离线宽限期宽限期内不再联网校验过期后再要求联网续期。从实际项目统计看绝大多数C#桌面软件、上位机程序最终选的是“离线激活为主在线校验为辅”。不要一上来就设计复杂的License Server先用离线方案把授权链路跑通后续再升级不迟。2.2 绑定到机器硬件指纹的采集维度与稳定性权衡授权要绑定机器就得采集能标识一台电脑的特征。可供选择的维度包括CPU序列号、主板序列号、硬盘序列号、MAC地址、BIOS UUID等。这里存在一个矛盾采集维度越少用户重装系统后越容易继续使用体验好但也越容易伪造采集维度越多越难被绕过可用户换一块硬盘就导致授权失效。我一般会折中取主板序列号加BIOS UUID作为主指纹硬盘序列号只作为辅助判据。原因有两个主板和BIOS序列号在重装系统后不会变用户重装Windows不影响授权这两个值虽然理论上可改但实际修改门槛远高于改MAC地址。不建议只用MAC因为虚拟网卡、USB网卡的MAC变化极其频繁而且可以随手修改这在C#上位机场景里很容易造成客户误报“软件坏了”。2.2.1 采集指纹的最小C#代码与WMI注意事项采集硬件信息最直接的方式是使用System.Management命名空间下的ManagementObjectSearcher。下面这段代码采集主板序列号、BIOS UUID和CPU标识using System; using System.Management; public static class MachineFingerprint { public static string GetCpuId() { using var searcher new ManagementObjectSearcher(SELECT ProcessorId FROM Win32_Processor); foreach (ManagementObject obj in searcher.Get()) { return obj[ProcessorId]?.ToString()?.Trim() ?? UNKNOWN; } return UNKNOWN; } public static string GetMotherboardSerial() { using var searcher new ManagementObjectSearcher(SELECT SerialNumber FROM Win32_BaseBoard); foreach (ManagementObject obj in searcher.Get()) { return obj[SerialNumber]?.ToString()?.Trim() ?? UNKNOWN; } return UNKNOWN; } public static string GetBiosUuid() { using var searcher new ManagementObjectSearcher(SELECT UUID FROM Win32_ComputerSystemProduct); foreach (ManagementObject obj in searcher.Get()) { return obj[UUID]?.ToString()?.Trim() ?? UNKNOWN; } return UNKNOWN; } public static string GenerateMachineCode() { string raw ${GetCpuId()}|{GetMotherboardSerial()}|{GetBiosUuid()}; return Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData( System.Text.Encoding.UTF8.GetBytes(raw))); } }代码逻辑很直接分别查三个WMI类取到对应的属性值拼接后用SHA256哈希再转Base64得到一个定长的机器码字符串。注意WMI查询是同步阻塞的首次调用可能耗时几百毫秒到几秒建议在程序启动时放到后台线程执行不要卡在窗口构造函数里。这里有两个关键的坑。第一部分品牌机尤其是组装机的主板序列号可能返回“Default string”或“To be filled by O.E.M.”这时这个字段就没有区分度必须依赖CPU和UUID。第二WMI在某些精简版Windows系统上可能查询超时建议给ManagementObjectSearcher设置超时时间并且在捕获到异常时降级为用Environment.MachineName拼接系统驱动器卷序列号保证程序不至于因取指纹崩溃。2.3 授权信息用什么承载JSON签名文件是当前最佳解确定指纹方案后接下来要决定授权文件格式。早期常见做法是把授权写入注册表但注册表容易被备份还原而且权限管理麻烦。另一个常见做法是生成一个包含到期日期的字符串用对称加密如DES/AES加密后交付给用户。对称加密的问题在于解密密钥藏在程序里用ILSpy或dnSpy一翻就能提取改了系统时间就能无限续期。更稳妥的做法是使用非对称签名让授权信息以明文JSON或自定义格式存储但对内容做RSA签名。程序内只保存公钥即使攻击者拿到授权文件也无法伪造新的授权因为私钥在你手里。文件格式上将Base64编码的授权数据与签名拼接成单行字符串既便于通过邮件、即时通讯发送又能避免路径转义问题。我习惯在文件里额外存放机器码哈希校验时先比对机器码不一致就直接报错不进入签名验证流程。3. 用RSA签名把完整Licence流程跑通3.1 授权文件的生成端用私钥签发License文件授权生成器通常是一个单独的命令行工具只在开发者电脑上运行不随软件分发。下面给出核心的签发代码using System; using System.IO; using System.Security.Cryptography; using System.Text; using System.Text.Json; public class LicenceData { public string MachineCode { get; set; } // 客户机器指纹的Base64 public string ExpireDate { get; set; } // 到期日期 yyyy-MM-dd public string Feature { get; set; } // 功能模块标识例如 SCADA / REPORT public DateTime IssueDate { get; set; } // 签发日期 } public static class LicenceIssuer { public static string CreateLicence(LicenceData data, string privateKeyXml) { string json JsonSerializer.Serialize(data); byte[] payloadBytes Encoding.UTF8.GetBytes(json); using var rsa RSA.Create(); rsa.FromXmlString(privateKeyXml); byte[] signature rsa.SignData(payloadBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); string payloadB64 Convert.ToBase64String(payloadBytes); string sigB64 Convert.ToBase64String(signature); // 格式Base64(JSON).Base64(签名) return ${payloadB64}.{sigB64}; } public static void SaveToFile(string licenceContent, string path) { File.WriteAllText(path, licenceContent, Encoding.ASCII); } }上面代码中SignData方法默认对字节数组做哈希后签名选择SHA256而不是MD5是因为MD5碰撞攻击成本太低。Pkcs1填充是.NET下最通用的填充方案兼容性好在跨语言校验时比如用Python脚本辅助验签也容易对应实现。签发流程很简单客户运行你的软件界面上显示一串机器码发给你你把机器码填进生成器设置到期日期和授权功能模块运行生成器得到一段由“点”分隔的字符串把这段字符串发给客户客户在软件里粘贴或者导入为.lic文件完成激活。3.2 校验端客户端只留公钥验证签名与有效期客户端校验逻辑要覆盖三个层面格式是否正确、签名是否有效、授权是否到期。下面的方法实现了完整校验public class LicenceValidator { private readonly string _publicKeyXml; public LicenceValidator(string publicKeyXml) { _publicKeyXml publicKeyXml; } public bool Validate(string licenceContent, string currentMachineCode, out LicenceData data) { data null; if (string.IsNullOrWhiteSpace(licenceContent)) return false; string[] parts licenceContent.Split(.); if (parts.Length ! 2) return false; try { byte[] payloadBytes Convert.FromBase64String(parts[0]); byte[] signature Convert.FromBase64String(parts[1]); using var rsa RSA.Create(); rsa.FromXmlString(_publicKeyXml); bool signatureValid rsa.VerifyData(payloadBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); if (!signatureValid) return false; string json Encoding.UTF8.GetString(payloadBytes); data JsonSerializer.DeserializeLicenceData(json); // 第一步校验机器码是否与当前机器一致大小写忽略 if (!string.Equals(data.MachineCode, currentMachineCode, StringComparison.OrdinalIgnoreCase)) return false; // 第二步校验到期日期 if (DateTime.TryParseExact(data.ExpireDate, yyyy-MM-dd, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime expireDate)) { if (DateTime.Now.Date expireDate.Date) return false; } return true; } catch (FormatException) { return false; // Base64字符不合法 } catch (CryptographicException) { return false; // 签名数据损坏 } } }这段代码体现了两个易被忽略的细节。其一验证机器码时用了OrdinalIgnoreCase避免用户手工录入时因大小写问题误判其二解析过期时间失败时没有直接return false而是跳到return true这里有个设计取舍如果授权文件里ExpireDate字段缺失说明生成器版本不统一为兼容旧版授权放行比拒绝更稳妥。但如果你的业务严格要求所有授权必须有过期时间此处应改为返回false。公钥从哪里来在开发期用RSA生成2048位密钥对后把公钥XML硬编码进程序集或者作为嵌入资源读取。公钥本身不保密泄露无妨关键是私钥不能进入客户端代码。一个常见错误是把私钥也编译进去了这等于把授权系统的后门直接交给用户。3.3 密钥管理私钥的保存与轮换机制私钥通常以XML字符串或PEM文件形式存放在开发机或内部服务器上。建议在生成器与主程序之间再做一层隔离生成器不直接读写密钥文件而是启动时从环境变量读取私钥内容这样即使生成器源码泄露攻击者也无法直接复制密钥。密钥轮换方面如果怀疑私钥泄露需要重新生成密钥对并通过升级客户端程序来替换公钥。但升级客户端本身就是一个大动作所以更实际的做法是把密钥保存在离线的USB加密狗或内部密钥管理系统中降低泄露概率。4. 把Licence嵌入真实项目对接WPF上位机与常见踩坑4.1 启动流程的接入方式登录窗口前加授权关卡在C#桌面应用里接入授权我一般放在程序入口处。对于WPF应用不要在App.xaml.cs的OnStartup里直接弹窗因为此时主窗口还没创建异常的UI上下文可能导致激活界面显示异常。建议在启动画面Splash Screen显示期间完成校验校验失败则弹出激活窗口成功则直接进入主界面。下面是一个最小实现框架protected override void OnStartup(StartupEventArgs e) { // 先显示启动画面 SplashScreen splash new SplashScreen(/Assets/splash.png); splash.Show(autoClose: false); string publicKey LoadPublicKeyFromResource(); // 从嵌入资源读取公钥 var validator new LicenceValidator(publicKey); string machineCode MachineFingerprint.GenerateMachineCode(); string licence LoadLicenceFromLocalFile(); if (string.IsNullOrEmpty(licence) || !validator.Validate(licence, machineCode, out _)) { splash.Close(); var activateWindow new ActivateWindow(machineCode); bool? result activateWindow.ShowDialog(); if (result ! true) { Shutdown(); return; } } splash.Close(); base.OnStartup(e); }这段逻辑保证未激活用户根本无法进入主程序而且激活窗口能拿到machineCode并用只读文本框展示方便用户复制。LoadLicenceFromLocalFile方法可以读安装目录下的license.lic文件也可以查注册表或用户目录。我建议优先读安装目录因为卸载软件时授权文件会一并删除避免用户在重装软件后产生“明明激活过为什么还要重新激活”的疑惑。4.2 时间篡改防护不能只信任DateTime.Now离线激活最容易被攻击的点就是系统时间。用户把时间改回授权到期日之前就能持续使用过期授权。常见的防护手段有两种其一把最近一次成功校验的时间写入本地文件每次校验时如果发现当前时间早于文件记录时间判定存在时间倒拨其二在授权文件里加入“签发日期”和“最后使用日期”每次运行时更新最后使用日期如果当前时间比最后使用日期提前超过一天判定异常。推荐做法是组合使用。代码片段string lastRunFile Path.Combine(AppDomain.CurrentDomain.BaseDirectory, lastrun.dat); DateTime lastRunTime File.Exists(lastRunFile) ? DateTime.Parse(File.ReadAllText(lastRunFile)) : DateTime.MinValue; if (DateTime.Now.Date lastRunTime.Date) { // 时间被回拨可要求重新激活或冻结功能 throw new LicenceTamperedException(系统时间异常请校准时间后重试); } File.WriteAllText(lastRunFile, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss));需要注意的是这个文件不能被放在Program Files目录下。安装在不同Windows用户账户下时普通用户对Program Files没有写权限会导致校验崩溃。放到%AppData%或者软件安装目录下的子文件夹里并设置为隐藏文件是更稳定的选择。另外这个方案只能在时间被回拨时报警无法防止用户把时间调到很久之后再调回来所以如果业务敏感就需要配合网络时间授时或在线校验。4.3 性能与并发别让指纹采集拖慢软件启动ManagementObjectSearcher的查询在部分老机器上可能耗时达两到三秒。一个优化方法是把机器码缓存到本地首次采集后写入配置文件后续启动直接从缓存读取。但这又引入了新问题用户拷贝整个软件目录到新机器缓存文件也一起被拷贝机器码校验仍然会失败因为缓存的机器码与新机器不匹配最终还是要走重新激活流程。因此缓存的收益有限更推荐在Splash Screen阶段异步执行。WPF中可以用async/await配合Task.Run但要注意ManagementObjectSearcher不是线程安全的控件放到Task.Run里没有任何问题因为它不接触UI线程。如果采集查询超时给ManagementObjectSearcher加上超时时间var options new ConnectionOptions { Timeout TimeSpan.FromSeconds(5) }; // 注意ManagementObjectSearcher构造函数的ConnectionOptions并非在所有重载中可用 // 更通用的做法在异步任务里包一层超时则放弃本次查询实际操作中我一般用Task.WhenAny配合延时任务来模拟超时控制捕获超时异常后回退到轻量级指纹机器名加系统驱动器序列号保证程序尽快进入主流程把授权校验放到更宽的边界上。4.4 软件著作权与授权标识的协同很多C#软件开发者会同步申请软件著作权这时候Licence文件中还可以写入版本号和授权单位名称在“关于”窗口中展示。一方面方便审计授权范围另一方面在客户现场与对方IT人员沟通时也能明确说明授权绑定的是哪台设备减少扯皮。这是一个成本极低但能显著提升专业度的细节。5. 上线后的三种特殊场景离线宽限期、重装与多实例5.1 离线宽限期应对无网环境的激活方案客户现场没有外网是C#上位机软件的常态但不能因此放弃在线校验。一个可行设计是首次激活时软件生成本机指纹并显示在界面上要求用户将指纹文件拷贝到一台能上网的电脑上传到你的激活服务端服务端返回授权码。这种“人工中转”的离线激活体验虽然原始但胜在稳定可控且不需要维护一套完整的在线激活协议。如果产品形态允许联网可以设计为“首次激活后每30天联网校验一次”。这种方案下授权文件里增加一个字段“LastOnlineCheckDate”每次在线校验成功后更新。离线期间启动只校验机器码和过期时间不做强制联网。宽限期的长度建议设为7到30天太短则客户出差期间软件直接不可用太长则失去吊销能力。5.2 重装操作系统后的授权恢复重装系统是License类软件最频繁的真实用户行为。如果指纹只绑定CPU和主板重装系统后机器码不变用户直接导入原授权文件即可恢复。这就是之前2.2节建议“不要绑定系统卷序列号”的原因因为重装系统可能重做磁盘分区结构卷序列号会变化。另一种应对方式是设计“授权迁移”机制允许用户在原机器上通过软件内的“导出授权回执”生成一个包含原机器码和时间戳的回执文件提交到你的服务端后换发新授权。回执文件必须包含原授权文件的签名否则任何人都能随便申请新授权。5.3 多实例限制控制单台机器同时运行多个进程有些授权是按实例数卖的需要限制同一台机器上同时运行的软件进程数量。实现上有两种途径命名互斥体Mutex和命名事件。Mutex方案最为简洁using System.Threading; bool isNewInstance; using (var mutex new Mutex(true, Global\MyApp_SingleInstance, out isNewInstance)) { if (!isNewInstance) { MessageBox.Show(本软件已在其他程序中运行请勿重复启动); return; } // 进入主程序 }注意这里使用了Global命名空间前缀这样同一个物理机器上的所有用户会话包括远程桌面会话都会共享同一个Mutex。如果不需要跨会话限制可以去掉Global前缀。还有一个细节Mutex的名称不要用产品名的明文否则其他程序可以伪造同名Mutex导致你的软件无法启动用“公司名GUID”的拼接形式更稳妥。5.4 验证授权系统是否可靠的三个实操方法写完授权模块后不要直接交付先用以下方法自测。第一把系统时间调到授权到期日之后一天启动软件确认激活窗口出现且不会崩溃第二把授权文件内容删掉几个字符确认程序提示“授权文件损坏”而不是直接抛异常第三在装有不同操作系统版本的虚拟机里分别导入同一授权文件确认机器码不受虚拟化影响或至少能给出明确的提示。硬件指纹的验证尤其要做一次在一台物理机上采集机器码然后在VMware里采集观察差异——如果两个环境返回的机器码一致说明你的指纹采集方案在实际部署中面临被模拟的风险。本文还有配套的精品资源点击获取
返回列表