The wind call my name

用知识和思考来丈量世界
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

《Programming .Net Components》学习笔记(十一)

Posted on 2007-06-10 07:13  徐鸿翼  阅读(1231)  评论(2编辑  收藏  举报

强程序集命名

像以前提到的那样,私有程序集驻留于客户端应用程序文件夹中,而共享程序集驻留于GAC中。虽然私有程序集可以直接而简单的使用,但在以下两种情况下,应当考虑使用共享程序集。第一种情况是支持同时使用同一程序集的不同版本。第二种情况是在多客户端应用程序之间共享程序集。共享就意味着一个改进的可兼容的程序集对于多个应用程序可以尽快使之可用,而不用单独修补每一个应用程序的私有程序集。构架和类库提供商就倾向于以共享的方式使用。

为了使GAC可能包含来自于多个提供商的程序集,.NET必须提供一种方式来唯一标识共享程序集。一个例如“MyAssembly”的友好命名是仅仅不够的,因为多个提供商可以会得出同样的友好命名。所以.NET必须通过一种方式确保程序集的惟一性。COM使用“globally unique identifiers(全局惟一标识符GUIDs)”——惟一的128位号码来指派每一个组件。使用GUIDs十分简单,但有一个致命的缺陷:任何团体都可以看到它,复制,用一个新的使用复制GUIDs的潜在恶意组件替换它。COM的GUIDs可以提供惟一标识,因为在任意同一时刻、任意计算机上都只有一个使用给定GUIDs注册的组件;但是,GUIDs不提供真实性和完整性。

为了兼具惟一性和真实性,.NET共享程序集必须包括它们的创建者和原始内容的惟一证明,这样的证明被称为“strong name(强命名)”。.NET使用一对加密密钥来创建强命名,一个公共密钥和一个私有密钥。除了指定的方式,私有密钥没有什么特别的。对于这对公共密钥和私有密钥十分重要的是,不论使用哪一个密钥加密,又必须用另一个来解密。例如,任何被私有密钥加密的都必须由公共密钥来解密,而不能用自身来解密。

编译期间,编译器使用私有密钥加密程序集的散列值(程序集清单不仅包括程序集的友好命名和版本号,还包括一个组成程序集的模块的散列值)。这样加密的结果是一个惟一的,并同时确保来源和内容的数字签名。随后编译器会追加该签名和公共密钥到程序集清单。每一个客户端又可以访问到公共密钥。另一方面,私有密钥应保持不可访问性。在编译期间,除了版本号之外,客户端引用的强命名程序集会在它的程序集清单中记录一个代表服务端程序集公共密钥的标记:

.assembly extern MyServerAssembly
{
     .publickeytoken = (22 90 49 07 87 53 81 98)
     .ver 1:2:3:4
}

当客户端程序集触发.NET去寻找服务端程序集时,如果一个匹配的程序集被发现,.NET会读取它的公共密钥并计算标记,比较它与客户端所预期的程序集信息。然后,.NET使用公共密钥解密数字签名,把数字签名中程序集的散列值与已发现程序集的散列值相比较。如果两个匹配,那么他就是客户端所期望的程序集。因为私有密钥是惟一的且被创建它的组织保存着,则强命名就确保了没有人可以用同样的数字签名来产生程序集,.NET同时维护了惟一性和真实性。

一个友好命名的程序集可添加任何强命名程序集的引用,如.NET Framework中的程序集。但是,反过来讲就不是正确的了,一个强命名的程序集只能引用其他强命名程序集。如果一个程序集中使用的类型定义来源于其他的友好命名的程序集,那么编译器会拒绝生成和赋值强命名到这样的程序集。原因是显而易见的:一个强命名意指客户端可以相信服务提供者的程序集的真实性和完整性,并且可以获得正确的版本。而如果强命名程序集可以使用其他的潜在的来历不明和未经核实的版本的程序集,那么信任的循环就会被破坏了。因此,强命名程序集只能引用其他强命名程序集。

一、程序集签名

可以在Visual Studio 2005创建加密密钥来签名程序集。在项目属性中选择“签名”面板,选中“为程序集签名”选择框,可使用现有的或新建的密钥文件来签名程序集:

version_step2

 version_step1

强命名密钥文件有两种格式:明码和密码保护。如果创建密钥文件时没有选中“使用密码保护密钥文件”,则会生成一个扩展名为snk的文件。但以这样原始的格式保存密钥文件会存在一个巨大的隐患:任何得到泄漏私有密钥的恶意组织都可以冒充组件提供者来产生组件。因此最好使用密码保护密钥文件,指定密码后Visual studio会生成一个扩展名为pfx的文件,当另一个用户第一次使用该文件时,就必须使用密码验证。提供了正确的密码后,Visual Studio就会从pfx文件中抽取出密钥并存储在证书容器中,这样该用户当第二次使用时就不再需要提供密码了。

选择现有的强命名密钥文件

当选择现有的snk文件或pfx文件时,Visual Studio会自动复制该文件到项目文件夹中。没有共享该类文件的方式,每个项目都必须有该文件独有的物理副本。

处理组织中的密钥

一个组织的强命名私有密钥应当是妥善保存的。如果私有密钥一旦泄漏,较低信誉的组织就可以伪造和分发该组织的组件。那么安全性和信誉(也是潜在法律责任)就会受到影响。因此,对于组织中私有密钥的访问必须受到严格限制,最好只提供给相应的开发团队(同时保留副本)。但是,这样会带来一些问题:如何在没有私有密钥的情况下执行中间的内部生成?如何能让客户端程序集引用该程序集?为了解决这些问题,.NET提供了“delay signing(延迟签名)”。当在签名面板中选中“仅延迟签名”时,编译器会在程序集清单中嵌入公共密钥,但不会生成数字签名。这样就允许在GAC中安装程序集并使客户端程序集可以引用它,可以执行内部生成和测试循环。只不过延迟签名的程序集不能运行和调试,因为在读取期间强命名验证进程会失败。

为了延迟签名,可以使用SN.exe实用工具的-p命令行开关来从包含公共和私有密钥的文件中抽取公共密钥到一个单独文件中:

     SN.exe  -p  MyKeys.pfx  MyPublickey.pfx

这样就可以在组织内部自由分发公共密钥了。为了运行延时签名的程序集,需要使用-Vr命令行开关关闭程序集的数字签名验证:

     SN.exe  -Vr  MyAssembly.dll

在发布程序集之前仍旧需要使用实际的私有密钥签名程序集。可以取消选中“仅延迟签名”选项并提供完整的密钥文件,也可以使用SN.exe实用工具-R命令行开关重新签名(包含公共密钥和私有密钥的密钥文件):

     SN.exe  -R  MyAssembly.dll  MyKeys.pfx

二、强命名与私有程序集

.NET辨别强命名私有程序集和普通私有程序集的方法就是看是否只有友好命名。如果一个私有程序集只包含友好命名,.NET就会在客户端程序集清单中记录该私有程序集的版本:

.assembly extern MyServerAssembly
{
     .ver 1:2:3:4
}

但是,事实上.NET会忽略客户端应用程序和私有程序集之间的版本不兼容性,即使在客户端程序集清单中版本号是可用的。如果应用程序引用一个具有友好命名的私有程序集,那么它将总是会被使用的,而.NET不会在GAC中查找。例如,一个具有非强命名私有程序集(版本号为1.0.0.0)的客户端应用程序编译部署后,现在要升级为2.0.0.0,复制2.0.0.0版本到应用程序文件夹,它就会覆盖1.0.0.0,在运行时.NET就会读取到新版本了。如果新版本是向后兼容的,应用程序就会工作的很好,但是如果2.0.0.0不兼容1.0.0.0,就会导致混乱。与“DLL地狱”不同的是,这样的问题不会影响到其他拥有不同版本程序集私有副本的应用程序。.NET的这种行为,是因为它假设客户端应用程序的管理员懂得版本、兼容性和判断是否值得冒险覆写新版本。

另一方面,如果私有程序集包含一个强命名,则.NET就会强制实施版本兼容策略。.NET在客户端程序集清单中记录请求的私有程序集公共密钥标记,并坚持版本匹配。回到刚才讨论的例子,在这种情况下,程序集解析器会在GAC中查找程序集的可兼容版本,如果在GAC查找不到就会抛出异常,因为私有版本的2.0.0.0被认为是不兼容的。由此得到以下重要结论:

1.只具有友好命名的私有程序集必须向后兼容。

2.具有强命名的私有程序集则不需要向后兼容,因为GAC可以包含一个较老的可兼容版本。

3.即使具有强命名的私有程序集向后兼容,如果版本号不兼容会导致异常(除非GAC中包含一个较老的可兼容版本)。

4.私有程序集部署模型只应用于友好命名的程序集。

三、友好程序集与强命名

使用InternalsVisibleTo特性可以标明一个友好客户端程序集。友好程序集可以访问所以服务端程序集内部的类型和成员。但是,不具有强命名的服务端程序集只可以标明非强命名的客户端程序集:

      [assembly: InternalsVisibleTo("MyClient")]

但这样明显是以一种不安全可靠的方式来暴露程序集的内部类型,因为所以第三方程序集都可以通过改写程序集友好命名的方式来访问服务端程序集内部。InternalsVisibleTo特性的使用应当限制与服务端程序集结合的程序集的开发。为了提供更高级的安全性给InternalsVisibleTo特性,具有强命名的服务端程序集只能标明强命名的客户端程序集为友好程序集。这样就必须在InternalsVisibleTo特性中同时标明客户端程序集命名和强命名标记:

      [assembly: InternalsVisibleTo("MyClient",PublicKeyToken=745901a54f88909b)]

这样就指明了只有具有匹配友好命名和强命名的客户端程序集才可以授权访问该服务端程序集内部,并且客户端编译器会确保这个限定。

四、安装共享程序集

一旦为程序集指定了一个强命名,就可以把它安装到GAC了。GAC位于在Windows文件夹下一个被称为assembly的特殊文件夹下,有多种方式来查看和操作GAC。.NET会安装一个Windows Shell扩展来使用文件浏览器显示GAC中的程序集,可以导航到GAC并拖拽共享程序集到GAC中,同样也可以通过文件浏览器从GAC中移除程序集。第二种操作方式是命令提示符实用工具GACUtil,通常会在应用程序安装程序中使用它。第三中管理GAC的方式是使用一个称为“.NET配置工具”的专门的管理工具,该工具是一个微软管理控制台(MMC)单元:

version_step3

点击“将程序集添加到应用程序缓存”可以选择共享程序集添加到GAC中。注意,只有系统管理员组的成员才能够在GAC中添加或移除程序集。点击“查看程序集缓存中的程序集列表”,会在配置工具中展示出GAC中的所有共享程序集的视图,包括每个程序集的友好命名,版本号,区域和公共密钥标记:

version_step4

验证共享程序集模式

为了某些开发和调试的目的,可以通过编程的方式验证服务端程序集是否是作为共享程序集使用。可以利用程序集类型的GlobalAssemblyCache布尔属性来处理。当程序集是从GAC读取的时候,GlobalAssemblyCache会被设为true。也可以使用Location属性来告知用户程序集实际是从哪读取的。下面是AssertSharedAssembly辅助方法的实现和使用——验证一个给定的程序集是否是从GAC中读取。如果不是,该方法会提示用户,并指出程序集实际读取的位置:

using System.Reflection;

static void AssertSharedAssembly(Assembly assembly)
{
    bool shared = assembly.GlobalAssemblyCache;
    Debug.Assert(shared);
    if (shared == false)
    {
        string message = @"The assembly should not be used as a shared assembly.
                         It was loaded instead from:"
;
        string currentDir = assembly.Location;
        MessageBox.Show(message + currentDir);
    }
}

Assembly assembly = Assembly.GetExecutingAssembly();
AssertSharedAssembly(assembly);

注意,编写依赖于程序集部署模式或位置的代码是错误的。例如AssertSharedAssembly()的方法应当只在开发期间用来检测错误。

同步执行

GAC可以存储一个具有相同命名程序集的多个版本。这是因为虽然GAC使用了Windows文件系统,但它不是平面结构。当每个程序集添加到GAC是,.NET会创建一组路径由程序集名称,版本号和程序集的公共密钥标记组成的文件夹,例如:

     C:\WINDOWS\assembly\GAC_MSIL\MySharedAssembly\1.0.0.0__745901a54f88909b\MySharedAssembly.dll

因此,同一程序集的两个不同版本会被放入两个不同的文件夹中。而每个引用程序集的客户端程序集清单中都包括程序集的友好命名,版本号和公共密钥标记等信息,这就足够使程序集解析器定位GAC中正确的程序集并读取它。这样,就确保了同步执行。

 

根据原版英文翻译,所以不足和错误之处请大家不吝指正,谢谢:)