65.9K
CodeProject 正在变化。 阅读更多。
Home

如何:在 .NET 中进行组件许可

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (23投票s)

2013 年 5 月 29 日

CPOL

9分钟阅读

viewsIcon

117401

downloadIcon

7670

开发 .NET 中的组件许可系统。

引言

您已经创建了下一个被数千名开发人员想要的超级小部件,但您有一个问题:如何确保您的客户购买许可证,而不是仅仅共享 DLL?您需要实施一个许可系统……在这里,我们将探讨如何实施一个许可系统并有效保护您的知识产权。

第一步 - 决策 

关于您的许可系统,您需要做出一些决定,第一个是如何处理许可证代码的生成。有很多方法,在我的网站上,我选择了 Wordpress + WooCommerce + Software Plugin。该软件插件生成许可证代码,这些代码可用于稍后验证许可证,并能处理基于应用程序的(另一篇文章)许可的激活。

之后,您将需要使用多种方法的组合来有效保护您的软件。在实现许可系统后,您需要防止反向工程。作为免责声明,我要说没有任何混淆工具是 100% 有效的,但我认为这里的目标是使其反向工程和破解代码比购买许可证密钥更费力。在本文的最后,我将列出一些混淆工具以及我对它们的看法,供您参考。

设计许可系统 

.NET 有一个内置的许可系统,它非常有用,尽管在不修改默认实现或提供的许可证提供商(LicFileLicenseProvider)的情况下,它几乎很容易被破解。LicFileLicenseProvider 只是在一个文本文件中保存一个包含完整类型名称和版本的许可证文件。如果文本文件包含此文本,则该组件已被许可。

那么,我们将需要处理哪些许可组件呢? 

  • LicenseManager   
    • LicenseManager 是一个实用类,可让我们验证和使用许可系统。我们将在其上使用的方法主要是 `Validate()` 方法。
  • LicenseProvider  
    • 这是一个抽象类,我们将从中派生来实现我们的提供程序。提供程序负责(令人惊讶地)向 LicenseManager 提供许可证。您将在已许可的组件上使用 LicenseProviderAttribute 来将 LicenseManager 指向正确的提供程序。
  • 许可证
    • 这是另一个我们将为许可证派生的抽象类。只有一个属性需要重写,那就是 `LicenseKey` 属性(实现时还需要实现 `Dispose` 方法,但除非您使用需要处理的某些东西,否则可以将其留空)。 

您可能想知道所有这些组件是如何协同工作的,下面是一个基本图,展示了组件之间的交互方式。

基本上,我们有蓝色部分的“可许可组件”。您使用 LicenseProviderAttribute 和您的实现许可证提供程序的类型来装饰这个类。在您的类中的某个地方(可能是构造函数中),您调用 LicenseManager.Validate 方法来验证许可证。许可证管理器返回一个 License 的上下文(您可以将其强制转换为您的实现的 License 类型),并检查特定于许可证的属性。

实现许可证 

从最后开始可能显得有些违反直觉,但稍后我们将在许可证提供程序中用到此组件,所以现在我们需要从许可证开始。 

所以,让我们直接进入代码。

/// <summary>
/// License granted to components.
/// </summary>
public class ComponentLicense : License
{
} 

在这里,我们将类派生自 License 基类型。在我提供的实现中,我将构造函数设为私有,并添加了内部静态方法以允许创建不同类型的许可证,但现在让我们看看类的核心部分: 

private bool VerifyKey()
{
    //Implement your own key verification here. For now we will
    //just verify that the last 8 characters are a valid CRC.
    //The key is a simple Guid with an extended 8 characters for
    //the CRC.

    if (string.IsNullOrEmpty(_licKey))
        return false;

    //Guid's are in the following format:
    //F2A7629C-5AAF-4E86-8EC2-64F73B6A4FE3
    //Developer keys are an extension of that, like:
    //F2A7629C-5AAF-4E86-8EC2-64F73B6A4FE3-XXXXXXXX

    //So a developer key MUST be 45 characters long

    if (_licKey.Length != 45)
        return false;

    //It must also contain -'s
    if (!_licKey.Contains('-'))
        return false;

    //Now split it
    string[] splitKey = _licKey.Split('-');

    //It has to have 6 parts or its invalid
    if (splitKey.Length != 6)
        return false;

    //Join elements 1 through 5, then convert to a byte array
    string baseKey = string.Join("-", splitKey, 0, 5);
    byte[] asciiBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(baseKey);

    //Get the CRC
    uint baseCRC = Crc32.Compute(asciiBytes);

    //Now let's compare the CRC to make sure its a valid key
    if (string.Equals(splitKey[5], baseCRC.ToString("X8"), 
                        StringComparison.OrdinalIgnoreCase))
        return true;

    return false;
} 

这只是一个示例,您可以根据自己的需要修改此方法来创建任意复杂的许可系统,但这是我的许可系统的工作方式: 

许可证密钥只是一个 GUID,后面附加了一个 CRCCRC 是通过获取格式化为字符串的 GUID,获取其 ASCII 字节,然后通过 CRC32 算法运行它来计算的。CRC 以 8 个十六进制字符的形式附加到许可证密钥中。

验证例程会执行一些基本检查,以验证它是一个有效的 GUID 格式的字符串,然后计算字符串部分的 CRC,并将其与许可证密钥进行比较。如果密钥匹配,则许可证有效,如果不匹配,则将返回演示许可证。

正如我上面所说,这只是一个示例,说明您可以做什么。您可以创建开发者代码,这些代码可以与 Web 服务进行验证,以将开发者锁定到一台计算机,只允许在星期三进行开发,使用 Web 服务根据订阅进行开发,等等。实际的实现取决于您,您能让它变得更复杂,您就能做得更好。您甚至可以在许可证密钥中嵌入数据以启用不同的功能。

创建许可证提供程序 

下一步是为您的组件实现自定义许可证提供程序。 许可证提供程序告诉许可证管理器如何获取许可证。您也可以在这里添加验证代码,实现方式完全取决于您。

/// <summary>
/// Gets a license for an instance or type of component.
/// </summary>
/// <param name="context">A <see cref="LicenseContext"/>
//     that specifies where you can use the licensed object.</param>
/// <param name="type">A <see cref="System.Type"/>
//    that represents the component requesting the license.</param>
/// <param name="instance">An object that is requesting the license.</param>
/// <param name="allowExceptions">true if a
///       <see cref="LicenseException"/> should be thrown
///       when the component cannot be granted a license; otherwise, false.</param>
/// <returns>A valid <see cref="License"/>.</returns>
public override License GetLicense(LicenseContext context, 
       Type type, object instance, bool allowExceptions)
{
    //Here you can check the context to see if it is
    //running in Design or Runtime. You can also do more
    //fun things like limit the number of instances
    //by tracking the keys (only allow X number of controls
    //on a form for example). You can add additional
    //data to the instance if you want, etc. 

    try
    {
        string devKey = GetDeveloperKey(type);

        return ComponentLicense.CreateLicense(devKey);
        //Returns a demo license if no key.
    }
    catch (LicenseException le)
    {
        if (allowExceptions)
            throw le;
        else
            return ComponentLicense.CreateDemoLicense();
    }
}

/// <summary>
/// Returns the string value of the DeveloperKey static property on the control.
/// </summary>
/// <param name="type">Type of licensed
///      component with a DeveloperKey property.</param>
/// <returns>String value of the developer key.</returns>
/// <exception cref=""></>
private string GetDeveloperKey(Type type)
{
    PropertyInfo pInfo = type.GetProperty("DeveloperKey");

    if (pInfo == null)
        throw new LicenseException(type, null, 
           "The licensed control does not contain " + 
           "a DeveloperKey static field. Contact the developer for assistance.");

    string value = pInfo.GetValue(null, null) as string;

    return value;
} 

在此类中,您只能重写一个方法,即 `GetLicense` 方法。在我提供的实现中,我使用反射来获取存储在控件的静态属性中的开发者密钥。然后将此密钥传递给 ComponentLicense.CreateLicense 静态方法。 

您还可以使用传递到 `GetLicense` 的上下文和实例数据来检查其他数据或在许可系统中使用它。在上面的实现中,我们所做的就是从已许可组件的开发者密钥属性创建许可证。还值得注意的是 `allowExceptions` 属性的使用,以了解是否允许抛出异常。我不太确定何时使用哪一个,但正确实现该方法很重要。

这就是创建自己的组件许可系统所需的所有内容。接下来,我们将探讨如何使用它。

使用您的许可系统 

使用您的许可系统并不困难。在我包含的示例项目中,有一个实现了我们许可系统的已许可控件。这是该组件的完整代码。

[LicenseProvider(typeof(Ingenious.Licensing.ComponentLicenseProvider))]
public partial class LicensableControl : UserControl
{

    #region Fields

    private bool _isDemo = true;
    private static string _licKey = "";

    #endregion

    #region Properties

    /// <summary>
    /// Gets/sets the developer key for use on this control.
    /// </summary>
    [Category("License"), 
      Description("Sets the license key for use on the control.")]
    public string LicenseKey 
    {
        get { return LicensableControl.DeveloperKey; }
        set 
        {
            LicensableControl.DeveloperKey = value;
            CheckLicense();
            this.Invalidate();
        }
    }

    [Browsable(false)]
    public static string DeveloperKey { get; set; }

    #endregion

    #region Construction / Deconstruction 

    public LicensableControl()
    {
        InitializeComponent();
        SetStyle(ControlStyles.ResizeRedraw, true);
        SetStyle(ControlStyles.UserPaint, true);
        
        CheckLicense();
    }

    #endregion

    #region Protected Methods

    protected override void OnPaint(PaintEventArgs e)
    {
        string text = (_isDemo ? "This control is running in demo mode." : 
                          "This is a fully licensed control.");

        StringFormat sf = new StringFormat();
        sf.LineAlignment = StringAlignment.Center;
        sf.Alignment = StringAlignment.Center;

        e.Graphics.DrawRectangle(Pens.Black, 0, 0, this.Width - 1, this.Height - 1);
        e.Graphics.DrawString(text, Font, Brushes.Black, ClientRectangle, sf);
    }

    #endregion

    #region Private Methods

    private void CheckLicense()
    {
        //If we want to only grant a license for design time, we can check like this:
        /*
        if (Site != null && Site.DesignMode)
        {
            //Check license here
        }
        else
        {
            _isDemo = false;
            //Allow runtime usage.
        }
        */

        ComponentLicense lic = 
          LicenseManager.Validate(typeof(LicensableControl), this) as ComponentLicense;

        if (lic != null)
        {
            _isDemo = lic.IsDemo;
        }
    }

    #endregion
}

您可以看到,让系统正常工作并不复杂。首先,您需要使用以 ComponentLicenseProvider 类型为提供程序的 LicenseProvider 属性来装饰您的类。然后,在构造函数中,我们可以检查许可证并设置一个标志(或保存整个许可证,如果其中包含更多元数据)。 

在我的实现中,我允许开发者在其中一个控件的属性窗口中输入许可证密钥,它会通过静态属性传播到其他控件。但值得注意的是,如果您这样做,控件在您最初粘贴开发者密钥的窗体打开之前将不会被许可。要解决此问题,请在您的 `Program.cs` 文件中,将 `LicensableControl.DeveloperKey` 设置为开发者密钥。唯一的问题是设计器将无法识别这一点,在设计器中它将看起来像一个未许可的组件。我在这方面没有花太多时间,因为它超出了本文的范围。

一些值得一提的事情 

选择一种许可方法并**坚持下去**非常重要。问题在于,当您更改许可系统时,会疏远现有的用户群。在游戏过程中更改许可系统可能是一个昂贵的教训,对您的用户来说也很痛苦。

混淆

我答应过要谈谈这个话题,现在来了:混淆是获取原本可读的代码并将其变成无法理解的垃圾的过程。主要目标是使原本易于反编译的程序变得即使被反编译,其含义也隐藏起来。 

即使是最聪明的混淆方法也有规避的方法,但正如我在文章前面所说的,目标是使反编译比用户直接购买许可证的成本更高。一个真正坚定的用户无论您做什么,都可以将您编译的混淆代码还原为源代码。

我花了一些时间评估一些混淆工具,以下是我对一些我找到的工具的看法: 

  • Redgate SmartAssembly
    • 来自我用过的最好的反编译工具(Redgate Reflector)的同一家公司,有一个可以避免反编译的工具,SmartAssembly。我没有评估此工具,因为价格昂贵,而且我本来就对 .NET 社区保持 Reflector 免费的承诺感到犹豫,然后又回过头来将其变成一个相当昂贵的工具。
  • Preemptive Solutions Dotfuscator
    • 唯一被信任可以与 MS VS 一起装箱的产品。实际上,我认为这是一个(在某种程度上)聪明的营销策略,因为 DotFuscator 的“社区”版是个笑话,只使用简单的变量重命名,这很容易被破解。大多数初学者开发者都买不起完整版的 DotFuscator。
  • SecureTeam Agile 6.0 Code Protection (CliSecure) 
    • 在我测试过的产品中,这是最好的。它有大量的选项,可以让反向工程变得非常困难。这是我购买的产品,虽然它相当昂贵,但我发现它产生了可用的程序集(有些产品将其损坏得如此严重,以至于它们甚至无法运行)。 
  •  Eziriz .NET Reactor 
    • 这是目前真正负担得起的混淆工具之一,价格为 179 美元,而且效果很好。我以前曾使用过同一家公司的 .NET Licensing 产品,效果不错,然后决定自己开发。

还有很多其他的,我只提了一些比较知名的,所以请自行搜索并决定。请记住,您让最终用户绕过许可的难度越大,您就能保留更多的收入。您也可以完全绕过许可,只销售支持合同,但“免费”软件的概念可以留给另一篇文章。

值得关注的点  

许可可以像您想要的那么复杂或那么简单,.NET 拥有一套非常强大的基类可供构建。正确地扩展这些类可以让您通过销售软件获利,而不仅仅是出于乐趣。在未来的文章中,我将探讨如何使用许多相同的概念来实现应用程序级别的许可。

历史

  • 2013 年 5 月 28 日 - 初始发布。
© . All rights reserved.