行业资讯
📅 2026/7/12 5:23:59
C#软件授权实战:基于AES加密与注册表的90天试用与机器绑定方案
1. 项目概述从“裸奔”到“武装”的软件授权之路做C#桌面应用开发的朋友尤其是独立开发者或小团队肯定都遇到过这个头疼的问题辛辛苦苦开发出来的软件怎么防止用户无限期白嫖怎么控制试用期怎么防止一个授权码被到处拷贝使用很多初学者的做法可能就是生成一个简单的机器码然后硬编码一个注册码进行比对或者把过期时间明文写在配置文件里。这种“裸奔”式的授权稍微懂点技术的用户用十六进制编辑器或者反编译工具比如ILSpy、dnSpy看一眼防线就瞬间崩溃了。我见过太多项目核心功能做得非常棒却因为授权机制太脆弱导致商业收入大打折扣。今天要聊的就是一个我在多个商业项目中验证过的、实战级的C#软件授权方案。它的核心目标很明确实现一个90天的固定试用期并且确保授权信息与特定电脑绑定防止拷贝扩散。我们不会用那些昂贵或复杂的第三方授权库就靠C#标准库和Windows系统自身的特性核心武器是AES对称加密和Windows注册表。这个方案的优势在于平衡了安全性与开发复杂度。AES加密保证了授权信息本身难以被篡改和伪造而注册表的特定位置比如为软件创建的子项则提供了存储和验证的场所结合机器硬件信息我们称之为“机器指纹”就能实现“一机一码”。下面我们就来彻底拆解这个方案从设计思路到每一行代码从加密原理到防破解技巧让你不仅能实现更能理解背后的“为什么”。2. 方案核心设计与思路拆解2.1 为什么是AES注册表而不是别的在开始写代码之前我们必须想清楚技术选型。市面上方案很多比如单纯的MD5、RSA非对称加密、或者把信息藏在文件、数据库里。为什么我们最终拍板用AES注册表首先看加密算法。MD5是哈希算法不可逆常用于校验完整性但它不能用于“加密后还能解密出原文”的场景比如我们需要存储一个包含过期日期的字符串。RSA非对称加密非常安全但速度较慢且管理公私钥对对于单机版软件来说增加了复杂度。AES高级加密标准属于对称加密加解密速度快安全性经过全球验证被美国政府选为加密标准非常适合用来保护我们软件本地的授权信息。它的特点是用同一个密钥进行加密和解密我们的任务就是把这个密钥藏好。再看存储位置。为什么是注册表而不是AppData下的一个文件或程序目录下的license.dat隐蔽性注册表对于普通用户来说比文件系统更陌生不会轻易去查看和修改。一个精心命名的注册表路径如HKEY_CURRENT_USER\Software\YourCompany\YourApp\License不容易被偶然发现。权限与持久化存储在HKEY_CURRENT_USER下软件有天然的读写权限且会随着用户配置漫游如果设置了漫游。即使软件被卸载如果没专门清理注册表信息可能还在这有时可以作为反破解的一个小策略当然我们安装程序应该提供清理。防文件拷贝如果授权信息只放在安装目录的文件里用户直接复制整个文件夹到另一台电脑授权就可能被带走了。而注册表信息是留在原电脑的复制文件夹无效。这是我们实现“防拷贝”的关键一环。当然注册表也有缺点比如在跨平台如macOS、Linux上不可用。但本方案主要针对经典的Windows桌面C#应用WinForms、WPF所以注册表是合适的选择。2.2 整体授权流程与生命周期我们的授权系统将围绕两个核心文件展开一个是负责授权信息生成和验证的LicenseManager类另一个是可能存在的用于生成正式授权码的离线工具LicenseGenerator。这里主要讲集成在主软件中的LicenseManager。其工作流程是一个清晰的闭环首次运行试用开始软件启动时LicenseManager检查指定注册表路径下是否存在有效的授权信息。不存在或无效则被认定为首次运行。系统会生成一个基于当前电脑的“机器指纹”然后以当前日期为起始日期计算90天后的日期作为试用到期日将这些信息用AES加密后写入注册表。此时软件进入试用模式。存在且有效LicenseManager从注册表读取加密字符串用AES解密得到明文的授权信息包含机器指纹和到期日。然后它重新计算当前电脑的机器指纹与解密得到的指纹比对。如果一致且当前日期未超过到期日则授权通过软件全功能可用。验证失败如果机器指纹不一致说明授权被拷贝到其他电脑或当前日期已过期则授权失败。软件应转入功能受限的试用状态或直接拒绝运行。输入正式授权码提供一个人口如输入框让用户输入由LicenseGenerator生成的正式授权码。LicenseManager验证该授权码的有效性同样涉及解密和校验若有效则用新的授权信息可能是永久许可更新注册表。这个流程确保了即使是试用期也通过加密和机器绑定增加了篡改难度。3. 核心模块实现与代码解析接下来我们进入实战环节一步步构建核心的LicenseManager类。我会先给出关键代码片段然后解释其背后的原理和注意事项。3.1 生成可靠的“机器指纹”“机器指纹”的目的是获取一个能相对唯一标识当前计算机的字符串。绝对唯一很难但我们可以组合多个相对稳定的硬件或系统信息。注意不要使用容易被修改的信息如计算机名、IP地址。using System; using System.Management; // 需要引用System.Management.dll using System.Security.Cryptography; using System.Text; public static class MachineFingerprint { public static string Generate() { var fingerprintBuilder new StringBuilder(); // 1. CPU ID (相对稳定) try { using (var searcher new ManagementObjectSearcher(SELECT ProcessorId FROM Win32_Processor)) { foreach (var mo in searcher.Get()) { fingerprintBuilder.Append(mo[ProcessorId]?.ToString()); break; // 通常取第一个CPU } } } catch { /* 忽略错误继续使用其他信息 */ } // 2. 主板序列号 (非常稳定) try { using (var searcher new ManagementObjectSearcher(SELECT SerialNumber FROM Win32_BaseBoard)) { foreach (var mo in searcher.Get()) { fingerprintBuilder.Append(|); fingerprintBuilder.Append(mo[SerialNumber]?.ToString()); break; } } } catch { } // 3. 系统驱动器卷序列号 (C盘) try { var drive new System.IO.DriveInfo(System.Environment.GetFolderPath(Environment.SpecialFolder.System).Substring(0, 3)); fingerprintBuilder.Append(|); fingerprintBuilder.Append(drive.VolumeSerialNumber.ToString(X)); } catch { } // 如果以上都失败使用一个备用方案但绑定效果会变差 if (string.IsNullOrEmpty(fingerprintBuilder.ToString()) || fingerprintBuilder.Length 10) { // 例如组合机器名和用户名但这不是好选择仅作演示 fingerprintBuilder.Append(Environment.MachineName); fingerprintBuilder.Append(|); fingerprintBuilder.Append(Environment.UserName); } // 将组合的字符串进行MD5哈希得到固定长度、不可逆的指纹 using (var md5 MD5.Create()) { var inputBytes Encoding.UTF8.GetBytes(fingerprintBuilder.ToString()); var hashBytes md5.ComputeHash(inputBytes); return BitConverter.ToString(hashBytes).Replace(-, ).ToLowerInvariant(); } } }注意使用System.Management需要确保项目有对应引用并且在某些精简版系统或特定权限下可能无法获取信息。因此代码中进行了try-catch。获取多个信息并组合可以增加指纹的稳定性。最后进行MD5哈希是为了将不定长的信息转化为定长的、不可逆的字符串既保护了原始信息又方便存储比对。3.2 AES加密与解密工具类这是我们的核心加密模块。我们需要一个固定的密钥Key和初始化向量IV来加解密。如何保管这个密钥是整个系统安全的关键。using System; using System.IO; using System.Security.Cryptography; using System.Text; public static class AesHelper { // !!! 关键安全警告以下密钥和IV在生产环境中绝不能硬编码在代码里 !!! // 理想做法将密钥的“种子”信息分散隐藏或通过运行时计算动态生成。 private static readonly byte[] Key Encoding.UTF8.GetBytes(Your32ByteLongSecretKey!!1234567890); // 必须32字节 private static readonly byte[] IV Encoding.UTF8.GetBytes(Your16ByteInitVec!!); // 必须16字节 public static string Encrypt(string plainText) { if (string.IsNullOrEmpty(plainText)) return string.Empty; using (var aes Aes.Create()) { aes.Key Key; aes.IV IV; aes.Mode CipherMode.CBC; // 使用CBC模式 aes.Padding PaddingMode.PKCS7; using (var encryptor aes.CreateEncryptor()) using (var ms new MemoryStream()) { using (var cs new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) using (var sw new StreamWriter(cs)) { sw.Write(plainText); } return Convert.ToBase64String(ms.ToArray()); } } } public static string Decrypt(string cipherText) { if (string.IsNullOrEmpty(cipherText)) return string.Empty; try { var cipherBytes Convert.FromBase64String(cipherText); using (var aes Aes.Create()) { aes.Key Key; aes.IV IV; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; using (var decryptor aes.CreateDecryptor()) using (var ms new MemoryStream(cipherBytes)) using (var cs new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) using (var sr new StreamReader(cs)) { return sr.ReadToEnd(); } } } catch (FormatException) { // 输入不是合法的Base64字符串 return string.Empty; } catch (CryptographicException) { // 解密失败密钥错误或数据被篡改 return string.Empty; } } }实操心得这里使用了CBC模式和PKCS7填充这是比较常用的安全组合。密钥和IV硬编码是最大的安全漏洞。一个进阶技巧是你可以将密钥字符串拆分成多个部分放在不同的资源文件、混淆后的常量、甚至通过一个简单的算法在运行时拼接出来。也可以利用程序集自身的某些元数据如版本号、编译时间的哈希值作为密钥生成的种子。目标是增加静态反编译直接获取密钥的难度。3.3 注册表读写封装我们需要一个安全、便捷的方式来操作注册表。将路径集中管理是个好习惯。using Microsoft.Win32; // 需要引用对应命名空间 public static class RegistryHelper { // 定义你的软件注册表根路径 private const string RegRoot HKEY_CURRENT_USER\Software\YourCompany\YourApp; private const string LicenseKeyName LicenseData; public static void WriteLicenseData(string encryptedData) { try { Registry.SetValue(RegRoot, LicenseKeyName, encryptedData, RegistryValueKind.String); } catch (UnauthorizedAccessException ex) { // 处理权限错误可能是注册表项被保护 // 可以尝试以管理员权限运行或写入到用户可写的位置如HKCU本身通常可写 throw new InvalidOperationException($无法写入注册表: {ex.Message}); } } public static string ReadLicenseData() { try { return Registry.GetValue(RegRoot, LicenseKeyName, null) as string; } catch { return null; } } public static bool DeleteLicenseData() { try { using (var key Registry.CurrentUser.OpenSubKey(Software\YourCompany, true)) { if (key ! null) { key.DeleteSubKeyTree(YourApp); return true; } } } catch { } return false; } }注意事项HKEY_CURRENT_USER(HKCU) 通常不需要管理员权限即可读写这比HKEY_LOCAL_MACHINE(HKLM) 更友好。路径中的YourCompany和YourApp请替换为你自己的公司和产品名。删除操作DeleteLicenseData在提供“清理授权”功能或卸载程序时非常有用。3.4 授权信息实体与试用期计算我们需要一个类来封装授权信息并处理试用期的逻辑。public class LicenseInfo { public string MachineFingerprint { get; set; } public DateTime ExpiryDate { get; set; } public LicenseType Type { get; set; } // 枚举Trial, Permanent public enum LicenseType { Trial, Permanent } // 将对象序列化为一个字符串用于加密存储 public override string ToString() { // 使用简单的分隔符如 |注意避免与数据内容冲突 return ${MachineFingerprint}|{ExpiryDate:yyyy-MM-dd HH:mm:ss}|{Type}; } // 从字符串反序列化 public static LicenseInfo FromString(string data) { if (string.IsNullOrEmpty(data)) return null; var parts data.Split(|); if (parts.Length ! 3) return null; return new LicenseInfo { MachineFingerprint parts[0], ExpiryDate DateTime.ParseExact(parts[1], yyyy-MM-dd HH:mm:ss, null), Type (LicenseType)Enum.Parse(typeof(LicenseType), parts[2]) }; } // 检查是否过期 public bool IsExpired() { return DateTime.Now ExpiryDate; } // 检查机器指纹是否匹配 public bool IsMachineMatched(string currentFingerprint) { return string.Equals(MachineFingerprint, currentFingerprint, StringComparison.OrdinalIgnoreCase); } }3.5 授权管理器主逻辑现在我们把所有模块组合起来形成完整的LicenseManager。public static class LicenseManager { private const int TrialDays 90; public static LicenseInfo CurrentLicense { get; private set; } public static bool IsLicensed CurrentLicense ! null !CurrentLicense.IsExpired(); public static void Initialize() { string encryptedData RegistryHelper.ReadLicenseData(); string currentFingerprint MachineFingerprint.Generate(); if (string.IsNullOrEmpty(encryptedData)) { // 首次运行创建试用授权 CreateTrialLicense(currentFingerprint); } else { // 尝试解密已有授权 string decryptedData AesHelper.Decrypt(encryptedData); CurrentLicense LicenseInfo.FromString(decryptedData); if (CurrentLicense null) { // 解密失败数据可能被破坏按试用处理或报错 // 这里选择重新创建试用但会覆盖旧的可能被篡改的数据 CreateTrialLicense(currentFingerprint); return; } // 验证机器指纹和有效期 if (!CurrentLicense.IsMachineMatched(currentFingerprint)) { // 机器不匹配授权无效强制转为试用或终止 // 你可以选择抛出异常或设置一个无效的License CurrentLicense null; // 或者创建一个新的试用但提示机器变更 // CreateTrialLicense(currentFingerprint); // MessageBox.Show(检测到运行环境变更试用期已重置。); return; } if (CurrentLicense.IsExpired()) { // 授权已过期 // CurrentLicense 保持不变但 IsLicensed 会为 false return; } // 所有验证通过授权有效 } } private static void CreateTrialLicense(string fingerprint) { var trialExpiry DateTime.Now.AddDays(TrialDays); CurrentLicense new LicenseInfo { MachineFingerprint fingerprint, ExpiryDate trialExpiry, Type LicenseInfo.LicenseType.Trial }; string licenseString CurrentLicense.ToString(); string encrypted AesHelper.Encrypt(licenseString); RegistryHelper.WriteLicenseData(encrypted); } public static bool ActivateLicense(string activationCode) { // 这里假设 activationCode 是另一个AES加密的字符串由LicenseGenerator生成 // 其解密后可能包含新的ExpiryDate如2099-12-31和LicenseType.Permanent // 同样需要验证其中的机器指纹或允许激活的指纹列表 // 实现逻辑与Initialize中解密验证类似 // 如果激活成功用新的LicenseInfo更新CurrentLicense并写入注册表 // 返回 true/false // 此处省略具体实现取决于你的激活码生成规则 throw new NotImplementedException(激活逻辑需根据生成器规则实现); } public static string GetTrialStatus() { if (CurrentLicense null) return 未检测到授权信息; if (CurrentLicense.Type LicenseInfo.LicenseType.Permanent) return 已激活永久授权; if (CurrentLicense.IsExpired()) return $试用已过期于 {CurrentLicense.ExpiryDate:yyyy-MM-dd}; var daysLeft (CurrentLicense.ExpiryDate - DateTime.Now).Days; return $试用版剩余 {daysLeft} 天; } }在主程序启动时例如Main方法或主窗体的构造函数最开始调用LicenseManager.Initialize()。之后整个程序可以通过LicenseManager.IsLicensed来判断是否具有完整功能权限并通过LicenseManager.GetTrialStatus()获取状态信息显示给用户。4. 防破解增强策略与进阶技巧基础的AES注册表方案已经能挡住大部分普通用户但对于有经验的破解者还需要更多防御层。下面分享几个我实战中总结的进阶技巧。4.1 对抗静态分析密钥隐藏与代码混淆硬编码的AES密钥是最大的弱点。破解者用反编译工具打开你的exe或dll直接搜索字符串常量很可能找到Your32ByteLongSecretKey!!1234567890。技巧一字符串分割与拼接。不要直接写完整的密钥字符串。把它拆成几段放在不同的地方运行时再拼起来。// 在代码的不同位置定义 private static string part1 Your32ByteLong; private static string part2 GetKeyPartFromResource(); // 从资源文件读取 private static string part3 SomeClass.StaticMethodReturnsSecret(); // 通过方法调用返回 // ... 在AesHelper的静态构造函数或首次使用时拼接 private static readonly byte[] Key Encoding.UTF8.GetBytes(part1 part2 part3 ...);技巧二动态计算密钥。利用程序本身的某些“不变”属性通过一个复杂的但你自己知道的算法计算出一个密钥。例如结合程序集版本号、编译时间戳的哈希值再与一个固定盐值Salt混合。private static byte[] GenerateKey() { var assembly Assembly.GetExecutingAssembly(); var version assembly.GetName().Version.ToString(); var buildDate new DateTime(2000, 1, 1).AddDays(version.Build).AddSeconds(version.Revision * 2); string seed ${version}-{buildDate:yyyyMMdd}-MySaltString; using (var sha256 SHA256.Create()) { // 用SHA256生成32字节的密钥 return sha256.ComputeHash(Encoding.UTF8.GetBytes(seed)); } }注意这种方式生成的密钥是固定的因为程序集信息固定但比明文字符串难找得多。破解者需要逆向你的密钥生成算法。技巧三使用专业的代码混淆工具。如Obfuscar、ConfuserEx等。它们会重命名你的类、方法、变量名为无意义的字符并可能添加控制流混淆使反编译后的代码难以阅读和理解大大增加破解者定位关键代码如AES解密调用的难度。这是商业软件非常推荐的一步。4.2 对抗动态调试与内存扫描即使代码被混淆破解者仍可能通过调试器如x64dbg在运行时拦截程序或者在内存中搜索解密后的明文授权信息。技巧一关键代码运行时解密SMC。将授权验证的核心函数代码本身进行加密存储只在运行时才解密到内存中执行执行后立即覆盖。这需要一定的汇编或Native代码知识在C#中实现较为复杂但可以通过注入本地代码或使用不安全的上下文来模拟。技巧二多阶段验证与心跳检查。不要只在启动时验证一次。在软件运行的关键功能点、定时器事件中随机地、不定期地重新验证授权信息。如果发现被篡改例如有人用调试器修改了内存中的验证结果标志可以触发静默的错误或 gracefully degrade功能降级而不是直接崩溃增加破解难度。技巧三检测调试器。使用System.Diagnostics.Debugger类的方法如Debugger.IsAttached或Debugger.IsLogging()如果发现调试器附着可以采取混淆行为、延迟执行或直接退出。不过有经验的破解者会绕过这些检测。4.3 注册表存储的“诡计”多位置存储与校验除了主注册表项可以将授权信息的某个校验和如CRC32或一部分数据写入注册表的另一个看似不相关的路径甚至写入一个隐藏的配置文件。验证时同时检查两处是否一致不一致则说明可能被手动修改了主注册表项。数据混淆在将授权信息字符串加密前可以先进行简单的混淆比如字节反转、与一个固定字节数组进行XOR运算等。这样即使有人直接查看注册表中的Base64字符串解密后得到的也是一串乱码需要再进行一次反混淆才能得到真正的LicenseInfo字符串。这增加了手工破解的步骤。利用注册表键值的不同类型除了REG_SZ字符串还可以将部分数据存储在REG_BINARY二进制或REG_DWORD双字中增加直接阅读和修改的难度。4.4 试用期计算的“陷阱”我们之前的试用期是从首次运行开始算90天。但用户可能会通过修改系统时间来“欺骗”软件。防御方法一记录首次运行时间。在创建试用授权时不仅记录到期日ExpiryDate同时将首次运行的日期InstallDate也加密存储。每次验证时检查当前时间是否早于InstallDate如果是说明用户可能回调了系统时间可以视为作弊使授权失效或采取其他措施。防御方法二使用相对时间戳。不存储具体的到期日期而是存储从安装日起经过的秒数或天数试用时长。同时在注册表或某个隐藏文件里加密存储一个“最后运行时间戳”。每次启动时读取当前时间与“最后运行时间戳”比较计算差值将这个差值累加到已使用的试用时间上再更新“最后运行时间戳”。这样即使用户修改系统时间只要他让电脑运行试用时间就会真实地累计。实现起来更复杂但更健壮。5. 常见问题排查与实战心得在实际部署和调试这套授权系统时你肯定会遇到各种问题。下面是我踩过的一些坑和解决方案。5.1 注册表访问被拒绝问题在WriteLicenseData时抛出UnauthorizedAccessException。原因虽然HKEY_CURRENT_USER通常可写但如果你的注册表路径包含不存在的父项且当前用户权限不足或者在某些严格管控的企业环境创建子项可能会失败。另一种可能是杀毒软件或系统策略阻止了写入。解决确保路径正确且你的软件有正常的用户权限运行。尝试将路径简化直接写在HKEY_CURRENT_USER\Software\下更深的层级。如果软件需要以管理员身份运行才能工作可以考虑请求管理员权限通过清单文件但这不是最佳用户体验。作为备选方案如果注册表写入失败可以回退到将加密信息写入用户AppData目录下的一个隐藏文件。但这会削弱“防拷贝”能力。5.2 机器指纹不稳定或为空问题MachineFingerprint.Generate()返回空字符串或在不同运行间发生变化。原因System.Management查询可能因WMI服务未启动、权限问题或虚拟机/特殊硬件环境而失败。卷序列号在重装系统或格式化后可能改变。解决增加更多的指纹源如硬盘型号、MAC地址注意虚拟机和无线网卡可能导致变化、BIOS序列号等。取多个源只要其中一个成功就能组合出指纹。对获取的原始信息进行清洗和规范化比如MAC地址去掉分隔符并转为统一大小写。实现一个“指纹容错”机制。如果本次生成的指纹与存储的指纹不完全匹配但大部分核心组件如CPU ID、主板序列号匹配可以提示用户环境有变化并允许手动确认或联系客服转移授权而不是直接判定为非法。重要在日志中记录生成的指纹原始组成部分方便在用户反馈“授权失效”时进行远程诊断。5.3 授权信息被用户手动清除问题用户通过regedit删除了你的注册表项软件又重新开始计算90天试用。解决这是桌面软件授权的永恒难题。没有绝对防御。多位置存储如上文所述在多个位置其他注册表路径、AppData文件、甚至某些系统目录存放校验信息。云验证终极方案对于重要软件最终极的解决方案是结合在线激活。首次试用或激活时将机器指纹发送到你的服务器服务器记录该指纹的激活状态。软件定期或每次启动时在后台与服务器通信验证。这样即使用户清除了本地所有信息服务器端记录依然存在。但这需要服务器后端支持并考虑离线使用的场景。法律与用户体验平衡对于大多数软件过于严苛的防护会伤害合法用户的体验。我们的目标应该是增加普通用户的破解成本而不是追求绝对无法破解。清晰的试用提示和合理的定价往往比复杂的防破解技术更能促进销售。5.4 时间校验的时区与用户修改问题问题用户出差到不同时区或者系统时区设置错误导致时间判断出错。解决在存储和比较时间时统一使用UTC时间(DateTime.UtcNow)。这样可以避免时区带来的影响。ExpiryDate在存储时也应以UTC格式存储。// 创建试用期时 ExpiryDate DateTime.UtcNow.AddDays(TrialDays); // 检查是否过期时 if (DateTime.UtcNow CurrentLicense.ExpiryDate) { ... }对于防御修改系统时间除了前面提到的记录首次安装时间还可以在软件中集成一个简单的NTP客户端在联网时悄悄获取一次网络时间与本地时间对比如果发现巨大偏差比如本地时间被改回几年前可以记录异常或提示用户。5.5 代码集成与调试建议隔离授权模块将LicenseManager、AesHelper、MachineFingerprint等类放在一个独立的类库项目DLL中。这样便于管理、混淆和更新授权逻辑。提供详细的日志在授权验证的每个关键步骤读取注册表、解密、指纹比对、过期检查都输出日志到文件或调试输出。在开发阶段这些日志是无价之宝。发布时可以通过条件编译 (#if DEBUG) 来关闭这些日志。设计一个“授权状态”窗口在软件的“关于”或“帮助”菜单里添加一个窗口显示从LicenseManager.GetTrialStatus()获取的详细信息以及原始的机器指纹可隐藏。这对于用户反馈问题和你的技术支持非常有用。做好向后兼容一旦软件发布注册表的数据结构LicenseInfo.ToString()的格式就尽量不要改了。如果必须修改要考虑旧版本数据如何迁移或兼容否则升级后用户可能会丢失授权状态。最后记住软件授权的本质是一种商业策略而非纯粹的技术问题。本文提供的AES注册表方案是一个在安全性、开发成本和用户体验之间取得的不错平衡点能够有效阻止绝大多数普通用户的随意分享和无限试用。但对于坚定的破解者没有100%安全的本地方案。因此在技术实现之外构建良好的产品价值、客户关系和合理的授权定价才是软件可持续发展的根本。希望这篇近万字的实战解析能帮你打造出自己软件的第一道可靠防线。