
做软件开发最怕什么不是需求改来改去也不是上线出Bug而是自己加班熬夜写出来的VB.NET程序被客户当成共享软件到处拷贝。一套业务系统卖出去结果对方公司五十台电脑全装上了你还一点办法都没有。这个场景我太熟了早期接外包的时候就被这样坑过几次。后来我把授权机制梳理成一套完整的一机一码方案彻底解决了软件被随意复制的问题也帮不少同行搭建过类似系统。这篇文章就把我在VB.NET项目里落地一机一码加密授权的完整思路、核心代码和经验教训全部摊开来讲从机器码采集到注册码生成校验再到防破解对抗一次讲透。一机一码说白了就是每台电脑根据硬件信息生成一个唯一标识机器码软件运行时用这个标识向开发者申请对应的注册码注册码和硬件绑定换台机器就失效。这样既不影响正版用户体验又能把软件的使用范围牢牢锁在授权的那台设备上是中小型软件和定制化项目最实用的一种授权保护方案。1. 授权方案的整体设计思路先想清楚你要防谁、怎么防1.1 一机一码到底解决什么问题在做任何技术方案之前先得搞清楚业务诉求。一机一码不是用来防专业破解者的因为任何一个成熟的商业软件都做不到绝对防破解。它的核心目标是解决两类问题第一类防止一个授权码装遍全公司。这是最常见的情况客户买了一套软件然后想方设法让所有电脑都能用。没有硬件绑定的授权码复制粘贴就能激活拦都拦不住。第二类防止修改系统时间无限试用。开发过试用版软件的都知道单纯用日期判断有效期客户把系统时间改到未来试用期就永久续上了。一机一码配合注册码里的有效期信息可以在客户端离线完成时间校验不需要依赖服务器。这套方案的本质逻辑是用硬件特征做身份识别用签名校验做合法性验证。它不是加密软件本身而是加密授权这个行为。软件代码被反编译的风险靠混淆和加固解决这是另一个层面的问题后面我会专门讲。1.2 离线授权还是在线授权怎么选一机一码有两种落地形态我建议根据客户场景来选离线授权软件在本地生成机器码用户把机器码发给开发者开发者用私有的授权工具生成注册码用户再把注册码填回软件。整个过程不需要联网适合内部管理系统、工业控制软件这类客户网络环境不可控的项目。在线授权软件启动时把机器码和授权状态发到服务器服务器校验后返回结果。适合有互联网连接的SaaS化产品还能做并发数控制但缺点是客户断网就无法使用维护成本也更高。我接触的项目里七八成都是离线授权。原因很简单很多客户的电脑在隔离内网里或者办公室网络很差在线验证会让客户觉得软件很麻烦。离线授权虽然不能做实时吊销但配合有效期机制已经能满足大部分授权管理需求了。下面我讲的方案以离线授权为主。1.3 授权流程的基本链路整个一机一码的链路分为三个环节客户端采集硬件信息 → 生成机器码 → 显示给用户开发者用私钥对机器码签名 → 生成注册码客户端用公钥验证注册码 → 通过后存储激活信息。这里有一个非常关键的架构决策注册码必须由机器码通过签名算法生成而不是简单的机器码密钥拼接。因为如果注册码是用对称加密或简单拼接生成的破解者只要拿到一个样本就能分析出规律批量伪造注册码。使用非对称签名例如RSA私钥只在开发者手里客户端只有公钥公钥只能验证签名、不能生成签名这样就能从根本上防止注册码伪造。2. 机器码采集一机一码的地基必须稳2.1 哪些硬件特征适合做机器码机器码的可靠性决定了整个授权系统的上限。如果机器码不稳定客户换个U盘机器码就变了或者每台电脑读取出来的硬件信息都是一样的那后面全白搭。我从实战角度说一下常用硬件特征的优缺点CPU序列号ProcessorId同一型号的CPU序列号一般不同读取稳定是很好的特征来源。但虚拟机里可能会重复或者为空。主板序列号BaseBoard SerialNumber品牌机一般有唯一序列号兼容机自己组装的电脑很多是空的读取出来是一串乱码或者Default string。硬盘序列号DiskDrive SerialNumber唯一性高是最常用的特征。注意不能拿盘符C:、D:因为盘符会变应该拿物理磁盘的序列号。MAC地址唯一性也不错但有虚拟网卡干扰而且用户可以手动修改注册表伪装MAC地址所以只能做辅助特征。实际操作中我一般取CPU、主板、硬盘三个特征的组合MAC地址作为备用。如果某个特征读取失败算法里要做好降级处理不能让整个机器码生成失败。2.2 VB.NET采集硬件信息的核心代码VB.NET里读取硬件信息最常用的是System.Management命名空间下的WMI接口。项目里要添加对System.Management.dll的引用然后通过ManagementObjectSearcher查询。这是我在项目中实测可用的采集代码Imports System.Management Imports System.Security.Cryptography Imports System.Text Public Class HardwareInfo summary 获取CPU序列号 /summary Public Shared Function GetCpuSerial() As String Try Dim searcher As New ManagementObjectSearcher(SELECT ProcessorId FROM Win32_Processor) For Each obj As ManagementObject In searcher.Get() If obj(ProcessorId) IsNot Nothing Then Return obj(ProcessorId).ToString().Trim() End If Next Catch ex As Exception WMI查询异常时返回空由上层做降级处理 End Try Return End Function summary 获取主板序列号 /summary Public Shared Function GetBoardSerial() As String Try Dim searcher As New ManagementObjectSearcher(SELECT SerialNumber FROM Win32_BaseBoard) For Each obj As ManagementObject In searcher.Get() If obj(SerialNumber) IsNot Nothing Then Return obj(SerialNumber).ToString().Trim() End If Next Catch ex As Exception End Try Return End Function summary 获取第一块物理硬盘的序列号 /summary Public Shared Function GetDiskSerial() As String Try Dim searcher As New ManagementObjectSearcher(SELECT SerialNumber FROM Win32_DiskDrive WHERE Index0) For Each obj As ManagementObject In searcher.Get() If obj(SerialNumber) IsNot Nothing Then Return obj(SerialNumber).ToString().Trim() End If Next Catch ex As Exception End Try Return End Function End Class注意WMI查询在Windows 7、10、11上都能用但某些精简版系统可能没有WMI服务或者权限受限。所以每一条读取都要写在Try...Catch里宁可返回空字符串也不能让软件直接崩溃。2.3 机器码生成算法的降级与容错拿到硬件信息之后不能直接拼接就完事要考虑几种现实情况第一种用户用的是虚拟机或云主机硬件信息不完整甚至每次启动都会变。这时候要结合系统安装时间、系统盘卷序列号等辅助信息兜底。第二种用户手动改过MAC地址或者接了USB网卡导致MAC地址特征不稳定。第三种某些硬件信息读取出来是固定的占位符比如To be filled by O.E.M.这种没有区分度的值必须过滤掉否则每台电脑生成一样的机器码。我整理了一个带权重和容错的处理方案Public Class MachineCodeGenerator Public Shared Function Generate() As String Dim cpu As String HardwareInfo.GetCpuSerial() Dim board As String HardwareInfo.GetBoardSerial() Dim disk As String HardwareInfo.GetDiskSerial() 过滤掉无意义的占位值 If String.IsNullOrEmpty(cpu) OrElse cpu.Contains(O.E.M) Then cpu If String.IsNullOrEmpty(board) OrElse board.Contains(O.E.M) OrElse board.IndexOf(Default, StringComparison.OrdinalIgnoreCase) 0 Then board If String.IsNullOrEmpty(disk) Then disk 至少要有两个有效特征否则生成的机器码不可靠 Dim raw As String cpu | board | disk If String.IsNullOrEmpty(cpu) AndAlso String.IsNullOrEmpty(board) Then 降级方案读取系统盘卷序列号补充 raw | GetVolumeSerial(C) End If 用MD5单向散列加固定盐防碰撞 Using md5 As MD5 MD5.Create() Dim input As Byte() Encoding.UTF8.GetBytes(raw 9X!qL2#vT$kP7wF) Dim hash As Byte() md5.ComputeHash(input) Dim sb As New StringBuilder() For Each b As Byte In hash sb.Append(b.ToString(X2)) Next 取前16位作为最终机器码方便客户抄写和客服核对 Return sb.ToString().Substring(0, 16) End Using End Function Private Shared Function GetVolumeSerial(drive As String) As String Try Dim searcher As New ManagementObjectSearcher($SELECT VolumeSerialNumber FROM Win32_LogicalDisk WHERE DeviceID{drive}:) For Each obj As ManagementObject In searcher.Get() If obj(VolumeSerialNumber) IsNot Nothing Then Return obj(VolumeSerialNumber).ToString().Trim() End If Next Catch ex As Exception End Try Return End Function End Class机器码生成后软件界面上把它显示给客户客户复制发给开发者。这里的小细节显示格式可以做一下分组比如ABCD-EFGH-IJKL-MNOP看起来规整客服电话沟通的时候也容易报号。3. 注册码的生成与校验核心逻辑决定安全性3.1 为什么不能拿机器码直接当注册码有人可能觉得既然机器码是唯一的那机器码本身不就是最好的授权凭证吗这个思路有问题。第一机器码是公开给客户的客户之间可以互换机器码相当于互相授权。第二机器码没有任何有效期、功能限制等信息你没法用它控制试用期。第三机器码的生成算法在客户端代码里反编译之后就能看懂破解者可以自己写程序生成任意机器码。所以真正的注册码必须满足三个要求绑定机器码、包含授权信息有效期、功能模块等、只能由开发者生成。3.2 用RSA私钥签名生成注册码我推荐用RSA非对称签名来实现这三条要求。原理不复杂开发者持有一对密钥私钥用来给注册信息生成签名公钥放进客户端程序里用来验签。由于私钥不公开别人拿到客户端也反推不出私钥自然伪造不了签名。注册码的内容结构设计如下机器码(16位) | 过期日期(8位) | 功能编号(2位) | 签名(前若干字节)签名是对前面三个字段拼接后做的RSA-SHA256签名然后把整个内容Base64编码再按5位一组格式化输出方便输入。开发者的授权工具用私钥生成注册码代码示例Imports System.Security.Cryptography Imports System.Text Public Class LicenseGenerator Private Shared ReadOnly _privateKey As String 在这里放你的RSA私钥XML summary 生成注册码 /summary param namemachineCode客户提供的机器码/param param nameexpireDate过期日期永久授权传29991231/param param namefeatureCode功能模块编号例如01表示基础版02表示专业版/param Public Shared Function Generate(machineCode As String, expireDate As String, featureCode As String) As String Dim licenseData As String machineCode | expireDate | featureCode Dim dataBytes As Byte() Encoding.UTF8.GetBytes(licenseData) Using rsa As New RSACryptoServiceProvider() rsa.FromXmlString(_privateKey) 用SHA256计算摘要再做签名 Dim signedBytes As Byte() rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSAPaddingMode.Pkcs1) 原始数据和签名一起放进注册码里 Dim signedData As Byte() dataBytes.Concat(signedBytes).ToArray() Dim base64Str As String Convert.ToBase64String(signedData) 按5位一组格式化方便输入 Return FormatLicenseCode(base64Str) End Using End Function Private Shared Function FormatLicenseCode(input As String) As String Dim clean As String input.Replace(, ).Replace(/, ).Replace(, ) 直接去掉Base64中的特殊字符保证用户手动输入时不出错 Dim sb As New StringBuilder() For i As Integer 0 To clean.Length - 1 If i 0 AndAlso i Mod 5 0 Then sb.Append(-) End If sb.Append(clean(i)) Next Return sb.ToString() End Function End Class提示RSA私钥XML字符串属于高度机密信息绝不能写进客户端程序也不能在网络上传输。它只应该存在于开发者的授权工具里比如一个单独的WinForms小工具由客服或管理员使用。3.3 客户端校验注册码的完整实现客户端拿到用户输入的注册码要做四件事解析、验签、比对机器码、检查有效期。每一步都不能少否则就会被绕过。实现代码如下Imports System.Security.Cryptography Imports System.Text Public Class LicenseValidator Private Shared ReadOnly _publicKey As String 在这里放你的RSA公钥XML summary 校验注册码是否合法 /summary Public Shared Function Validate(inputCode As String, currentMachineCode As String) As LicenseInfo Dim result As New LicenseInfo() 第一步清理输入的格式去掉分隔符 Dim cleanCode As String inputCode.Replace(-, ).Trim() Try 第二步Base64解码得到原始数据和签名 Dim signedData As Byte() Convert.FromBase64String(cleanCode) 数据区是前三个字段签名区是最后256字节RSA2048 Dim dataLen As Integer signedData.Length - 256 If dataLen 0 Then result.IsValid False result.Message 注册码格式错误 Return result End If Dim dataBytes As Byte() New Byte(dataLen - 1) {} Array.Copy(signedData, 0, dataBytes, 0, dataLen) Dim signature As Byte() New Byte(255) {} Array.Copy(signedData, dataLen, signature, 0, 256) 第三步用公钥验签确保注册码是开发者签发的 Using rsa As New RSACryptoServiceProvider() rsa.FromXmlString(_publicKey) Dim isSigned As Boolean rsa.VerifyData(dataBytes, signature, HashAlgorithmName.SHA256, RSAPaddingMode.Pkcs1) If Not isSigned Then result.IsValid False result.Message 注册码校验失败不是官方签发的授权 Return result End If End Using 第四步解析数据内容 Dim dataStr As String Encoding.UTF8.GetString(dataBytes) Dim fields As String() dataStr.Split(|c) If fields.Length 3 Then result.IsValid False result.Message 注册码数据异常 Return result End If result.MachineCode fields(0) result.ExpireDate fields(1) result.FeatureCode fields(2) 第五步比对机器码 If String.Compare(result.MachineCode, currentMachineCode, True) 0 Then result.IsValid False result.Message 授权码与当前设备不匹配 Return result End If 第六步检查有效期 Dim expire As DateTime If DateTime.TryParseExact(result.ExpireDate, yyyyMMdd, Nothing, Globalization.DateTimeStyles.None, expire) Then If expire DateTime.Today Then result.IsValid False result.Message 授权已过期请联系供应商续费 Return result End If Else result.IsValid False result.Message 授权日期格式错误 Return result End If result.IsValid True result.Message 激活成功 Return result Catch ex As Exception result.IsValid False result.Message 注册码解析失败 ex.Message Return result End Try End Function End Class Public Class LicenseInfo Public Property IsValid As Boolean Public Property Message As String Public Property MachineCode As String Public Property ExpireDate As String Public Property FeatureCode As String End Class这里的密钥对生成用PowerShell或openssl都可以注意生成2048位以上RSA密钥。生成XML格式的密钥对后私钥放授权工具公钥硬编码到客户端。3.4 激活信息的存储注册表还是文件激活成功后要把授权状态保存下来不然每次启动都要重新输入注册码。我一般用注册表加文件双写的方式互相备份。注册表位置HKEY_CURRENT_USER\SOFTWARE\你的公司名\你的产品名写入内容机器码、注册码密文、激活时间、到期时间。注册表按用户隔离不需要管理员权限适合WinForms程序。用一个辅助类来管理存储和读取Imports Microsoft.Win32 Public Class LicenseStorage Private Shared ReadOnly RegPath As String SOFTWARE\YourCompany\YourProduct Public Shared Sub Save(LicenseInfo As LicenseInfo) Try Using key As RegistryKey Registry.CurrentUser.CreateSubKey(RegPath) If key IsNot Nothing Then key.SetValue(MachineCode, LicenseInfo.MachineCode) key.SetValue(ExpireDate, LicenseInfo.ExpireDate) key.SetValue(FeatureCode, LicenseInfo.FeatureCode) key.SetValue(ActivateTime, DateTime.Now.ToString(yyyyMMddHHmmss)) End If End Using Catch ex As Exception 注册表写入失败时静默处理不影响软件主流程 End Try End Sub Public Shared Function Load() As LicenseInfo Dim info As New LicenseInfo() Try Using key As RegistryKey Registry.CurrentUser.OpenSubKey(RegPath) If key IsNot Nothing Then info.MachineCode key.GetValue(MachineCode, ).ToString() info.ExpireDate key.GetValue(ExpireDate, ).ToString() info.FeatureCode key.GetValue(FeatureCode, ).ToString() End If End Using Catch ex As Exception End Try Return info End Function Public Shared Sub Clear() Try Registry.CurrentUser.DeleteSubKeyTree(RegPath, False) Catch ex As Exception End Try End Sub End Class注意激活信息存下来之后软件启动时不能光读注册表里的状态必须重新读取当前机器码和存储的机器码比对。如果客户把整台电脑的注册表复制到另一台电脑上机器码对不上授权就自动失效了。这一句比对逻辑是防止注册表搬运最关键的一环。4. 实操过程从开发机到授权工具4.1 在现有VB.NET项目中接入授权模块接入授权模块不要污染主业务代码。我习惯的做法是程序启动时在Main入口或者登录界面的Load事件里加入授权检查逻辑。流程如下程序启动 → 读取本地激活信息 → 检查是否存在→ 若存在重新生成当前机器码比对激活记录里的机器码是否一致再比对有效期 → 全部通过则进入主界面→ 若不存在弹出激活窗口显示机器码等待用户输入注册码 → 校验通过后写入存储 → 进入主界面示例代码精简版Private Sub Form_Load(sender As Object, e As EventArgs) Handles MyBase.Load 先隐藏主界面避免闪屏 Me.ShowInTaskbar False Me.Opacity 0 Dim currentMachineCode As String MachineCodeGenerator.Generate() Dim saved As LicenseInfo LicenseStorage.Load() If String.IsNullOrEmpty(saved.MachineCode) Then 第一次运行弹激活窗口 Dim activateForm As New FormActivate(currentMachineCode) If activateForm.ShowDialog() DialogResult.OK Then Me.Show() Me.Opacity 1 Else Application.Exit() Return End If Else 校验已存授权 If saved.MachineCode currentMachineCode AndAlso IsNotExpired(saved.ExpireDate) Then Me.Show() Me.Opacity 1 Else Dim activateForm As New FormActivate(currentMachineCode) 说明需要重新激活 If activateForm.ShowDialog() DialogResult.OK Then Me.Show() Me.Opacity 1 Else Application.Exit() Return End If End If End If End Sub激活窗口的核心逻辑就是在激活按钮事件里调用LicenseValidator.Validate校验通过就保存并返回OK。4.2 给客服写一个授权码生成小工具客户端做完了开发者这边还需要一个内部使用的授权码生成器。这个工具不能跟客户软件放在一起它是你用来给客户签发注册码的。我做得比较完整的是一个WinForms程序包含三个输入框客户名备注、机器码、授权类型再加一个生成注册码按钮。这个工具有几个功能根据授权类型自动生成有效期和功能编号。比如商业版一年期功能编号02有效期是当前日期加365天终身版功能编号99有效期29991231。生成完成后自动复制注册码到剪贴板客服直接粘贴发给客户。每次生成的操作记录写入日志文件方便追踪某个客户什么时候发的什么码。有个小细节客服手上可能有同一个客户的多个电脑的机器码建议工具里维护一个客户列表一个客户对应多条机器码授权记录。省得下次续费的时候找不到历史记录。4.3 防止注册码被转用的几个做法一机一码方案天然防止了一个注册码多台机器用但还有几个细节需要补漏防转用第一层注册码里包含机器码只要判断机器码不一致就直接拒绝激活。防转用第二层激活时把机器码和注册码的关联记录写到注册表同时把激活次数存入一个隐藏文件。我见过有一些破解者直接复制整个操作系统镜像到新电脑上这时候机器码匹配但系统时间可能还是旧的。所以要加一个系统时间合理性校验如果注册表里记录的激活时间比当前时间还晚说明系统时间被改过或者镜像被搬运过。防转用第三层在软件的关键功能入口比如保存、导出、打印也加上授权有效性检查。有些破解者会跳过启动时的激活检查直接改内存让主窗体加载但业务功能入口的校验他们往往忽略这里能多挡一下。5. 常见问题与破解对抗5.1 客户重装系统或更换硬件怎么办这是售后最常遇到的问题处理不好容易跟客户起纠纷。重装系统注册表里的激活信息丢失但硬件没变机器码不变。所以客户只需要把之前拿到的注册码再输入一次就能重新激活不麻烦。更换主板或硬盘机器码变了旧注册码必然失效。这是硬件绑定的正常代价但你要在合作协议里写清楚硬件变更需要联系开发方重新授权。实际操作中我一般会在机器码生成界面加一行说明文字提醒客户更换关键硬件会导致授权失效。为了防止误伤比如客户只是加了一根内存条并没有换主板机器码采集时可以设置容错规则CPU主板硬盘三个特征里只要任意两个匹配就算同一台机器。我在一些项目里用这个策略用户体验好很多。容错的注册码格式就要相应调整数据区记录的是机器码A版本还是机器码B版本校验时用匹配算法而不是简单字符串相等。这个实现稍微复杂一点但对于售后体验提升很有帮助。5.2 VB.NET程序容易被反编译怎么加固VB.NET和C#一样编译出来是IL中间语言用反编译工具能直接还原成接近源码的代码。就算注册码逻辑写得再严破解者把客户端反编译后把校验函数改成永远返回True再重新编译你的授权就形同虚设。所以授权校验必须配合代码混淆使用。我的实践经验是使用商业混淆器比如那一类.NET混淆工具开启流程混淆、字符串加密、反调试等选项。免费版就够用了关键是要把授权校验相关的类和方法名全部混淆掉。重点混淆类一定要勾选不要全部混淆。有些混淆器全选之后程序集签名会损坏而且某些反射调用的地方会被混淆器改坏需要逐项测试。程序启动时加一个防篡改检查对主程序集的哈希值做签名验证。如果破解者修改了你的exe/dll程序启动时发现哈希不匹配直接拒绝运行。加壳也是一种选择但有些加壳工具会导致程序启动变慢、杀毒软件误报我建议先测兼容性再决定。5.3 常见授权绕过方式与对策根据我看到的实际破解案例这类软件常见的破解方式就这几种第一种暴力修改激活状态。破解者反编译代码找到校验结果的位置把IsValid强制改为True。对策是不要把IsValid当做一个布尔变量而是把授权结果散落到程序的多个功能点每个功能点都独立校验签名增加修改成本。第二种内存补丁。运行软件后用调试工具修改内存里存储授权状态的值。对策是启动后定时重新校验发现授权状态异常就自动退出。第三种直接抄走别人激活好的系统镜像。对策就是前面说的系统时间合理性校验和多特征容错机制。这里必须说清楚没有绝对防破解的方案你的目标是让破解成本高于软件本身的价值。对大部分中小型业务软件来说代码混淆签名校验多点验证已经足以劝退绝大多数试图白嫖的人。5.4 故障排查速查表我在踩过不少坑之后整理了一张常见问题排查表给客服做远程支持的时候可以照着排查现象可能原因排查步骤机器码为空或全是0虚拟机或WMI服务未启动检查客户是否使用虚拟机运行wmimgmt.msc确认WMI服务状态注册码输入后提示校验失败注册码复制粘贴时丢失字符确认输入的注册码是否完整去掉所有空格和换行同一台电脑重装系统后机器码变了采集到的硬件信息被系统信息覆盖用旧版的机器码生成工具对比确认是否采集字段有变化两台同型号电脑生成的机器码一样硬件特征读取到了默认占位值检查主板序列号和硬盘序列号是否为空加强其他特征激活成功后第二天又提示未激活注册表写入失效检查杀毒软件是否拦截了注册表写入建议改用用户配置文件存储客户改了系统时间后授权失效有效性判断使用了本地时间授权校验时加入时间戳偏移检测允许几分钟误差这里重点说一下重装系统后机器码变了这个高频问题。很多情况下不是代码采集逻辑问题而是新版系统或者系统更新让硬件信息接口返回的格式变了。比如老系统返回的硬盘序列号是ABCD1234新系统返回ABCD1234_0000看起来就不一样了。解决办法是采集后做规范化处理截断多余后缀统一大小写。实操心得授权方案要留有余地这套一机一码方案我陆续用了将近五年服务过的客户项目有几十个。和我一开始预想的不同技术上最难的其实不是加密算法也不是机器码采集而是售后和客户沟通。有的客户会觉得我就是换个电脑为什么不能自己激活有的客户会觉得授权码还要找你们要太麻烦。所以我现在做授权方案一定会搭配一个自助式的处理办法客户软件里自带重发授权申请功能点击后自动把机器码和软件版本打包成文本客户复制发过来就行。客服那边一键生成授权码发回去全程不需要远程控制对方电脑。技术选型上我个人不建议一开始就上超复杂的在线授权系统。对绝大多数项目来说一机一码离线授权已经够用而且实现和维护成本都低得多。等客户规模大了、软件到了需要做SaaS化运营的阶段再平滑升级到在线授权也不迟。另一个心得是签名密钥的管理。曾经有一家同行把RSA私钥直接放在开发的电脑里后来开发离职了把私钥也带走了。结果那家公司发布的软件全部停摆所有客户都无法正常授权只能紧急发新版重新绑定机器码。我现在的习惯是私钥放在加密U盘里授权工具从U盘读取私钥电脑上不留任何痕迹。公钥虽然可以被反编译拿到但公钥本身没有危害。最后再分享一个小技巧机器码和注册码的显示格式可以略微迷惑一下操作者。比如机器码不直接用大写的十六进制字符串而是混入小写字母和数字注册码里故意加入一些看起来像校验位的字符。这些对真实用户没有影响但会增加破解者分析数据格式的难度算是一笔性价比很高的投入。授权保护是一场持续博弈没有一劳永逸的方案。但把机器码采集的稳定性、签名校验的严谨性、代码混淆的完整性都做到位再配合一个响应及时的售后流程你的软件就比市面上大部分同类产品安全得多也专业得多。