乐于分享
好东西不私藏

工控软件保卫战:WPF上位机授权方案解析

工控软件保卫战:WPF上位机授权方案解析

WPF这类本地部署的应用非常容易被复制传播,必要的授权机制可以很大程度上防止盗版和未授权的传播,从而保护开发者的合法权益。一个合格的WPF应用软件授权方案,应该具备:

  • 完整性 – 授权文件不能被伪造或修改
  • 唯一性 – 绑定硬件,保证一机一码
  • 时效性 – 支持有效期、按功能授权

这里我们介绍一种解决方案,架构设计如下:

1.机器码生成 : 收集 CPU ID、主板序列号、硬盘序列号,组合Hash生成唯一机器码。

2.非对称加密 :

    • 私钥 : 保存在开发者手中,用于生成授权文件。

    • 公钥 : WPF客户端中,用于验证授权文件。

3.时间篡改检测:在离线环境下,要具备时间篡改检测功能,检测用户是否修改了系统时间。

4.授权方式: 使用Json格式,包含机器码、过期时间、功能标志等,需要对Json进行数字签名。

核心模块代码

1.机器码生成

这里要先安装“ System.Management” NuGet包,获取硬件信息需要用到。

public static class DeviceFingerprint{    // 使用 Lazy 缓存机器码,避免频繁调用 WMI 导致程序卡顿        private static readonly Lazy<string> _machineCode = new Lazy<string>(GenerateMachineCode);    public static string Value => _machineCode.Value;    privatestaticstringGenerateMachineCode()    {        string cpu = GetWmiInfo("Win32_Processor""ProcessorId");        string hdd = GetWmiInfo("Win32_DiskDrive""SerialNumber");        string board = GetWmiInfo("Win32_BaseBoard""SerialNumber");        string rawFingerprint = $"CPU>>{cpu}||HDD>>{hdd}||BOARD>>{board}";        byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(rawFingerprint));        return BitConverter.ToString(hash).Replace("-""");    }    privatestaticstringGetWmiInfo(string table, string prop)    {        using (var searcher = new ManagementObjectSearcher($"SELECT {prop} FROM {table}"))        {            foreach (var obj in searcher.Get())            {                var val = obj[prop]?.ToString();                // 排除空值和"To be filled by O.E"垃圾数据                if (!string.IsNullOrWhiteSpace(val) && !val.Contains("O.E.M"))                    return val.Trim();            }        }        throw new Exception($"从{table}获取WMI属性 {prop}失败!");    }}

2.基于RSA的签名与验证

public static class SecurityGuard{    // 生成密钥对:privateKey 开发者保存(不能公开),publicKey 硬编码到 WPF 客户端(可公开)    publicstaticvoidGenerateKeys(outstring publicKey, outstring privateKey)    {        using (var rsa = RSA.Create(2048))        {            privateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey());            publicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo());        }    }    // 用私钥签名数据 - 开发者使用    publicstaticstringSignData(string data, string privateKey)    {        using (var rsa = RSA.Create())        {            rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);            byte[] dataBytes = Encoding.UTF8.GetBytes(data);            // 使用 SHA256 + Pkcs1 填充进行签名            byte[] signature = rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);            return Convert.ToBase64String(signature);        }    }    // 用公钥验证签名 - 客户端使用    publicstaticboolVerifyData(string data, string signature, string publicKey)    {        try        {            using (var rsa = RSA.Create())            {                rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKey), out _);                byte[] dataBytes = Encoding.UTF8.GetBytes(data);                byte[] signBytes = Convert.FromBase64String(signature);                return rsa.VerifyData(dataBytes, signBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);            }        }        catch        {            return false;        }    }}

3.整合模块,提供统一接口

// 授权数据public class LicenseModel{    public string MachineCode { getset; }  // 绑定的机器码    public DateTime ExpireDate { getset; } // 过期时间    public string[] Features { getset; }   // 开启的功能模块}// 授权文件结构public class LicenseContainer{    public string Payload { getset; }   // LicenseModel 的 JSON (明文,Base64格式)    public string Signature { getset; } // Payload 的 RSA 签名}public static class LicenseManager{    // 替换为上面 GenerateKeys 生成的实际公钥    private const string PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."    // 验证授权文件    publicstaticboolValidate(string licenseFileContent, out LicenseModel? model)    {        model = null;        message = string.Empty;        try        {            // 提取授权文件            var container = JsonSerializer.Deserialize<LicenseContainer>(licenseFileContent);            if (container == null)                return false//授权文件格式损坏            // 用公钥验证签名            bool isSignatureValid = SecurityGuard.VerifyData(container.Payload, container.Signature, PUBLIC_KEY);            if (!isSignatureValid)                 return false;//授权文件被篡改            // 提取授权文件内部数据            string jsonPayload = Encoding.UTF8.GetString(Convert.FromBase64String(container.Payload));            model = JsonSerializer.Deserialize<LicenseModel>(jsonPayload);            // 验证授权硬件            string currentMachine = DeviceFingerprint.Value;            if (model?.MachineCode != currentMachine)                return false;//硬件不匹配            // 验证授权是否过期             if (DateTime.Now > model.ExpireDate)                 return false;//授权过期            return true;//授权有效        }        catch         {            return false;//验证过程发生异常        }    }}

4.时间篡改检测

防止“时间倒流”:C# 上位机中的时间篡改检测这篇文章中有完整的解决方案,这里不再提供。

5.WPF 客户端用法

这里假设在程序启动时进行授权检查,可根据实际需求更改检查时机。

public partial class App : Application{    protectedoverridevoidOnStartup(StartupEventArgs e)    {        base.OnStartup(e);        //授权文件路径        string licensePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic");        bool isSuccess = false;        if (File.Exists(licensePath))        {            string content = File.ReadAllText(licensePath);            // 调用校验逻辑            if (LicenseManager.Validate(content, outvar model))                 isSuccess = true;// 授权通过        }        if (!isSuccess)        {            // 获取本机机器码,方便用户复制发给开发者            string machineCode = DeviceFingerprint.Value;            string prompt = $"验证失败: {failReason}\n\n本机机器码: {machineCode}\n\n请联系供应商获取有效授权文件。";            // 也可以在这里弹出一个简单的注册窗口,让用户导入文件             MessageBox.Show(prompt, "系统授权", MessageBoxButton.OK, MessageBoxImage.Error);             Shutdown();         }        else        {            // 正常启动主窗口            new MainWindow().Show();        }    }}

6.授权文件生成器

需要一个小工具用于给客户生成授权文件,这里使用控制台举例。

// 生成密钥对 (仅运行一次) // SecurityGuard.GenerateKeys(out string pub, out string pri);// Console.WriteLine("Private Key (存好!): " + pri);// Console.WriteLine("Public Key (放入WPF代码): " + pub);// ----------------------------------------------------// 导入私钥 (从安全的地方读取)string privateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoI..."// 填写授权信息var model = new LicenseModel{    MachineCode = "E32F5A1189B2CC01"// 客户发来的机器码    ExpireDate = new DateTime(20261231),//有效时间    Features = new[] { "MotionControl""VisionInspection" }//需要解锁的功能};// 使用私钥进行签名string jsonPayload = JsonSerializer.Serialize(model);string payloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(jsonPayload)); string signature = SecurityGuard.SignData(payloadBase64, privateKey);// 生成授权文件var container = new LicenseContainer{    Payload = payloadBase64,    Signature = signature};string finalFileParams = JsonSerializer.Serialize(container);File.WriteAllText("license.lic", finalFileParams);Console.WriteLine("授权文件 license.lic 生成成功!");

#WPF #CSharp #上位机 #软件授权

C# 高性能编程:深入解析 Lazy<T> 的用法和使用场景
拔掉网线,上位机必须“假装没事发生”

System.Text.Json 使用指南

详解工业自动化中的NTP时间同步:从IPC到PLC的全链路打通

【技术干货】揭秘数据的“数字指纹”:哈希算法(Hash)

【技术科普】给你的数据加把锁:一文读懂对称加密与非对称加密

拒绝蓝屏重启!工控机操作系统终极避坑指南

【硬核C#】告别重复代码!一文读懂AOP面向切面编程

通俗理解C#委托与事件:像搭积木一样组装代码逻辑

 喜欢就点个赞 
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 工控软件保卫战:WPF上位机授权方案解析

评论 抢沙发

6 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮