65.9K
CodeProject 正在变化。 阅读更多。
Home

使用证书进行 WCF 安全性的简便方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (37投票s)

2007年4月30日

MIT

12分钟阅读

viewsIcon

491031

downloadIcon

7232

一种使用证书在 Windows Communication Foundation 中加载从文件加载证书的简便解决方案

引言

安全性在任何分布式应用中都扮演着重要角色,而 Windows Communication Foundation(简称 WCF 或 Indigo)这个新的 Microsoft 通信框架实现了许多安全标准,并提供了广泛的功能。

安全最重要的方面之一是身份验证。WCF 可以配置为使用多种身份验证方法:

  • 匿名调用者
  • 用户名和密码
  • 证书
  • Windows
  • CardSpace

在本文中,我将向您展示如何配置 WCF 使用证书通过另一种方法来验证服务客户端和服务器。

如果您想完全理解我的实现,请继续阅读下一节。如果您只想了解如何使用证书配置 WCF,请直接跳转到“快速入门教程”部分。

背景概念

接下来的几节假设您熟悉许多 WCF 和安全概念。如果您想回顾其中一些概念或获取更多信息,请参阅“外部资源”部分。

问题所在

使用证书进行身份验证并非新方法,但它仍然是验证主体的最常用方法之一。WCF 内置支持符合 Web Services Security (WS-Security) 标准的证书。

默认配置和可用示例存在的问题是,所有证书都必须安装在证书存储中,证书存储基本上是 Windows 保存所有证书(也用于其他应用程序:Internet Explorer 等)的中心位置。

为什么此解决方案会导致一些问题?简单来说,是因为正确配置所有证书并不容易。有关更多详细信息

  • 当您将服务部署到服务器时,必须在证书存储中安装所有使用的证书(根据证书的用途安装在不同的位置)。
    此操作必须使用安装程序、脚本文件或批处理文件执行。因此,使用 xcopy/ClickOnce 安装部署应用程序很困难。
  • 每个客户端也必须安装用于自身身份验证的证书,同样要安装在证书存储中。如果您客户端数量很少,这很简单,但如果您必须手动配置每台计算机,则非常困难(此外,对于客户端,您不能使用 xcopy/ClickOnce 安装)。
  • 您必须授予正在运行的进程(如 ASP.NET)读取证书私钥的权限。此步骤通常需要更改文件系统权限。这再次需要脚本文件或安装,这并不总是很容易。
  • 如果您使用的是共享主机,您可能无法安装证书或更改证书权限。
  • 作为开发人员,我希望每个项目都能与其他项目隔离。我希望能够轻松测试不同的配置或应用程序,我希望能够直接从代码存储库下载最新版本并运行它,而无需任何特殊配置。使用证书存储,我每次都必须记住安装或卸载证书。

在以下 MSDN 页面上,您可以看到一个使用证书的配置示例,以及关于如何使用经典解决方案安装证书的说明:MSDN:消息安全证书

这些是我决定尝试不同方法的原因,我将在接下来的几节中进行描述。

解决方案

我的目标是找到一种不使用证书存储的简便方法来使用证书。WCF 可以轻松扩展;在本文中,我将向您展示如何扩展 WCF 以从文件中加载证书。

我知道将证书存储在文件系统中安全性较低,但我认为通过一些注意,这可能是一个有用的替代方案。有关我方法的潜在问题讨论,请参阅“缺点”部分。

请注意,通过我的解决方案,我仅改变了证书的加载方式,WCF 的所有优势(标准、经过验证的代码等)仍然有效。最重要的是,您仍然必须使用大多数证书所需的设置。有关完整且可正常工作的示例,我建议查看 zip 文件中的示例项目,或遵循“快速入门教程”部分自行在项目中实现此解决方案。

要理解此解决方案的工作原理,请继续阅读下一节。

实现细节

从文件加载证书非常简单,您只需使用 System.Security.Cryptography.X509Certificates.X509Certificate2 类。

//Load the certificate from a file
X509Certificate2 certificate =
        new X509Certificate2(fullpath, password);    

第一个参数是证书文件的路径,第二个参数是用于加密私钥的密码(如果存在)。

使用 X509Certificate2 类,您可以加载两种文件:

  • .cer 文件 - 用于存储公钥
  • .pfx 文件 - 用于存储公钥+私钥(可选,使用密码加密)

您可以从公共证书颁发机构获取这些文件,或使用 makecert.exepvk2pfx.exe 创建自己的自签名证书,两者都可作为 Visual Studio 工具获得。以下是如何创建证书的示例:

makecert -r -pe -n "CN=CompanyXYZ Server" -b 01/01/2007 -e 01/01/2010
        -sky exchange Server.cer -sv Server.pvk
pvk2pfx.exe -pvk Server.pvk -spc Server.cer -pfx Server.pfx    

第一个命令(makecert)生成公钥(在此示例中为 Server.cer)和私钥(在此示例中为 Server.pvk)。第二个命令(pvk2pfx)将这两个文件合并到一个 .pfx 文件中(在此示例中为 Server.pfx)。

执行 makecertpvk2pfx 时,您可以输入用于加密私钥的密码。

服务身份验证

本节介绍如何配置服务器以使用用于验证服务的证书。

首先,您必须添加对 DevAge.ServiceModel.dll 的引用。此程序集(包含在带有完整代码的示例项目中)包含一些将在稍后介绍的用于从文件加载证书的类。

然后,您必须生成服务证书并将其放在安全目录中。对于 ASP.NET 网站,我认为一个好的解决方案是使用 App_Data 目录,该目录已自动配置为不允许公开访问。我在 App_Data 目录中生成了两个文件:Server.cerServer.pfx

通常,您可以在 .config 文件的 system.serviceModel 部分中像这样指定服务证书:

<behaviors>
  <serviceBehaviors>
    <behavior name="serviceCredentialBehavior">
      <serviceCredentials>
        <serviceCertificate findValue="Contoso.com"
                            storeLocation="LocalMachine"
                            storeName="My"
                            x509FindType="FindBySubjectName" />
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>    

此配置基本上告诉 WCF 在证书存储中查找证书的位置。

要从文件加载证书,很遗憾,您不能简单地更改配置,而必须设置 ServiceHost.Credentials.ServiceCertificate.Certificate 属性。

我创建了一个新的配置节,您可以在其中指定服务器证书的位置:

<devage.serviceModel>
<services>
  <add name="MathService"
        serverCertificate="App_Data\Server.pfx"
                />
</services>
</devage.serviceModel>    

然后,我创建了一个新的 ServiceHost 派生类 DevAge.ComponentModel.CertificateServiceHost,它会自动读取新的配置节并设置 ServiceHost.Credentials.ServiceCertificate.Certificate 属性。

如果您直接创建 ServiceHost 类,您可以简单地更改代码,改为创建 DevAge.ComponentModel.CertificateServiceHost
如果您使用 ASP.NET(它会自动创建 ServiceHost 类),您必须像这样配置 .svc 文件:

<% @ServiceHost Language=C# Debug="true"
        Service="MathService"
        CodeBehind="~/App_Code/MathService.cs"
        Factory="DevAge.ServiceModel.CertificateServiceHostFactory" %>    

注意 Factory 属性,它使用一个特定的工厂类来创建我的 DevAge.ComponentModel.CertificateServiceHost
此配置替换了标准的 serviceCertificate 部分。

注意:当您使用 svcutil 创建代理客户端时,您会在 endpoint 部分中看到一个 identity 部分。

<identity>
  <certificate encodedValue="...." />
</identity>

每次更改服务证书时都必须重新创建此部分,因为它包含在设计时生成的证书的公共标识。客户端使用此值来确保与预期的服务通信。

客户端身份验证

本节介绍如何使用证书来验证每个客户端。

您必须为每个客户端创建一个证书(或为多个客户端共享同一个证书)。您可以使用相同的命令生成自签名证书,或从证书颁发机构获取。

在我的示例中,我在客户端目录中生成了两个文件:Client.cerClient.pfx

通常,每个客户端都可以使用以下配置来配置应用程序:

<behaviors>
  <endpointBehaviors>
    <behavior name="ClientCredentialsBehavior">
      <clientCredentials>
        <clientCertificate findValue="Cohowinery.com"
             storeLocation="CurrentUser"
             storeName="My"
             x509FindType="FindBySubjectName" />
      </clientCredentials>
    </behavior>
  </endpointBehaviors>
</behaviors>    

此配置始终使用证书存储来查找要使用的正确证书,并且与前一个示例一样,没有直接使用证书文件的途径。因此,我再次创建了一个新的配置节来指定证书文件名:

<devage.serviceModel>
<endPoints>
  <add contract="Client.MathService.IMathService"
        clientCertificate="Client.pfx" />
</endPoints>
</devage.serviceModel>    

要手动设置客户端证书,您可以设置 ClientProxy.ClientCredentials.ClientCertificate.Certificate 属性。

不幸的是,在这种情况下,没有办法通过读取新节来自动设置此属性(至少我找不到方法...),因此您必须使用如下代码配置每个代理:

//Create the client proxy as usual
MathService.MathServiceClient service =
         new Client.MathService.MathServiceClient();

//Configure the client reading the devage.serviceModel configuration section
DevAge.ServiceModel.Proxy<MathService.IMathService>.Configure(service);    

DevAge.ServiceModel.Proxy 类读取配置节并设置 ClientProxy.ClientCredentials.ClientCertificate.Certificate 属性。

如果客户端使用的证书受服务器信任(通常必须将其添加到证书存储的“受信任人员”中),则您的配置就完成了。如果证书是自签名的或无论如何不受服务器信任,则必须配置服务器以接受该证书。

为了解决此问题,我实现了一个自定义的 X509CertificateValidatorDevAge.ServiceModel.CustomCertificateValidator,该类可以为每个服务进行配置。您必须扩展服务(服务器)的 .config 文件的 devage.serviceModel 部分,如下所示:

<devage.serviceModel>
<services>
  <add name="MathService"
        serverCertificate="App_Data\Server.pfx"
        clientCertificates="App_Data\Client1.cer,App_Data\Client2.cer"
                />
</services>
</devage.serviceModel>    

注意新的 clientCertificates 属性,其中包含一个要视为受信任的客户端证书列表。

您必须将所有客户端公钥文件(在此示例中为 Client1.cerClient2.cer 文件)复制到服务器的 App_Data 目录中。

CertificateServiceHost 类会自动读取此新节,并使用如下代码设置 X509CertificateValidator

X509ClientCertificateAuthentication authentication =
            serviceHost.Credentials.ClientCertificate.Authentication;

authentication.CertificateValidationMode =
        System.ServiceModel.Security.X509CertificateValidationMode.Custom;

authentication.CustomCertificateValidator =
    new CustomCertificateValidator(clientCertificates);    

如果证书文件有密码,您可以在文件名中使用此格式指定:filename|password。例如,您可以使用以下值指定 web.configserverCertificate 属性:App_Data\Server.pfx|yourpassword
同时请记住,您可以为 *.pvk*.pfx 文件指定两个不同的密码,使用的密码是为 *.pfx 文件指定的密码。有关如何设置密码的更多详细信息,请参阅 makecertpvk2pfx 文档。

缺点

将证书存储在文件中更简单,但安全性较低,因为恶意用户可能会窃取私钥。公钥可以毫无问题地共享,但如果有人窃取了私钥,您的安全就会受到威胁。使用私钥,恶意用户可以解密消息,或者冒充您的服务或客户端。

但我认为,通常情况下,如果有人可以访问您的文件系统,您可能还有许多其他安全问题。还要考虑,如果有人访问您的文件系统,他们很可能(取决于攻击类型)也能访问证书存储。

我的建议是正确评估您的应用程序和安全需求。在许多情况下,我认为将证书保存在文件中是相当安全的,并且可能很有用。

最后,请考虑您可以使用混合配置,例如在证书存储中读取服务器证书,在文件系统中读取客户端证书。

快速入门教程

这是一个使用 IIS/ASP.NET 托管的服务和控制台客户端应用程序来使用证书的简单教程。我将创建一个简单的 Math 服务,其中包含一个 Sum 方法。

创建服务

创建一个新的网站项目;添加对 DevAge.ServiceModel.dll 程序集的引用(您可以在示例项目中找到它)。

添加这些文件:

  • App_Code/MathService.cs

    using System;
    using System.ServiceModel;
    using System.Runtime.Serialization;
    
    [ServiceContract()]
    public interface IMathService
    {
        [OperationContract]
        double Sum(double a, double b);
    }
    
    public class MathService : IMathService
    {
        public double Sum(double a, double b)
        {
            return a + b;
        }
    }    
  • MathService.svc

    <% @ServiceHost Language=C# Debug="true"
            Service="MathService"
            CodeBehind="~/App_Code/MathService.cs"
            Factory="DevAge.ServiceModel.CertificateServiceHostFactory" %>    
  • Web.config

    <?xml version="1.0"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
      <configSections>
        <!-- Add devage.serviceModel section handler -->
        <section name="devage.serviceModel"
            type="DevAge.ServiceModel.Configuration.Section, 
                        DevAge.ServiceModel" />
      </configSections>
    
      <system.serviceModel>
        <services>
          <service name="MathService" behaviorConfiguration="behavior1">
            <endpoint contract="IMathService"
                binding="wsHttpBinding" bindingConfiguration="binding1"/>
          </service>
        </services>
    
        <behaviors>
          <serviceBehaviors>
            <behavior name="behavior1">
              <serviceMetadata httpGetEnabled="true"/>
              <serviceDebug includeExceptionDetailInFaults="true"/>
            </behavior>
    
          </serviceBehaviors>
        </behaviors>
        <bindings>
          <wsHttpBinding>
            <binding name="binding1">
              <security mode="Message">
    
                <message clientCredentialType="Certificate" />
              </security>
            </binding>
          </wsHttpBinding>
        </bindings>
      </system.serviceModel>
    
      <!-- Specific DevAge.ServiceModel configuration used
            to set the client and server certificate -->
      <devage.serviceModel>
        <!-- List of service type each with the relative server
            certificate and client certificate list -->
        <services>
          <add name="MathService"
                serverCertificate="App_Data\Server.pfx"
                clientCertificates=""
                        />
        </services>
    
      </devage.serviceModel>
    </configuration>    

现在您必须获取一个服务器证书,您可以从证书颁发机构购买,或者按照以下过程创建一个自签名证书:
打开 Visual Studio 命令提示符,转到 App_Data 目录并执行以下命令(在提示输入密码时不要输入):

makecert -r -pe -n "CN=CompanyXYZ Server" -b 01/01/2007 -e 01/01/2010
        -sky exchange Server.cer -sv Server.pvk
pvk2pfx.exe -pvk Server.pvk -spc Server.cer -pfx Server.pfx    

在 Visual Studio 中运行项目以启动 ASP.NET 开发服务器,然后尝试访问 MathService.svc 页面。

创建客户端

使用“Client”作为项目名称创建一个新的控制台应用程序项目;添加对 DevAge.ServiceModel.dll 程序集的引用(您可以在示例项目中找到它)。在 Visual Studio 中,您必须右键单击项目并选择“添加服务引用”。插入您的服务的 URL(可能类似于 https://:1281/Server/MathService.svc)和名称 MathService

创建一个新的 Program.cs 文件来像这样测试您的服务:

using System;
using System.Collections.Generic;
using System.Text;

namespace Client
{
class Program
{
    static void Main(string[] args)
    {
        MathService.MathServiceClient service =
                new MathService.MathServiceClient();

        //Configure the client reading the devage.serviceModel 
        //configuration section
        DevAge.ServiceModel.Proxy<MathService.IMathService>.Configure(service);

        try
        {
            double val = service.Sum(10, 20);

            Console.WriteLine("OK");
        }
        catch (Exception ex)
        {
            service.Abort();

            Console.WriteLine(ex.ToString());
        }

        Console.WriteLine("Press Enter key to exit...");
        Console.ReadLine();
    }
}
}    

打开 app.config 文件,将 certificate 标签中的 encodedValue 属性保存到临时文件中。用下面的配置替换 app.config 文件,但请记住恢复以前的 encodedValue 属性,并检查 address 属性以指向正确的服务 URL:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>

    <!-- Add devage.serviceModel section handler -->
    <section name="devage.serviceModel"
        type="DevAge.ServiceModel.Configuration.Section, DevAge.ServiceModel" />
  </configSections>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>

        <binding name="binding1" >
          <security mode="Message">
            <!-- Set the client authentication mode -->
            <message clientCredentialType="Certificate" />
          </security>
        </binding>

      </wsHttpBinding>
    </bindings>
    <behaviors>
      <endpointBehaviors>
        <behavior name="behavior1">
          <clientCredentials>

            <serviceCertificate>
              <authentication certificateValidationMode="None" />
            </serviceCertificate>
          </clientCredentials>
        </behavior>
      </endpointBehaviors>

    </behaviors>
    <client>
      <endpoint address="https://:1281/Server/MathService.svc"
          binding="wsHttpBinding" bindingConfiguration="binding1"
          contract="Client.MathService.IMathService"
          behaviorConfiguration="behavior1"
           name="endpoint1" >
        <identity>
          <!--This is the value used to check the server certificate identity,
            this value is generated by svcutil -->
          <certificate encodedValue="TODO Here use your specific encodedValue" />
        </identity>
      </endpoint>
    </client>
  </system.serviceModel>
  <!-- Specific DevAge.ServiceModel configuration -->
  <devage.serviceModel>

    <!-- List of endPoints each with the relative client certificate
         to use for authentication. -->
    <endPoints>
      <add contract="Client.MathService.IMathService"
            clientCertificate="..\..\Client.pfx" />
    </endPoints>
  </devage.serviceModel>
</configuration>    

现在我们必须生成客户端证书。同样,此证书可以从证书颁发机构获取,或通过以下过程创建:
打开 Visual Studio 命令提示符,转到客户端项目目录并执行以下命令(在提示输入密码时不要输入):

makecert -r -pe -n "CN=CompanyXYZ Client" -b 01/01/2007 -e 01/01/2010
        -sky exchange Client.cer -sv Client.pvk
pvk2pfx.exe -pvk Client.pvk -spc Client.cer -pfx Client.pfx    

最后,将 Client.cer 文件复制到服务器的 App_Data 目录,并修改服务器的 web.config 文件,如下所示添加客户端证书:

<!-- Specific DevAge.ServiceModel configuration used to set
    the client and server certificate -->
<devage.serviceModel>

    <!-- List of service type each with the relative server
          certificate and client certificate list -->
    <services>
      <add name="MathService"
            serverCertificate="App_Data\Server.pfx"
            clientCertificates="App_Data\Client.cer"
                    />
    </services>
</devage.serviceModel>    

注意 clientCertificates 属性,它现在包含 Client.cer 文件。

现在您可以测试您的客户端应用程序了。

示例项目

在示例项目中,我创建了一个完整的可运行示例,包括:

  • Server - 一个由网站项目托管的服务
  • Client - 实现为控制台应用程序的服务客户端
  • DevAge.ServiceModel - 包含用于从文件加载证书的新实现的代码库

您只需使用 Visual Studio 2005 打开解决方案,然后运行 Client 项目即可测试代码。

结论

我认为本文展示了不使用证书存储从文件加载证书的优点和缺点。安全性很重要,我知道此解决方案并非完美无缺,但可能是一个有用的替代方案。但请记住,更改配置文件以使用经典方法始终是容易的。

如果您发现任何错误(代码或文档),或者有任何问题、疑问或建议,请写信给我。我将非常感谢您的反馈。

外部资源

历史

  • 2007 年 4 月 30 日:首次发布
一种易于为 WCF 安全性使用证书的方法 - CodeProject - 代码之家
© . All rights reserved.