加密/解密内部和外部节
除了 web.config 内部节的加密/解密,该工具还展示了如何加密/解密配置文件中的外部节。
引言
最近,有人要求我加密 web.config 文件中的几个节。但我们的配置驻留在由 configSource
属性引用的外部配置文件中的节里。
网上有很多关于如何使用工具或以编程方式加密或解密某个节的帮助,但所有这些方法本质上都相同,它们只是在不产生任何错误消息的情况下就无法处理外部文件。
背景
在我参与项目之前,所有配置都存储在 AppSettings
和 ConnectionStrings
节中,或者存储在自定义节中,始终在 web.config 文件里。为了便于维护和配置,在我参与时,我决定将上述所有配置值拆分并组织到各种节中,这些节要么是自定义的,要么基于 NameValue
模板。
我另外决定,这些节应该实现为外部文件,并由 web.config 文件引用。我不认为有必要解释上述差异的好处,但对我来说,为创建自动化部署过程增加的灵活性总是好的。
我一直喜欢核心开发,所以我创建的所有内容通常都位于提供清洁、开箱即用功能的框架项目中,供同一组织中的其他开发人员使用。由于框架代码已经增长了很多,我创建了一个精简版项目,其中只包含本文所需的源代码文件。我之所以这么说,是因为该项目的复杂性可能显得有些奇怪,但请记住,其中一些部分用于此处未显示的其他功能。
通过谷歌搜索,可以轻松找到关于加密或解密配置文件工具或 API 的帮助。
关于代码的几句话
负责实际加密和解密的代码位于 BaseConfigEncryption
中,该类有望能够管理 web.config 文件或 app.config 文件。目前,只支持 web.config 文件,其功能由其子类 WebConfigEncryption
提供。
EncryptionStartParameters
是一个泛型基类,它继承了处理 main
函数的 string
[]
args 参数的功能。基本上,想法是通过命令行参数列表创建一个期望的类型化实例。当进程作为智能客户端执行时,它还处理参数。
要加密或解密一个配置,您需要应用程序的路径(该应用程序包含目标配置文件)、加密提供程序(例如 DataProtectionConfigurationProvider
)以及我们希望加密的节。解密也需要这些,但不需要提供程序。
参数示例
参数的示例是
- 用于加密
-e -p="D:\Web Sites\CCMS_EFG_Prod\telamon-CCMS" -sp=DataProtectionConfigurationProvider -s=DatabaseSection
- 用于解密
-d -p="D:\Web Sites\CCMS_EFG_Prod\telamon-CCMS" -s=DatabaseSection
请记住,命令行参数处理程序的设计不是为了像各种 DOS 命令和其他工具那样处理参数,但这在这里不是重点。
实现
BaseConfigEncryption
在其他一些辅助函数的帮助下完成了所有工作。
首先,我们必须加载配置文件。这有点棘手,但可重写的 GetConfiguration
函数可以完成这项工作,返回一个 Configuration
对象,因此基类不需要知道它正在处理 web 配置还是普通应用程序配置。
protected override System.Configuration.Configuration GetConfiguration()
{
WebConfigurationFileMap wcfm = new WebConfigurationFileMap();
wcfm.VirtualDirectories.Add("/",
new VirtualDirectoryMapping(base.RootPath, true, "web.config"));
return WebConfigurationManager.OpenMappedWebConfiguration(wcfm, "/");
}
此时,配置已加载,我们必须对其进行加密或解密。
想法是这样的。无论我们是加密还是解密,对于每个必需的节,我们都会检查该节是实现为根配置文件(如 web.config)中的,还是实现为外部文件中的。
foreach (string sectionName in sections)
{
ConfigurationSection section = configuration.GetSection(sectionName);
if (!String.IsNullOrEmpty(section.SectionInformation.ConfigSource))
{
this.sectionsWithExternalFiles.Add(sectionName);
}
}
因此,首要任务是找到所有实现为外部文件的节,并将它们填充到 sectionsWithExternalFiles
中。对于这些节中的每一个,我的想法是将实现从外部文件复制到根配置文件中。这是通过 MoveSectionsFromExternalFile
函数完成的,该函数迭代 MoveSectionFromExternalFile
。
private void MoveSectionsFromExternalFile()
{
if (this.sectionsWithExternalFiles.Count == 0)
{
return;
}
XDocument xDocument = XDocument.Load(ConfigFilePath);
XElement xRoot = xDocument.Root;
string nameSpace = xDocument.Root.GetDefaultNamespace().NamespaceName;
foreach (string sectionName in this.sectionsWithExternalFiles)
{
MoveSectionFromExternalFile(xDocument.Root.Element
(XName.Get(sectionName, nameSpace)), sectionName);
}
xDocument.Save(ConfigFilePath);
ClearXmlNS(ConfigFilePath);
}
private void MoveSectionFromExternalFile(XElement xSection,string sectionName)
XAttribute xConfigSource=xSection.Attribute("configSource");
string configFile = xConfigSource.Value;
this.externalFiles.Add(sectionName,configFile);
XDocument xConfigFile = XDocument.Load(Path.Combine(RootPath, configFile));
xConfigSource.Remove();
xSection.Add(xConfigFile.Root.Attributes());
xSection.Add(xConfigFile.Root.Elements());
ClearXmlNS(xSection);
}
在上述过程中,externalFiles
被填充了节名称和文件名对,稍后将用于相同的反向过程。
此时,我们有了一个 web.config 文件,该文件已与所有必需的外部文件合并,因此唯一要做的就是使用 API(通过搜索引擎找到的)进行操作。
对于每个节,我们检查它是否已被加密,并根据我们是加密还是解密,调用相应的命令。
foreach (string sectionName in sections)
{
ConfigurationSection section = configuration.GetSection(sectionName);
switch (String.IsNullOrEmpty(protectionProvider))
{
case false:
if (!section.SectionInformation.IsProtected)
{
section.SectionInformation.ProtectSection(protectionProvider);
}
break;
case true:
if (section.SectionInformation.IsProtected)
{
section.SectionInformation.UnprotectSection();
}
break;
}
}
之后,我们只需将节信息从根配置文件移动到外部文件,并为这些节重新应用 configSource
属性。要做到这一点,我们调用 MoveSectionsToExternalFile
,它会为每个节迭代 MoveSectionToExternalFile
。
private void MoveSectionsToExternalFile()
{
if (this.sectionsWithExternalFiles.Count == 0)
{
return;
}
XDocument xDocument = XDocument.Load(ConfigFilePath);
XElement xRoot = xDocument.Root;
string nameSpace = xDocument.Root.GetDefaultNamespace().NamespaceName;
foreach (string sectionName in this.sectionsWithExternalFiles)
{
MoveSectionToExternalFile(xDocument.Root.Element
(XName.Get(sectionName, nameSpace)), sectionName);
}
xDocument.Save(ConfigFilePath);
ClearXmlNS(ConfigFilePath);
}
private void MoveSectionToExternalFile(XElement xSection,string sectionName)
{
string configFile = this.externalFiles[sectionName];
XDocument xConfigFile = XDocument.Load(Path.Combine(RootPath, configFile));
xConfigFile.Root.RemoveAll();
xConfigFile.Root.Add(xSection.Attributes());
xConfigFile.Root.Add(xSection.Elements());
ClearXmlNS(xConfigFile.Root);
string configFilePath = Path.Combine(RootPath, configFile);
xConfigFile.SaveWithoutHeader(configFilePath);
ClearXmlNS(configFilePath);
xSection.RemoveAll();
xSection.Add(new XAttribute(XName.Get("configSource"), configFile));
}
所有节的迁移都借助 LINQ TO XML 完成,这比以前访问 XML 文档的机制要好得多。我遇到的唯一麻烦是编辑过的 XML 中嵌入的各种节点的命名空间。我认为这与原始配置文件中的命名空间声明有关。
经过一番搜索,我通过告诉每个元素其命名空间名称为空来解决了这个问题,这导致了在保存的文件中出现 xmlns=""
。然后我加载文件,将这些文字替换为空,一切都恢复正常。执行这些清理工作的函数是
private void ClearXmlNS(XElement xElement)
{
foreach (XElement e in xElement.DescendantsAndSelf())
{
if (e.Name.Namespace != XNamespace.None)
{
e.Name = XNamespace.None.GetName(e.Name.LocalName);
}
}
}
private void ClearXmlNS(string filePath)
{
string text = File.ReadAllText(filePath);
text = text.Replace("xmlns=\"\"", "");
File.WriteAllText(filePath,text);
}
如果您不进行清理,那么命名空间就会被加密,而 System.Configuration
的透明解密会抛出异常。即使是空白命名空间名称也会如此。我知道这可能不是最好的方法,但以我有限的时间我找不到更好的方法。
关注点
在此工具的开发过程中,做了一些假设。
- 由于我们根据节名称对文件名有一个特定的命名模式,如果该模式没有被遵循,可能会出现问题。我认为这无关紧要,但因为我在创建工具时考虑到了这一点,所以最好让您知道。
- 不支持自定义加密提供程序,因为我对它们不感兴趣。我创建这个工具完全基于我们项目的需求。将来如果出现需求,可能会添加。
- 该工具仅针对
DataProtectionConfigurationProvider
提供程序进行了测试。 - 该工具有一个
/?
参数,会产生帮助信息。如果参数无效,也会产生相同的帮助信息。
历史
- 版本1
- 文章创建于 2010 年 3 月 5 日