为Microsoft .NET框架创建应用程序时,你获得的最大的一个承诺就是能避免所谓的
DLL地狱。它是指当一个组件更新后,可能会中断依赖于它的其他应用程序。然而,为了理解这个承诺,开发者需要熟悉“强名称”(Strong Names)的概念与实现。本文将引导你理解强名称在托管代码中的应用。
为什么要使用强名称
在讨论强名称的好处之前,先来看看它的定义。强名称由用于标识一个程序集的信息构成,其中包括程序集的文本名称、分为4部分的版本号、区域性信息(如果有的话)、一个公钥以及一个数字签名。这些信息存储在程序集的清单(manifest)中。清单包含了程序集的元数据,并嵌入程序集的某个文件中。
注意
大多数程序集(比如使用Visual Studio .NET创建的那些)都是单文件程序集,也就是只有一个.exe或者.dll文件。在这种情况下,清单直接嵌入单文件程序集中。但是,你可用“程序集生成工具”(Al.exe)来创建多文件程序集。
在程序集中包括一个强名称后,公共语言运行库(CLR)可保证具有相同强名称的两个程序集在任何方面都是完全一致的。换言之,强名称为CLR提供了一个程序集的惟一性标识。除此之外,添加一个强名称还可确保二进制完整性,因为CLR可在程序集加载时执行验证,判断它自从编译以来是否被篡改过。
在两种主要的情况下,开发者应为一个程序集包括强名称:
- 共享程序集。通过包括强名称,程序集在安装到“全局程序集缓存”(GAC)之后,就由同一台机器上运行的多个应用程序共享。这种代码共享模型与非托管世界中使用的模型刚好相反。在非托管世界中,COM组件一旦编译并在系统注册表中注册,就自动共享。
- Serviced Component。一个.NET类要想使用企业服务(COM+服务),比如分布式事务处理和对象池等等,那么包含类(称为Serviced Component,因为它从EnterpriseServices.ServicedComponent类继承)的程序集必须有一个强名称。有了强名称后,企业服务才能保证加载正确的程序集。企业服务(DLLHost.exe)容纳的进程中所运行的Serviced Component应放到GAC中;相反,作为库应用程序在调用者的进程中运行的那些ServicedComponent则不必这样做。
在第一种情况下(共享程序集放到GAC中),主要的优点包括:
单一部署
由于同一台机器上的所有应用程序都从GAC加载共享程序集,所以不需要为每个应用程序都部署程序集。这有别于.NET框架默认的“私有程序集”,它必须随同每个应用程序来部署。
绕过验证
如前所述,强名称允许CLR的类加载器验证程序集自编译后便没有被篡改过。将程序集放到GAC中,只有在程序集首次放入GAC时才会执行验证,而不是应用程序每次加载它时都执行验证,这有助于提升性能。
减少工作量
如果多个应用程序引用同一个共享程序集,所有应用程序都从相同的位置加载程序集。因此,操作系统在所有应用程序中共享程序集的代码页,这减少了内存占用。
集中更新
程序集部署到GAC后,就可集中部署修正内容。虽然应用程序默认使用程序当初编译时的版本,而且GAC允许相同程序集的多个版本并存,但只需在machine.config文件中添加一个版本策略,即可强迫机器上的所有应用程序使用新版本,而不是使用旧版本。除此之外,一旦程序集包含强名称,其他代码就可指定:只有来自具有特定强名称的一个程序集中的代码才能调用自己。例如,为一个类添加StrongNameIdentityPermissionAttribute后,就可确保只有具有指定强名称的调用者才能创建这个类的实例,如清单A所示。这个例子指定了PublicKey属性,所以凡是使用与这个公钥配对的私钥签署的任何程序集,都允许实例化Products类。
要注意的问题
虽然强名称提供了许多好处,包括允许代码共享,并允许托管代码使用企业服务等,但开发者需要注意以下问题:
调用私有程序集
假如一个共享程序集试图从私有程序集加载一个类型,CLR会引发一个异常,因为共享程序集只能引用其他共享程序集。正是因为存在这个限制,所以避免了DLL冲突。
受信任的程序集
强名称虽然引入了“身份”的概念,但没有包括“信任”机制。例如,使用强名称签署的一个程序集虽然能保证版本兼容性,但不能保证要加载的程序集来自Quilogy。为了用Authenticode数字签名来签署程序集,开发者要使用.NET框架配套提供的命令行实用程序Signcode.exe。程序集使用Authenticode签名进行签署之后,管理员就可创建相应的策略,利用代码访问安全性(CAS)机制,允许它下载到用户的机器上并进行加载。签名将成为CLR的类加载器所使用的身份凭证的一部分,用于判断程序集是否应该加载。
安装问题
具有强名称的程序集通常放在GAC中,假如应用程序必须将程序集安装到GAC中,安装过程就必然会复杂一些。幸好,用Visual Studio .NET创建的Windows Installer项目可将程序集自动安装到GAC中。此外,开发者还可使用命令行实用程序Gacutil.exe。
部署问题
与安装密切相关的就是部署问题。.NET框架应用程序最大的一个好处在于,它们可采取一种XCOPY方式来部署。也就是说,应用程序的目录可直接移动到另一台机器,而不必注册组件。然而,如果使用了共享程序集,这一过程就没那么简单了,因为当应用程序部署到另一台机器时,共享程序集也必须安装到GAC中。
强名称的用法
要为程序集创建一个强名称,开发者可使用程序集创建工具(Al.exe),或将System.Reflection命名空间中的属性包括到自己的代码中。但是,首先必须准备好一对公钥和私钥,以便用它们来签署程序集。在准备生成强名称的那台机器上,可用一个文件来包含这个密钥对,或者将其放到“加密服务提供程序”(CSP)内的某个密钥容器中(最终放到注册表中)。
要想在文件中创建密钥对,可使用.NET框架提供的“强名称”实用程序(Sn.exe)。例如,要创建名为keyfile.dat的一个文件,并在其中包括新的密钥对,可以像下面这样运行实用程序:
Sn.exe –k keyfile.dat
随后,程序集生成工具可利用这个密钥文件来生成强名称:
Al.exe /out:AtomicData.dll /keyfile:keyfile.dat
利用程序集生成工具的开关选项,你可指定使用一个特定的CSP及密钥容器。更常见的情况是,开发者可从AssemblyInfo.vb(或.cs)文件中选用某个属性,包括AssemblyKeyFileAttribute、AssemblyKeyNameAttribute或者AssemblyDelaySignAttribute:
<Assembly: AssemblyKeyFile("keyfile.dat")>
<Assembly: AssemblyVersion("1.0.0.*")>
在本例中,包含密钥对的文件将在编译时访问,以创建强名称并将其放到程序集清单中。另外,开发者可使用AssemblyKeyNameAttribute来指定用于存储密钥对的容器的名称。
这里展示的技术适用于小公司或者个人开发。在大型企业中,用于签署代码的密钥对往往会被严密看守。在这种情况下,开发者一般只能访问公钥,私钥只掌握在少数几个受信任的人的手中。但是,开发期间对公钥的访问仍是至关重要的,因为引用“强名称程序集”的任何程序集都必须在自己的清单(manifest)里包含公钥。为了确保开发过程的顺利进行,.NET框架也支持推迟签署或部分签署。
“部分签署”是指在编译时,在程序集清单中为完整的强名称签名保留空间。签名可在以后的某个时间添加。同时,其他程序集仍可引用强名称程序集。为了实现“推迟签署”,AssemblyInfo文件可包括AssemblyDelaySign属性,并向构造函数传递True。这意味着AssemblyKeyFile中引用的密钥文件只包含公钥(可使用Sn.exe工具的一个开关来完成)。
如果程序集是部分签署的,它的验证功能也必须关闭。这是由于部分构造的强名称是无效的,不能通过前面说过的二进制完整性检查。要绕过验证,请使用Sn.exe工具的–Vr开关:
Sn.exe –Vr AtomicData.dll
在以后某个时候,可将程序集拿给掌握着私钥的人,由其使用–R开关来签署强名称:
sn.exe –R AtomicData.dll keyfile.dat
小结
为程序集创建一个强名称,并利用全局程序集缓存(GAC),开发者就可与机器上的其他应用程序共享程序集。另外,还可访问包括分布式事务处理和对象池在内的“企业服务”。但是,创建强名称和使用GAC之后,编译时和部署时必须进行更多的工作。开发者应仔细考虑对于自己创建的程序集来说,创建一个强名称是否划算。