受信任的外观服务

TrustedFacade 示例演示如何使用 Windows Communication Foundation (WCF) 安全基础结构将调用方的身份信息从一个服务流向另一个服务。

通过门面服务将服务提供的功能公开给公共网络是一种常见的设计模式。 外观服务通常驻留在外围网络(也称为外围网络、非军事区域和屏蔽子网)中,并与实现业务逻辑并有权访问内部数据的后端服务通信。 外观服务和后端服务之间的通信通道通过防火墙,通常仅限于单一用途。

此示例包含以下组件:

  • 计算器客户端

  • 计算器外观服务

  • 计算器后端服务

外观服务负责验证请求和对调用方进行身份验证。 成功进行身份验证和验证后,它会使用从外围网络的受控通信通道将请求转发到后端服务,并将其转发到内部网络。 作为所转发请求的一部分,外观服务包含有关调用方标识的信息,这样后端服务可以在其处理过程中使用此信息。 调用方的身份通过信息Username标头中的Security安全令牌进行传输。 此示例使用 WCF 安全基础结构从 Security 标头传输和提取此信息。

重要

后端服务委托外观服务对调用方进行身份验证。 因此,后端服务不会再次对调用方进行身份验证;它在转发的请求中使用外观服务提供的标识信息。 由于此信任关系,后端服务必须对外观服务进行身份验证,以确保转发的消息来自受信任的源(在本例中为外观服务)。

执行

此示例中有两个通信路径。 第一个是在客户端和外观服务之间,第二个是在外观服务和后端服务之间。

客户端和外观服务之间的通信路径

客户端到外观服务这一通信路径使用具有 wsHttpBinding 客户端凭据类型的 UserName 。 这意味着客户端使用用户名和密码向外观服务进行身份验证,外观服务使用 X.509 证书向客户端进行身份验证。 绑定配置如以下示例所示。

<bindings>
  <wsHttpBinding>
    <binding name="Binding1">
      <security mode="Message">
        <message clientCredentialType="UserName"/>
      </security>
    </binding>
  </wsHttpBinding>
</bindings>

外观服务使用自定义 UserNamePasswordValidator 实现对调用方进行身份验证。 出于演示目的,身份验证只确保调用方用户名与显示的密码匹配。 在实际情况下,用户可能使用 Active Directory 或自定义 ASP.NET 成员身份提供程序进行身份验证。 验证程序实现驻留在 FacadeService.cs 文件中。

public class MyUserNamePasswordValidator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        // check that username matches password
        if (null == userName || userName != password)
        {
            Console.WriteLine("Invalid username or password");
            throw new SecurityTokenValidationException(
                       "Invalid username or password");
        }
    }
}

自定义验证程序配置为在外观服务配置文件的 serviceCredentials 行为内部使用。 此行为还用于配置服务的 X.509 证书。

<behaviors>
  <serviceBehaviors>
    <behavior name="FacadeServiceBehavior">
      <!--The serviceCredentials behavior allows you to define -->
      <!--a service certificate. -->
      <!--A service certificate is used by the service to  -->
      <!--authenticate itself to its clients and to provide  -->
      <!--message protection. -->
      <!--This configuration references the "localhost"  -->
      <!--certificate installed during the setup instructions. -->
      <serviceCredentials>
        <serviceCertificate
               findValue="localhost"
               storeLocation="LocalMachine"
               storeName="My"
               x509FindType="FindBySubjectName" />
        <userNameAuthentication userNamePasswordValidationMode="Custom"
            customUserNamePasswordValidatorType=
           "Microsoft.ServiceModel.Samples.MyUserNamePasswordValidator,
            FacadeService"/>
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>

门面服务和后端服务之间的通信路径

外观服务到后端服务这一通信路径使用包含多个绑定元素的 customBinding 。 此绑定可实现两件事情。 它对外观服务和后端服务进行身份验证,以确保通信是安全的,并且来自受信任的源。 此外,它还在Username安全令牌内传输初始调用者的身份。 在这种情况下,仅将初始调用方用户名传输到后端服务,消息中不包含密码。 这是因为后端服务信任外观服务,以便在将请求转发给调用方之前对调用方进行身份验证。 由于外观服务向后端服务进行身份验证,因此后端服务可以信任转发请求中包含的信息。

下面是此通信路径的绑定配置。

<bindings>
  <customBinding>
    <binding name="ClientBinding">
      <security authenticationMode="UserNameOverTransport"/>
      <windowsStreamSecurity/>
      <tcpTransport/>
    </binding>
  </customBinding>
</bindings>

<安全>绑定元素负责初始调用方用户名传输和提取。 <windowsStreamSecurity><tcpTransport> 负责对外观和后端服务进行身份验证,并保护消息。

若要转发请求,外观服务实现必须提供初始调用方用户名,以便 WCF 安全基础结构可以将它放入转发的消息中。 初始调用方用户名在外观服务实现中提供,方法是在外观服务用来与后端服务通信的客户端代理实例的属性中 ClientCredentials 设置该用户名。

下面的代码演示如何在外观服务上实现 GetCallerIdentity 方法。 其他方法使用相同的模式。

public string GetCallerIdentity()
{
    CalculatorClient client = new CalculatorClient();
    client.ClientCredentials.UserName.UserName = ServiceSecurityContext.Current.PrimaryIdentity.Name;
    string result = client.GetCallerIdentity();
    client.Close();
    return result;
}

如前面的代码所示,未在 ClientCredentials 属性上设置密码,仅设置用户名。 在这种情况下,WCF 安全基础结构会创建用户名安全令牌,而无需密码,这正是此方案中所需的。

在后端服务中,必须对用户名安全令牌中包含的信息进行身份验证。 默认情况下,WCF 安全尝试使用提供的密码将用户映射到 Windows 帐户。 在这种情况下,没有提供密码,后端服务不需要对用户名进行身份验证,因为身份验证已由外观服务执行。 为了在 WCF 中实现此功能,提供了一个自定义 UserNamePasswordValidator 项,仅强制在令牌中指定用户名,并且不执行任何其他身份验证。

public class MyUserNamePasswordValidator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        // Ignore the password because it is empty,
        // we trust the facade service to authenticate the client.
        // Accept the username information here so that the
        // application gets access to it.
        if (null == userName)
        {
            Console.WriteLine("Invalid username");
            throw new
             SecurityTokenValidationException("Invalid username");
        }
    }
}

自定义验证程序配置为在外观服务配置文件的 serviceCredentials 行为内部使用。

<behaviors>
  <serviceBehaviors>
    <behavior name="BackendServiceBehavior">
      <serviceCredentials>
        <userNameAuthentication userNamePasswordValidationMode="Custom"
           customUserNamePasswordValidatorType=
          "Microsoft.ServiceModel.Samples.MyUserNamePasswordValidator,
           BackendService"/>
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>

为提取用户名信息和有关受信任外观服务帐户的信息,后端服务实现使用 ServiceSecurityContext 类。 以下代码展示了如何实现 GetCallerIdentity 方法。

public string GetCallerIdentity()
{
    // Facade service is authenticated using Windows authentication.
    //Its identity is accessible.
    // On ServiceSecurityContext.Current.WindowsIdentity.
    string facadeServiceIdentityName =
          ServiceSecurityContext.Current.WindowsIdentity.Name;

    // The client name is transmitted using Username authentication on
    //the message level without the password
    // using a supporting encrypted UserNameToken.
    // Claims extracted from this supporting token are available in
    // ServiceSecurityContext.Current.AuthorizationContext.ClaimSets
    // collection.
    string clientName = null;
    foreach (ClaimSet claimSet in
        ServiceSecurityContext.Current.AuthorizationContext.ClaimSets)
    {
        foreach (Claim claim in claimSet)
        {
            if (claim.ClaimType == ClaimTypes.Name &&
                                   claim.Right == Rights.Identity)
            {
                clientName = (string)claim.Resource;
                break;
            }
        }
    }
    if (clientName == null)
    {
        // In case there was no UserNameToken attached to the request.
        // In the real world implementation the service should reject
        // this request.
        return "Anonymous caller via " + facadeServiceIdentityName;
    }

    return clientName + " via " + facadeServiceIdentityName;
}

使用ServiceSecurityContext.Current.WindowsIdentity属性提取界面服务帐户信息。 后端服务使用ServiceSecurityContext.Current.AuthorizationContext.ClaimSets属性来访问有关初始调用者的信息。 该属性查找类型为 IdentityName声明。 此声明由 WCF 安全基础结构从安全令牌中包含的 Username 信息自动生成。

运行示例

运行示例时,操作请求和响应将显示在客户端控制台窗口中。 在客户端窗口中按 Enter 关闭客户端。 可以在外观和后端服务控制台窗口中按 Enter 关闭服务。

Username authentication required.
Provide a valid machine or ___domain ac
   Enter username:
user
   Enter password:
****
user via MyMachine\testaccount
Add(100,15.99) = 115.99
Subtract(145,76.54) = 68.46
Multiply(9,81.25) = 731.25
Divide(22,7) = 3.14285714285714

Press <ENTER> to terminate client.

使用“受信任外观”方案示例中包括的 Setup.bat 批处理文件可以用相关证书配置服务器,以便运行需要基于证书的安全向客户端对其自身进行身份验证的外观服务。 有关详细信息,请参阅本主题末尾的设置过程。

下面简要概述了批处理文件的不同部分。

  • 创建服务器证书。

    Setup.bat 批处理文件中的以下行创建要使用的服务器证书。

    echo ************
    echo Server cert setup starting
    echo %SERVER_NAME%
    echo ************
    echo making server cert
    echo ************
    makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=%SERVER_NAME% -sky exchange -pe
    

    变量 %SERVER_NAME% 指定服务器名称 - 默认值为 localhost。 证书存储在 LocalMachine 存储中。

  • 将外观服务的证书安装到客户端的受信任证书存储区中。

    下面的行将外观服务的证书复制到客户端的受信任人存储中。 此步骤是必需的,因为 Makecert.exe 生成的证书不受客户端系统隐式信任。 如果已有一个证书,该证书已植根于客户端受信任的根证书(例如Microsoft颁发的证书),则不需要使用服务器证书填充客户端证书存储区。

    certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r CurrentUser -s TrustedPeople
    

设置、生成和运行示例

  1. 确保已为 Windows Communication Foundation 示例 执行One-Time 安装过程。

  2. 若要生成解决方案的 C# 或 Visual Basic .NET 版本,请按照 生成 Windows Communication Foundation 示例中的说明进行操作。

在同一计算机上运行示例

  1. 确保路径包含 Makecert.exe 所在的文件夹。

  2. 从示例安装文件夹运行 Setup.bat。 这会安装运行示例所需的所有证书。

  3. 在单独的控制台窗口中从 \BackendService\bin 目录启动 BackendService.exe

  4. 在单独的控制台窗口中从 \FacadeService\bin 目录启动 FacadeService.exe

  5. 从 \client\bin 启动 Client.exe。 客户端活动显示在客户端控制台应用程序中。

  6. 如果客户端和服务无法通信,请参阅 WCF 示例 故障排除提示。

运行示例后进行清理

  1. 运行完示例后,在示例文件夹中运行 Cleanup.bat。