在学习程序集时,总是发现程序集被冠以各种头衔。程序集按文件数量可分为:单文件程序集和多文件程序集;按是否签名,可分为:普通程序集和强命名程序集;按部署方式,可分为:私有程序集和共享程序集。下面开始分别介绍。
程序集可以由多个模块组成。在大多数情况下,程序集只由一个模块构成。这种情况下,程序集就一个文件,因此被称为单文件程序集。多文件程序集是一组文件集合,包括主模块、辅助模块以及资源。大多数程序集是单文件程序集。多文件程序集的存在主要用来支持两个情况:第一种情况是用现购现付的方法实现程序集的下载,以便客户端下载一个程序集时,通过垂滴方式可以下载所需的模块;第二种情况是实现“本地化”。下图显示了单文件程序集于多文件程序集的构成:
多文件程序集的特点如下:
- 可用单独的文件对程序集进行划分,允许文件“增量”方式下载。
- 可以使某个文件成为程序集的一部分,如Excel文件.。
- 程序集的各个模块可以用不同编程语言实现。
多文件程序集包括主模块、辅助模块以及资源。主模块包含了程序集的清单。辅助模块的文件扩展名多为.netmodule(CLR没有强制要求),它也包含IL和元数据,同时还有一个模块级别的清单,该清单只记录该模块外部引用的程序集。资源中则可能包含图标或本地化资源。组成一个多文件程序集的模块并没有相互连接成一个大文件,相反,它们只是依靠在主模块中记录的信息逻辑地联系在一起。
一旦CLR加载包含了清单的那个文件,就可以确定在程序集的其它文件中,具体是哪一些文件包含应用程序引用的类型和资源。每次加载一个程序集CLR和OS都要花费一定时间来查找、加载并初始化程序集。所以,需要加载的文件数量越少,性能越好。
VS没有提供多文件程序集模板,我们需要使用命令行创建多文件程序集,具体用法请参考MSDN的 csc.exe 手册。
VS支持引用多文件程序集,只需要引用主模块,VS会自动复制相关的模块到项目。在使用多文件程序集中的类型时,会按需加载模块。
我们可以以各种方式组合多文件程序集,但从最佳实践角度讲,应该遵循以下的组合规则:
- 总是在独立的程序集中存储特定的地区设定资源,而不是作为程序集中的一个嵌入的资源使用它们。
- 避免带有不包含IL模块的多文件类型程序集。
- 最小化应用程序集的代码。关注可视化布局,而将业务逻辑封装在其他类库程序集中。
- 确保一个类库的所有组件有同样的生命周期,并且总是有相同的版本号和安全凭证。如果你预见到可能有偏差,则可将程序集分割成两个类库。
CLR支持“普通程序集”和“强名称程序集”,两者在结构上完全一致,其区别在于,强名称程序集使用发布者的公钥/私钥对进行了签名,它唯一地标识了程序集的发布者。这一对密钥允许对程序集进行唯一的标识、保护和版本控制。由于程序集被唯一性地标识,所以当一个应用程序试图绑定到一个强名称程序集时,CLR可以应用一些已知安全的策略。(建议使用强名称程序集,因为:第一,不同的普通程序集可能具有相同的名称,会造成混淆;第二,如果 CLR 能唯一地标识了一个程序集,就可以应用更多的版本控制和向后兼容策略。)
(一) 为普通程序集分配强名称
一个强名称程序集的唯一标识由四部分组成:
- 文件名(不计扩展名)
- 版本号([AssemblyVersion] 特性)
- 语言文化标识([AssemblyCulture] 特性)
- 公钥([AssemblyKeyFile] 特性:显式指定时,VS会生成警告来通知你使用正规途径建立密钥文件。)
下图显示了一个普通程序集的这四部分的值:
.NET使用标准的公钥/私钥加密技术(唯一且抗篡改),不仅能在程序集安装到一台机器上时检查其二进制数据的完整性,还允许每个发布者能授予一套不同的权限。因为公钥/私钥对不会重复,所以即使程序集具有相同名称、版本、文化也不会造成冲突。
1 创建密钥
使用VS创建密钥:
当勾选使用密码保护时,生成.pfx的密钥文件(用密码保护的密钥文件);而不使用密码保护时生成.snk的密钥文件,文件中都包含了二进制形式的公钥/私钥对。
2 签名
编译时,编译器将基于公钥和私钥数据生成的数字签名,并把它嵌入到程序集中,如下图所示:
签名的确切含义是:生成一个强名称程序集时,程序集的FileDef清单元数据表列出了构成程序集的所有文件,每次将一个文件的名称添加到清单中,文件的内容都会进行哈希处理,得到的哈希值会和文件名一道存储在FlieDef表中。(可以在代码中使用[assembly:AssemblyAlgorithmId]特性来改变默认的SHA-1算法。)
编译器知道密钥文件的位置后,就会在编译过程中使用.publickey标记把密钥值记录到程序集清单中。生成了包含清单的PE文件后,会对PE文件的完整内容进行哈希处理(生成一个基于整个程序集的散列值,只要程序集内容有一丁点的修改,这个散列值都会改变),此时使用的算法始终是SHA-1,而且不能改变。这个哈希值使用发布者的私钥进行签名(散列值结合私钥组成数字签名),最终得到的RSA数字签名会存储到PE文件的一个保留区域中(进行哈希处理时会忽略这个区域)。PE文件的CLR头会进行更新,反映出数字签名在文件中的嵌入位置(数字签名被嵌入到了CLR头)。发布者公钥也嵌入这个PE文件的AssemblyDef清单元数据表中。文件名、程序集版本号、语言文化、公钥的组合为这个程序集赋予了一个强名称,它保证了唯一性。
公钥会写入程序集清单,签名可以使用公钥来验证,私钥不存储在程序集中,这样就可以确保除了私钥的主人外,没有人可以修改该程序集。
3 查看结果
再次查看构成程序集信息:
程序集清单:
从上图可看出2个公钥不太像。这是因为,公钥太大了,为了简化,设计了公钥标记(public key tocken)。公钥标记是公钥的64位哈希值(最后8个字节)。AssemblyRef存储的实际上是简化了的公钥标记而不是公钥。我们看到的虽然是公钥标记,但是,CLR在做出安全或信任决策时,永远都不会使用公钥标记,因为不同公钥可能会得到相同的公钥标记。
查看AssemblyRef:
查看AssemblyDef:
(二) 延迟签名
延迟签名也称部分签名。延迟签名允许你只用公钥来生成一个程序集。在全局程序集属性AssemblyDelaySign设置为true时,签名就不会存储在程序集中,但保留了足够的空间,以便以后添加,但是,不使用密钥,就不能测试程序集,在全局程序集缓存(GAC)中安装它,也无法实现防篡改保护。如果为了保护组织私钥的安全,可以使用临时密钥进行测试,以后再用真正的密钥代替这个临时密钥。延迟签名的更多用处在于配合混淆器使用。程序集在完全签名之后,不能再对它运行混淆器,否则哈希值就不正确了。所以,想混淆一个程序集文件,或者进行其它形式的“生成后”操作,就应该使用延迟签名。
在VS中只要勾选“仅延迟签名”即可:
如上图所示,延迟签名后该项目将不会运行,也不能进行调试。可以使用sn.exe,跳过验证过程,操作流程如下:
(1) 创建密钥:sn -k key.snk
(2) 在VS中勾选“仅延迟签名”并编译程序集,或者执行命令行:csc /keyfile:key.PublicKey /delaysign myCustom.cs
(3) 关闭签名的验证功能,以便安装到GAC:sn -Vr myCustom.dll
(4) 部署到GAC
(5) 发布前,用私钥重新签名:sn -R myCustom.dll key.PrivateKey
(6) 重新启用对程序集的验证:sn -Vu myCustom.dll
延迟签名只能在开发过程中关闭,不经过验证是不能进行发布程序的,因为这个程序集可能被怀有恶意的程序集替代。
(三) 防篡改
完全的强名称程序集可以防篡改。强签名程序集可以私有部署,也可以共享部署,两种部署方式都提供了检查机制来防止篡改。
1 共享部署的检查机制
将一个强名称程序集安装到GAC时,系统会执行一次检查,核对含有清单的那个文件没有被篡改,系统对包含清单的文件的内容进行哈希处理,并将哈希值与PE文件中嵌入的RSA数字签名进行比较(在公钥解除了对它的签名之后)。如果两个值完全一致,表明文件的内容未被篡改,可保证你拿到的公钥与发布者的私钥是完全对应的。除此之外,系统还会对程序集的其它文件的内容进行哈希处理,并将哈希值与清单文件的FileDef表中存储的哈希值进行比较。任何一个哈希值不匹配,表明程序集至少有一个文件被篡改,程序集将无法安装到GAC。这个检查只在安装时执行一次。除此之外,为了增强性能,如果程序集被完全信任,并加载到一个完全信任的AppDomain,CLR不会检查强名称的程序集是否被篡改。
应用程序需要绑定一个程序集时,CLR根据所引用程序集的属性(名称、版本、语言文化、公钥)在GAC中定位程序集。如果能够找到引用的程序集,就返回包含它的那个子目录,并加载容纳清单的文件。以这种方式查找程序集,可以保证运行时加载的程序集和最初编译时所生成的程序集来自同一个发布者。之所以能做出这样的保证,是因为进行引用的程序集的AssemblyRef表中的公钥标记与被引用的程序集的AssemblyDef表中的公钥是匹配的。如果被引用的程序集不在GAC中,CLR会查找应用程序的基目录,然后查找应用程序配置文件中标注的任何私有路径。然后,如果应用程序是用MSI安装的,CLR会要求MSI定位程序集。如果在任何位置都找不到程序集,绑定操作会失败,并抛出FileNotFoundException异常。
2 私有部署的检查机制
如果程序集是从GAC之外的位置加载的,CLR会在程序集加载时比较哈希值,应用程序每次执行并加载程序集时,都会校验程序集的清单,并对程序集文件进行一次哈希处理,以牺牲一定的性能为代价,保证程序集文件的内容没有被篡改。CLR在运行时检测到不匹配的哈希值,会跑出FileLoadException异常。
程序集可以采用两种方式部署:私有或共享。私有程序集部署到应用程序的目录中。普通程序集只能以私有方式部署。共享程序集是指部署到一次已知位置的程序集。强名称程序集既可以部署为私有,也可以部署为共享。
(一) 私有程序集
私有程序集要求放置在客户端应用程序目录或者其子目录下。
1 私有程序集标识
私有程序集的全名称包括友好名称和数字版本号,两者都被记录在程序集的清单中,友好名称就是模块(包含程序集清单的)名字减去文件扩展名。鉴于私有程序集的独立性,CLR在解析它的位置时并不忙于使用它的版本号。
2 探测过程
.NET使用一种“探测”技术解析私有程序集的位置。探测是一种把外部程序集请求映射到被请求的二进制文件位置的过程。一个加载请求可以是显式的或者隐式的。隐式的加载请求发送在CLR查询清单的.assembly extern标记来解析程序集的位置的时候;显式加载请求发送在以编程方式调用System.Reflection.Assembly的Load()或者LoadFrom()方法时。
不管隐式还是显式,CLR获得程序集的友好名称后,便开始探测应用程序目录下的程序集文件(*.dll)。如果找不到,CLR会尝试查找该目录下具有程序集友好名称的子目录。如果找不到文件,它就尝试去查找具有相同名称的可执行程序集(*.exe),在次将之前的目录再查找一遍。如果还是找不到,CLR会抛出FileNotFound异常。CLR只会加载探测过程中第一个找到的程序集。附属程序集遵循类似的规则,只是CLR会在应用程序基目录下的一个子目录中查找它,子目录的名称与语言文化的名称相符。
通过使用配置文件可以影响CLR的探测过程。配置文件必须拥有和应用程序相同的名称,扩展名为.config,并位于与程序集相同的目录。如果想让CLR探测程序的MyCustom子目录,可以如下设置配置文件:
如果我们程序集应用了“en-US”语言文化,和上例中的配置文件,那么CLR查找程序集do.dll和附属程序集language.dll的探测顺序如下:
为了节省探测过程时间,在配置文件中,最好指定一个或者多个culture元素,对CLR查找附属程序集的探测过程进行限制。
3 私有部署强名称程序集
可以通过配置文件将强名称程序集部署到一个已知路径下面,如下图所示:
(二) 共享程序集
共享程序集的一个副本可以供一台机器上的多个应用程序使用,所以,如果要创建一个机器级别的类库,那么需要把它部署为共享程序集。共享程序集安装在全局程序集缓存(GAC)中。GAC在Windows目录下的名为Assembly的目录。只有*.dll文件且程序集是强名称的才可以部署为共享程序集。
1 GAC
默认情况下GAC只能由系统管理员权限的用户操作。GAC目录是结构化的,其中包含很多子目录,并用一个算法来生成这些子目录的名称。因为不清楚GAC的内部结构,所以,安装程序集时不能手动复制程序到GAC目录,必须通过拖拽或使用工具来完成这项任务,以便生成正确的子目录。
2 在GAC中安装和移除共享程序集
安装的最简单方法就打开Windows资源管理器,把程序集直接拖到GAC中(不要复制、黏贴),以便本地测试使用。
最好使用gacutil.exe来管理GAC,运行时会自动显示帮助:
3 使用共享程序集
在VS的添加共享程序集的引用时,不能通过浏览GAC添加,只能在GAC外部其它目录下引用程序集。因为共享程序集是强名称的,VS会假定这个程序集已经部署到GAC,因此,在调试和发布时,不会对该程序集进行复制。可以在下图所示选项中进行修改:
4 动态重定向到共享程序集的特定版本
在配置文件中进行如下设置可以使CLR加载一个不同于程序清单中的特定版本的共享程序集:
5 发行者策略程序集
配置文件可以忽略清单中的版本,使客户端绑定指定版本的程序集。如果使用这种方法,我们必须保证每个客户端上的配置文件都必须被正确配置,显然每次修改起来很麻烦。发行者策略允许程序集的发行者在安装程序集到GAC的同时,把配置文件的二进制版本(作为程序集看待,发行者策略程序集)也安装带GAC中。这样,客户端就不需要配置文件了。CLR会读取当前客户端程序的清单,尝试在GAC中查找被请求的版本。如果CLR找到一个发行者策略程序集,它会读取其中的配置信息,在GAC级别执行请求重定向。
创建发行者策略的步骤如下:
(1) 创建发行者策略文件
发行者策略文件是一个把现有版本或某个版本范围重定向到新版本的XML文件,如前面”动态重定向到共享程序集的特定版本”的配置文件就可以作为发行者策略文件。
(2) 创建发行者策略程序集
(3) 将发行者程序集添加到GAC
将发行者策略添加到GAC之后,可以删除客户端配置文件,发行者策略将会生效。
通过配置文件可以关闭发行者策略: