给.NET程序加上许可证Licenses

摘要 : To. NET procedures and the permit Licenses

我们知道,要实现一套.Net中的许可证验证机制还需要一个类作为License的具体实现,而这个类在.Net Framework的类库中是无法找到的,但这个具体的实现类是肯定存在的。打开.Net Framework中自带的MSIL反汇编程序(ildasm.exe)打开System.dll查看,发现在LicFileLicenseProvider内部定义了一个私有类LicFileLicense,这个类就是License在这套许可证验证机制中的具体实现。

If you're reading this, you're undoubtedly a Windows software developer. And if you're a Windows software developer, you probably write software because you're passionate about, well, writing software. I'd happily code all day long just because I enjoy it, and I'm sure many of you feel the same way. Could there be a better way to make a living? 
如果你正在阅读本文,毫无疑问你是一个Windows平台软件的开发人员。如果你是一个Windows软件的开发人员的话,你写软件或许是因为你对写软件充满了jiqing。我乐于整日地编写代码,因为觉得这是种享受。我相信你在很多方面与我感觉相同。这或许是生活的好方式。

可是这是可遇而不可求的事。如果你不考虑写软件的经济因素,我们会遇到一个简单平实的道理:当我们因为喜欢 而努力工作时,我们要养家糊口,要付该死的帐单,坦率而言,我们还想全家偶尔去外出渡假。毕竟使用我们软件的很多人可能对他们职业的态度与我们相同,但是 日常生活中人们对此的看法却很不公平。他们工作并想为他们的勤奋工作得到报酬,甚至他内心里热爱他们正在做的和从事的事情。

问题是当最终用户得到软件时,软件经常成了一件说不清楚的事情。他们所看到的全部东西只是应用程序自身,甚 至,这个程序只会在有电时存在电脑内存短暂的一会儿。似乎很难看出这是花了成百上千个小时开发测试出来的产品,或者至少无法和汽车、电冰箱这样的物理直观 的东西比较。正直、勤奋的电脑用户从不会想到要偷窃一辆汽车或是一台电冰箱,但是他们会经常与朋友交换软件,他们甚至知道这是有违软件的授权协议的。但是 软件仅仅是CD上的数码而已,对吧,把软件传给朋友或兄弟它能有什么损伤吗?

不错,你知道这个答案是什么——用户没有对你的努力工作付钱。按商业术语的说法你没有了收入。如果你没有足 够的收入,作为商家,你无法维持你的生意。作为个人,你可能不能承担你的财务支出(你付费的帐单)。因此你或是找一个速食连锁店工作,或是宣布破产并且去 住在某个高速路桥下面的纸板房(cardbox)里。就本我而言,我宁愿相信用户会为软件付费,会对我设计、开发、测试、市场宣传、分发软件所作出的努力 付费的。

解决方案就是软件授权许可证。作为一个工业,许多人设法以各种方式解决这个问题。在过去,软件是存在一种防 复制的媒介(记得是在一种有多余磁轨的软盘上。译者注:也有在磁盘上人为制造一个坏区的)上销售的。现在通常的做法是要求你键入一串密钥才能使软件包正常 使用。微软最近开始对他们高端的操作系统和产品(诸如Office,可能是最经常盗版的软件)进行基于互联网的在线软件授权。在.NET Framework里,也存在这样的技术宝藏,你能用它来颁发你的软件的授权许可证,这正是我现在就要展示给你的。

控件许可证

几乎你阅读的有关.NET 许可证的每样东西都是把两种许可证概念绑定在一起的。一种控件的许可证概念,另一种思想是控件开发者所装配的控件许可证可以在设计时进行或是在运行时进 行。当许可发生时,你可以把框架许可(Framework licensing)应用到派生自System.Windows.Forms.Control的任何类,它包含了整个Windows Forms的应用程序,但是我会以控件自身开始。图1所示为许可证UML静态类的基本控件图表。


图1. 许可证静态类.NET控件图表

在图2所显示(一个UML顺序图表)的是一个通常的的执行顺序。在被许可证控件的构造函数中要求一个来自许可证管理者(LicenseManager)的授权许可证:

license = LicenseManager.Validate(typeof(MyLicensedControl), this);

在这种情况下,构造函数用于控件许可,在MyLicensedControl这个类中实现。极趣的是,在处理许可证对象自身上你可能什么也不需要做(取决于你许可证的实现),除了适当安排附加加资源外。从这个暗箱里,我们采取的重要步骤就是调用,许可证管理器申请一个许可证。如果因某些原因许可证没有被接收认可,Validate() 调用会抛出一个失败异常,如果异常想被得到。反之,如果异常被抑制,则返回一个空许可证。 (LicenseProvider.GetLicense() 控件调用,以及默认的Framework实现都允许异常。)


图2.控件许可顺序

许可证管理者依次调用控件许可证提供者的GetLicense()方法。许可证管理者,一个.NET Framework组件,是如何知道所用的许可证提供者是哪一个的呢?毕竟你提供了许可证提供者... 答案是通过传统.NET方式中的元数据、通过属性:

[LicenseProvider(typeof(MyLicenseProvider))]

public class MyLicensedControl : System.Windows.Forms.Control

{

   ...

}

许可证管理者会寻找附加许可证控件类的LicenseProvider属性。由于LicenseProvider属性把许可证提供者的类型当做构造函数的输入,许可证管理器在要求校验这个许可证时,能够射这个信息。你的许可证提供者必须派生自类LicenseProvider,必须重载GetLicense(),这是一个方法(MustOverride 对于VB用户而言):

public class MyLicenseProvider : System.ComponentModel.LicenseProvider
{
   ...

   public override License GetLicense( LicenseContext context,    Type type,
      object instance,
      bool allowExceptions)
   {
      ...
   }
}

在.NET环境中,允许授权认可的魔法构成了我刚刚描述的程序。但最终,真正的许可证魔法是在GetLincense()方法中编码的。

我们在有太多想象前,你应该了解,微软用框架装配一个被称之为LicFileLicenseProvider的基本的许可证提供者。我会完整地对你描述如何使用LicFileLicenseProvider,我也相信任何发送他们软件许可证的人,很可能会避免这样简单的方案,以利于他们自己。更有建设性的方案,我会在描述完LicFileLicenseProvider之后介绍它们其中一个。

LicFileLicenseProvider

LicFileLicenseProvider的前提简单,它会检查许 可证文件的存在性。如果文件存在就会检查文件内容是否合法。这个文件可能和程序一起保存在磁盘上,或者可能被编码成一个应用程序资源文件,你需要另写一些 代码把它读取出来。如文件存在并且内容合法,许可证提供者会颁发一个许可证给许可证管理者,最终颁发给这个控件。否则,许可证提供者会抛出一个LicenseException 异常,并且你的控件进程将会阻塞并且死亡。

LicFileLicenseProvider重载GetLicense()方法,这是必须的,同时又提供了两个额外的虚拟方法,IsKeyValid()和GetKey()。使用LicFileLicenseProvider,它在框架中实现时,GetKey()会在程序集的执行目录当中寻找一个文件,文件命名格式是{full name}.lic,这里{full name}是许可证控件类的完整类型名,比较代表性的格式是{assembly name}.{class name}。如果程序集 MyControlAssembly声明了我们已经使用的许过控件,在这个文件中许可证文件会被命名成:

MyControlAssembly.MyLicensedControl.lic

IsKeyValid()方法用所掌握的该文件内容与字串"{licensed class} is a licensed component." (包括句点)比较。我举的例子中,文件MyControlAssembly.MyLicensedControl.lic容纳了这个字串"MyControlAssembly.MyLicensedControl is a licensed component.", 如果这个许可证文件丢失了,或是内容不正确,合法性校验会失败,并且这个控件不会被实例化。I've included with downloadable samples solution an example that has a licensed version of the MSDN sample color-picker ComboBox control.我已经提供了一个可下载的样例解决方案,这个例子是MSDN颜色拾取器组合框控件,这是已被许可的版本。

由于IsKeyValid()和GetKey()是 虚拟方法,因此你很容易派生一个新类。这个新类或为许可证文件呈现在别的地方,或对一个不同的字串进行校验(或许是一个被加密的字串、或是一个简单地使用 不同短语的字串)。无论怎样,由于基类已经声明了这样的功能,一个“良好”的方法实现仍旧会基本上处理这些许可证文件。如果你宁愿用一个完全不同的方式来 实现许可证方案的话,直接从LicenseProvider派生一个许可证提供者是一个很好的选择。由于我对大家的智商很有信心,因此我不打算提供提供一个被派生的许可证文件提供者的示例。相反,我会描述一些可选的许可证方案。

  

编译许可证文件

在把所有许可证文件留下前,我应该提一下lc.exe。lc.exe或者说许可证编译器(license compiler),是一个工具,用于装配.NET Framework,它获取许可证文件信息并作为程序集资源进行编码。由于LicFileLicenseProvider默 认的实现方式并不会去寻找一个基于资源的许可证文件,因此这个工具是被Framework临时添加进来的,但是它有某些令人感兴趣的灵活性。首先它产生资 源信息方法很简便。更令人感兴趣的是,它能把多个许可证文件编码成单一资源,并且允许你把不同的许可证文件字串一次性地许可给多个控件。在我看来,它也存 在着比较大的局限性,也就是说你必须使用程序集链接器(al.exe)来汇编你已编译过的模块和资源。对于敲命令行有偏爱的人来说这不是个问题。由于 Visual Studio .NET不会编译或生成多模块汇编,因此对于大多数使用Visual Studio .NET的人而言,这是一个限制(虽然你可用一个“内嵌资源”的build操作把资源输入嵌入到你的项目文件当中)。

说到Visual Studio .NET,你会发现如果想创建一个颁发许可证的控件,你找不到自动化的支持,无论是Visual Studio的2002版还是2003版。因此怎样才能创建这样的一个控件呢?答案是,你简单地手动创建一个这样的文件并且把它和编译过的程序集装配在一 起。就我个人而言,把这个文件用“none”的build操作添加到Visual Studio的项目当中,这样做没有必要。当这个文件和别的项目/解决方案文件被包装在一起时,这仅仅使源代码控件稍微容易了点。还要不得不手动把这个文 件复制到程序集执行目录当中。

可选的许可证方案

我认为微软有义务为.NET Framework提供一个默认的许可证提供者,并且选择类似于ActiveX控件许可证的实现方式。我也认为微软不会考虑把这么简单的许可证方案应用于 他们的任何软件产品当中(或者至少是任何重要的软件产品)。无论怎样,我们为什么提供一个更容易装配、更智能化、更难于破解的许可证方案来取而代之呢?下 面就是这个可选的许可证方案。

你可以以你喜欢的任何方式对你的控件颁发许可证。或许你能设计成仅在周二运行,或者你能根据用户本地区的天 气预报是否是晴天来决定是否在一个程序中执行这个控件(例如:获取用户的邮政编码并从互联网上下载相关的天气预报)。我绝不是在开玩笑。你可以做到。这个 许可证颁发流程灵活到你可以做到这些,但这并不意味着你应该这样做。我将演示一个更实际的方法,采用注册表许可证方式。在我详细描述这个方案前,你应该知 道一个事实:

.NET Windows Forms应用程序派生于System.Windows.Forms.Form,它轮流由System.Windows.Forms.Control派生!

等等……这意味着什么呢?不错,这意味着,你象给一个控件颁发许可证一样很容易就能对整个应用程序进行颁发许可授权。虽然我给你演示的是以应用程序而不是控件的许可证授权为目标的,但是它同样可以应用于这两种情况。让我们思考一会儿这句话的含义吧。

应用程序和控件许可证

意识到应用程序与控件许可证间的基本差别是当你真正关心许可证的时候。如果你为了谋生开发了些控件和组件 ,目标市场是由别的软件开发者构成。这意味着你的许可证方案要在设计时(design time )中对加入到Visual Studio 项目的插入操作做合法性检查。由于你可能想让你的控件库销售量达几百万份,你会免费提供这个运行时许可证,或者根本不强加上一个运行时的许可证。毕竟你的目标市场是销售给应用程序,这样他们会从你这里买更多的控件。

然而应用程序的许可证会有所不同。我们自己在设计时构建应用程序,不会想到把设计时许可证强加于自身。相反,我们希望的是在运行时(run time)的适当位置确保用户得到相应的许可证信息。

框架设计者在他们设计许可证结构时考虑到了这一点,当GetLicense() 调用发生时,你可以检查许可证的请求状态,无论是在设计时还是运行时。这两种情况我都可以演示给你,但我们现在要考虑的校验许可证仅仅是一种情况就是运行时,这是由于这是应用程序的许可证颁发不同于控件的。因此,你应该意识到这是一个重要差别。

基于注册表的许可证

基于注册表的许可证实现了一个许可证的方案,这种方案会检查含有指定键值的注册键存在性。应用程序自身不必 为实现写一个注册表键值而编写代码──这由安装程序来完成。由于许多程序都以各种各样的形式利用注册表,因此这不会成为发展的限制。我们的安装程序会提前 预占一个注册键值,那时起就有了注册。

对于写入注册表中的键值,如果我愿意还可以写的更好些,但是对这个例子而言,我仅仅简单的写“Installed”这个字串。我会留些更好的实现方式给你(hey,我不能把我的全部秘密给你!)。下面这个键值是我要找的:

  

HKEY_CURRENT_USER\Software\Acme\HostKeys\2de915e1-df71- 3443-9f4d-32259c92ced2

你看到 GUID 值是我分配给我的应用程序的一个GUID。Acme Software销售的每个应用程序都有自己的GUID,但是通过反射,我能对所有的应用程序使用相同的许可证提供者。我会在这里填满所有Acme Software许可证键值,因此这个键名就是“主键”(HostKeys)。在你自己的应用程序中,你可随意的把这个主键放在任何合适的地方。

正象你在图3看到的应用程序非常简单。


图 3. 基于注册表许可证的应用程序

如果你看到这个窗口,这份许可证是合法的。另一方面,如果这份许可证是非法的话,你会看到图4的异常对话框。


图 4. 非法注册表许可证反馈

为了使之运行,我们需要创建一个新的应用并且要对用设计向导产生的代码做些轻微的改动。顺便说一句,虽然在此展示的代码是C#,但是我没轻视VB程序员的意思,这些例子比较容易转换,另外我这篇文章附加的示例源代码当中也提供了C#和VB.NET两种语言。

开始前,我们除了写我们自己的许可证提供者以外,要对主程序的四个地方修改源代码。这四个地方是:

  1. 应用程序类的声明(加些属性)

  2. 应用程序类的构造函数

  3. 应用程序类的Dispose()方法

  4. 应用程序的"Main"方法

由于我使用了GuidAttribute ,因此我会增加一个using子句。我们先看看类声明:

  

/// <summary>
/// Summary description for frmMain.
/// </summary>
[GuidAttribute("2de915e1-df71-3443-9f4d-32259c92ced2")]
[LicenseProvider(typeof(RegistryLicenseProvider))]
public class frmMain : System.Windows.Forms.Form
{
   ...
}

我给这个类加了两个属性:GuidAttribute和LicenseProvider。你已经看过LicenseProvider了,另一个GuidAttribute属 性的目的是分配一个GUID的数值给这个应用程序类。通常这个属性用于COM互操作,我这里重用它,简单地给应用程序类分配一个独一无二的数字识别码,这 是Framework所支持的(也就是说,我们以后能很容易地决定类类型的GUID)。我可以生成自己的属性,可是我却会失去内置的框架GUID的侦测支 持。我指定给GuidAttribute的GUID值一定要和我在注册表键值中使用的GUID值相匹配,这一点非常重要。我的许可证提供者反射给产生应用程序注册表键值的值就是这个值。因为我使用了GuidAttribute 属性,我必须把这个值加进我的命名空间集合中:

using System.Runtime.InteropServices;

应用程序类的构造函数中,我把前面代码填加进去:

private License _license = null;

public frmMain()
{
   //
   // Required for Windows Form Designer support
   //
   InitializeComponent();

   // Obtain the license
   _license = LicenseManager.Validate(typeof(frmMain), this);
}

这里我简单地调用了许可证管理者的 Validate() 方法,无特别之处。但是,如果许可证是非法的, Validate() 方法能抛出一个异常。因此在某些地方捕捉这个异常是个不错的想法。但是也要注意我填加了一个调用_license的私有数据成员。某些许可证方案利用了这个许可证对象(本例中没有提供),但在所有的 情况中,当应用程序终结时我会需要处理许可证。分配给应该正确释放的许可证的可能是一些资源,因此我不得不对应用程序的 Dispose() 方法做修改:

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
   if( disposing )
   {
      if (components != null)
      {
         components.Dispose();
      }

      if (license != null)
      {
         license.Dispose();
         license = null;
      }
   }
   base.Dispose( disposing );
}

这里我对null的许可证做了一个速检,如果值不为null,我调用它的 Dispose() 方法,否则调用它的引用。即使这个样例中我不使用许可证 license,这个操作仍是必要的,因为这个许可证含有必须被释放的资源。

最后,我修改了应用程序的 Main() 方法,要考虑到许可证管理器的异常:

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
   // Create an instance of the licensed application
   frmMain app = null;
   try
   {
      // This will throw a LicenseException if the
      // license is invalid... if we get an exception,
      // "app" will remain null and the Run() method
      // (below) will not be executed...
      app = new frmMain();
   } // try
   catch (Exception ex)
   {
      // Catch any error, but especially licensing errors...
      string strErr =
          String.Format("Error executing application: '{0}'",
                        ex.Message);
      MessageBox.Show( strErr,
                       "RegistryLicensedApplication Error",
                       MessageBoxButtons.OK,
                       MessageBoxIcon.Error);
   } // catch

   if ( app != null ) Application.Run(app);
}

注册表许可证提供者

正如我前面所提到的,大多数许可证的校验工作是由你的许可证提供者的 GetLicense() 方法去做的。这里有些代码,是为我自己的注册表许可证提供者写的(你可以在RegistryLicenseProvider.cs 这个源文件示例中找到):

public override License GetLicense(
   LicenseContext context,
   Type type,
   object instance,
   bool allowExceptions)
{
   // We'll test for the usage mode...run time v. design time.
   // Note we only check if run time...
   if (context.UsageMode == LicenseUsageMode.Runtime)
   {
      // The Registry key we'll check
      RegistryKey licenseKey =
    Registry.CurrentUser.OpenSubKey("Software\Acme\HostKeys");

      if ( licenseKey != null )
      {
         // Passed the first test, which is the existence of the
         // Registry key itself. Now obtain the stored value
         // associated with our app's GUID.
         string strLic =
(string)licenseKey.GetValue(type.GUID.ToString()); // reflected!
         if ( strLic != null )
         {
            // Passed the second test, which is some value
            // exists that's associated with our app's GUID.
            if ( String.Compare("Installed",strLic,false) == 0 )
            {
               // Got it...valid license...
               return new RuntimeRegistryLicense(type);
            } // if
         } // if
      } // if

      // if we got this far, we failed the license test. We then
      // check to see if exceptions are allowed, and if so, throw
      // a new license exception...
      if ( allowExceptions == true )
      {
         throw new LicenseException(type,
                                    instance,
                                    "Your license is invalid");
      } // if

      // Exceptions are not desired, so we'll simply return null.
      return null;
   } // if
   else
   {
      return new DesigntimeRegistryLicense(type);
   } // else
}

  下面这部分检查代码比较令人感兴趣,我们可以检查当前运行在什么模式下,是设计时还是运行时:

if (context.UsageMode == LicenseUsageMode.Runtime)
{
   ... // Run time mode
}
else
{
   ... // Design time mode
} // else

如果在设计时操作,我们会简单地创建并返回一个我们设计时许可证对象的实例:

return new DesigntimeRegistryLicense(type);

然而如果是运行时,我们可以打开我们特殊的注册键值去寻找我们应用程序的GUID键值对。我们如下方式打开注册表键:

RegistryKey licenseKey =
Registry.CurrentUser.OpenSubKey("Software\Acme\HostKeys");

通过反射应用程序GUID,我们生成这个键值。这个GUID来自于我们得到的对象(还记得放在我们许可证类型的GuidAttribute属性吧):

string strLic =
   (string)licenseKey.GetValue(type.GUID.ToString());

GetValue() 方法不是返回与这个键相关联的值就是返回null,因此我们对null值测试,对我们期望返回的值进行检查看是不是代表一个合法的许可证:

if ( strLic != null )
{
   // Passed the second test, which is some value
   // exists that's associated with our app's GUID.
   if ( String.Compare("Installed",strLic,false) == 0 )
   {
      // Got it...valid license...
      return new RuntimeRegistryLicense(type);
   } // if
} // if

如果一切正常,我们会返回我们运行时许可证对象的一个新实例。否则我们会检查是否允许异常,如果是就抛出一个新许可证异常:

if ( allowExceptions == true )
{
   throw new LicenseException(type,
                              instance,
                              "Your license is invalid");
} // if

如异常不被允许,我们就简单的返回null。

这种情形下许可证类是一样的,但是相同不是必需的。运行时许可证如下显示:

public class RuntimeRegistryLicense : License
{
   private Type type;

   public RuntimeRegistryLicense(Type type)
   {
      if ( type == null )
         throw new NullReferenceException(
"The licensed type reference may not be null.");
      this.type = type;
   }

   public override string LicenseKey
   {
      get
      {
         // Simply return the application's GUID
         return type.GUID.ToString();
      }
   }
   public override void Dispose()
   {
   }
}

由于从基类License派生了一个新的许可证类型,我必须重载这个 LicenseKey 属性(它是abstract方法)。当被要求时,会返回应用程序的GUID。不管怎样,我可以重新得到注册表值或者对定期的失效和别的某些非法条件做检查。

更多的其它许可证方案

我在这里演示的技巧相当基础。但是一方面许多用户在没有大量帮助的情况下不能打开注册表找到键值,另一方面有些人有相当的能力去破解这个方案。因此,你要做的就是要让你的许可证方案更难破解。

你所要做的最明显的事就是用一个产品有效期数据来替换我的简单的&ldquo;Installed&rdquo;字串(并且对这个值 加密)。或者你写一个更复杂的许可证提供者,让它调用Windows或Web服务向它们申请一个执行应用程序的许可。这两种方式我都写过非常有效。正如我 提过的,许可证对象自身可以使自己无效,迫使应用程序关闭,它得到的只是些无效的证书。或许可以从Web服务或数据库中检查的许可证来决定这个应用程序日 常的许可证状态。无论在何种情况下,你要确保把你产品汇编后结果做混淆处理!

结论

如果你已经阅读到了这里并且研究了基本的框加许可过程,你可能奇怪:为什么我写了这么多东西都是局限于这个框架(.NET Framework)内的呢?我们难首不能创建一个自己的许可证管理器吗?为什么没有一个非框架的许可证构架呢?只要把一个布尔值传线GetLicense() 方法启用异常呢?(我个人希望是指定给LicenseManger或许可证类一个属性而不是把一个布尔值传给 GetLicense()&hellip;&hellip; 我还没看见过有任何别的方式能改变默认框架行为)

我仅能假设使用框架的许可证架构而不是你自己的这会对你的将来开发更有利。毕竟你要遵循框架规定的模式,改变框架可能会对你造成一定的影响。因为你肯定要利用一些框架提供的组件,重用这些资源可以节省(尽管少)你维护的开销。

就许可证自身而言,唯一的限制是你自己的想象力。现在就去赚回你损失的钱吧!

源代码下载

Download source code: LicensedApps.zip - 76 kb

上一篇 :TFS查看挂起的签入
下一篇 :破解网页加密的源代码