授权策略

此示例演示如何实现自定义声明授权策略和关联的自定义服务授权管理器。 这在服务对服务操作进行基于声明的访问检查,并在进行访问检查之前授予调用方某些权限时很有用。 此示例演示了添加声明的过程以及针对最终声明集执行访问检查的过程。 客户端和服务器之间的所有应用程序消息都经过签名和加密。 默认情况下,使用 wsHttpBinding 绑定时,客户端提供的用户名和密码用于登录到有效的 Windows 帐户。 此示例演示如何利用自定义 UserNamePasswordValidator 对客户端进行身份验证。 此外,此示例还显示使用 X.509 证书向服务进行身份验证的客户端。 此示例展示了实现IAuthorizationPolicy和ServiceAuthorizationManager,二者共同为特定用户提供对服务特定方法的访问权限。 此示例基于 消息安全用户名,但演示了如何在调用 ServiceAuthorizationManager 之前执行声明转换。

注释

本示例的设置过程和生成说明位于本主题末尾。

总之,此示例演示了如何:

  • 可以使用用户名密码对客户端进行身份验证。

  • 可以使用 X.509 证书对客户端进行身份验证。

  • 服务器根据自定义 UsernamePassword 验证程序验证客户端凭据。

  • 服务器使用服务器的 X.509 证书进行身份验证。

  • 服务器可用于 ServiceAuthorizationManager 控制对服务中某些方法的访问。

  • 如何实现 IAuthorizationPolicy。

该服务公开两个终结点,用于与服务通信,使用配置文件 App.config定义。每个终结点由地址、绑定和协定组成。 一个绑定配置了标准的 wsHttpBinding 绑定,该绑定使用 WS-Security 和客户端用户名身份验证。 另一个绑定是使用标准 wsHttpBinding 绑定配置的,该标准绑定使用 WS-Security 和客户端证书身份验证。 指定的 <行为> 是使用用户凭据进行服务身份验证。 服务器证书必须包含与 SubjectName属性findValue相同的值<。

<system.serviceModel>
  <services>
    <service name="Microsoft.ServiceModel.Samples.CalculatorService"
             behaviorConfiguration="CalculatorServiceBehavior">
      <host>
        <baseAddresses>
          <!-- configure base address provided by host -->
          <add baseAddress ="http://localhost:8001/servicemodelsamples/service"/>
        </baseAddresses>
      </host>
      <!-- use base address provided by host, provide two endpoints -->
      <endpoint address="username"
                binding="wsHttpBinding"
                bindingConfiguration="Binding1"
                contract="Microsoft.ServiceModel.Samples.ICalculator" />
      <endpoint address="certificate"
                binding="wsHttpBinding"
                bindingConfiguration="Binding2"
                contract="Microsoft.ServiceModel.Samples.ICalculator" />
    </service>
  </services>

  <bindings>
    <wsHttpBinding>
      <!-- Username binding -->
      <binding name="Binding1">
        <security mode="Message">
    <message clientCredentialType="UserName" />
        </security>
      </binding>
      <!-- X509 certificate binding -->
      <binding name="Binding2">
        <security mode="Message">
          <message clientCredentialType="Certificate" />
        </security>
      </binding>
    </wsHttpBinding>
  </bindings>

  <behaviors>
    <serviceBehaviors>
      <behavior name="CalculatorServiceBehavior" >
        <serviceDebug includeExceptionDetailInFaults ="true" />
        <serviceCredentials>
          <!--
          The serviceCredentials behavior allows one to specify a custom validator for username/password combinations.
          -->
          <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="Microsoft.ServiceModel.Samples.MyCustomUserNameValidator, service" />
          <!--
          The serviceCredentials behavior allows one to specify authentication constraints on client certificates.
          -->
          <clientCertificate>
            <!--
            Setting the certificateValidationMode to PeerOrChainTrust means that if the certificate
            is in the user's Trusted People store, then it will be trusted without performing a
            validation of the certificate's issuer chain. This setting is used here for convenience so that the
            sample can be run without having to have certificates issued by a certification authority (CA).
            This setting is less secure than the default, ChainTrust. The security implications of this
            setting should be carefully considered before using PeerOrChainTrust in production code.
            -->
            <authentication certificateValidationMode="PeerOrChainTrust" />
          </clientCertificate>
          <!--
          The serviceCredentials behavior allows one to define a service certificate.
          A service certificate is used by a client to authenticate the service and provide message protection.
          This configuration references the "localhost" certificate installed during the setup instructions.
          -->
          <serviceCertificate findValue="localhost" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
        </serviceCredentials>
        <serviceAuthorization serviceAuthorizationManagerType="Microsoft.ServiceModel.Samples.MyServiceAuthorizationManager, service">
          <!--
          The serviceAuthorization behavior allows one to specify custom authorization policies.
          -->
          <authorizationPolicies>
            <add policyType="Microsoft.ServiceModel.Samples.CustomAuthorizationPolicy.MyAuthorizationPolicy, PolicyLibrary" />
          </authorizationPolicies>
        </serviceAuthorization>
      </behavior>
    </serviceBehaviors>
  </behaviors>

</system.serviceModel>

每个客户端终结点配置由配置名称、服务终结点的绝对地址、绑定和协定组成。 客户端绑定是使用本例中在<安全性>和 clientCredentialType 中指定的适当的安全模式以及在 <message> 中指定的适当的安全模式来进行配置的。

<system.serviceModel>

    <client>
      <!-- Username based endpoint -->
      <endpoint name="Username"
            address="http://localhost:8001/servicemodelsamples/service/username"
    binding="wsHttpBinding"
    bindingConfiguration="Binding1"
                behaviorConfiguration="ClientCertificateBehavior"
                contract="Microsoft.ServiceModel.Samples.ICalculator" >
      </endpoint>
      <!-- X509 certificate based endpoint -->
      <endpoint name="Certificate"
                        address="http://localhost:8001/servicemodelsamples/service/certificate"
                binding="wsHttpBinding"
            bindingConfiguration="Binding2"
                behaviorConfiguration="ClientCertificateBehavior"
                contract="Microsoft.ServiceModel.Samples.ICalculator">
      </endpoint>
    </client>

    <bindings>
      <wsHttpBinding>
        <!-- Username binding -->
      <binding name="Binding1">
        <security mode="Message">
          <message clientCredentialType="UserName" />
        </security>
      </binding>
        <!-- X509 certificate binding -->
        <binding name="Binding2">
          <security mode="Message">
            <message clientCredentialType="Certificate" />
          </security>
        </binding>
    </wsHttpBinding>
    </bindings>

    <behaviors>
      <behavior name="ClientCertificateBehavior">
        <clientCredentials>
          <serviceCertificate>
            <!--
            Setting the certificateValidationMode to PeerOrChainTrust
            means that if the certificate
            is in the user's Trusted People store, then it will be
            trusted without performing a
            validation of the certificate's issuer chain. This setting
            is used here for convenience so that the
            sample can be run without having to have certificates
            issued by a certification authority (CA).
            This setting is less secure than the default, ChainTrust.
            The security implications of this
            setting should be carefully considered before using
            PeerOrChainTrust in production code.
            -->
            <authentication certificateValidationMode = "PeerOrChainTrust" />
          </serviceCertificate>
        </clientCredentials>
      </behavior>
    </behaviors>

  </system.serviceModel>

对于基于用户名的终结点,客户端实现设置要使用的用户名和密码。

// Create a client with Username endpoint configuration
CalculatorClient client1 = new CalculatorClient("Username");

client1.ClientCredentials.UserName.UserName = "test1";
client1.ClientCredentials.UserName.Password = "1tset";

try
{
    // Call the Add service operation.
    double value1 = 100.00D;
    double value2 = 15.99D;
    double result = client1.Add(value1, value2);
    Console.WriteLine("Add({0},{1}) = {2}", value1, value2, result);
    ...
}
catch (Exception e)
{
    Console.WriteLine("Call failed : {0}", e.Message);
}

client1.Close();

对于基于证书的终结点,客户端实现将设置要使用的客户端证书。

// Create a client with Certificate endpoint configuration
CalculatorClient client2 = new CalculatorClient("Certificate");

client2.ClientCredentials.ClientCertificate.SetCertificate(StoreLocation.CurrentUser, StoreName.My, X509FindType.FindBySubjectName, "test1");

try
{
    // Call the Add service operation.
    double value1 = 100.00D;
    double value2 = 15.99D;
    double result = client2.Add(value1, value2);
    Console.WriteLine("Add({0},{1}) = {2}", value1, value2, result);
    ...
}
catch (Exception e)
{
    Console.WriteLine("Call failed : {0}", e.Message);
}

client2.Close();

此示例使用自定义 UserNamePasswordValidator 来验证用户名和密码。 该示例实现MyCustomUserNamePasswordValidator,这是从UserNamePasswordValidator派生而来的。 有关详细信息,请参阅相关 UserNamePasswordValidator 文档。 为了演示与 UserNamePasswordValidator 的集成,此自定义验证程序示例实现 Validate 接受用户名/密码对的方法,用户名与密码匹配,如以下代码所示。

public class MyCustomUserNamePasswordValidator : UserNamePasswordValidator
{
  // This method validates users. It allows in two users,
  // test1 and test2 with passwords 1tset and 2tset respectively.
  // This code is for illustration purposes only and
  // MUST NOT be used in a production environment because it
  // is NOT secure.
  public override void Validate(string userName, string password)
  {
    if (null == userName || null == password)
    {
      throw new ArgumentNullException();
    }

    if (!(userName == "test1" && password == "1tset") && !(userName == "test2" && password == "2tset"))
    {
      throw new SecurityTokenException("Unknown Username or Password");
    }
  }
}

在服务代码中实现验证程序后,必须告知服务主机要使用的验证程序实例。 这是使用以下代码完成的:

Servicehost.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
serviceHost.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new MyCustomUserNamePasswordValidatorProvider();

或者,您可以在配置中做同样的事情。

<behavior>
    <serviceCredentials>
      <!--
      The serviceCredentials behavior allows one to specify a custom validator for username/password combinations.
      -->
      <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="Microsoft.ServiceModel.Samples.MyCustomUserNameValidator, service" />
    ...
    </serviceCredentials>
</behavior>

Windows Communication Foundation (WCF)提供了丰富的基于声明的模型来执行访问检查。 该 ServiceAuthorizationManager 对象用于执行访问检查,并确定与客户端关联的声明是否满足访问服务方法所需的要求。

为了演示目的,此示例展示了ServiceAuthorizationManager的实现,该实现包含CheckAccessCore方法,以允许用户基于类型http://example.com/claims/allowedoperation的声明访问方法,其中该声明的值为被允许调用的操作的动作 URI。

public class MyServiceAuthorizationManager : ServiceAuthorizationManager
{
  protected override bool CheckAccessCore(OperationContext operationContext)
  {
    string action = operationContext.RequestContext.RequestMessage.Headers.Action;
    Console.WriteLine("action: {0}", action);
    foreach(ClaimSet cs in operationContext.ServiceSecurityContext.AuthorizationContext.ClaimSets)
    {
      if ( cs.Issuer == ClaimSet.System )
      {
        foreach (Claim c in cs.FindClaims("http://example.com/claims/allowedoperation", Rights.PossessProperty))
        {
          Console.WriteLine("resource: {0}", c.Resource.ToString());
          if (action == c.Resource.ToString())
            return true;
        }
      }
    }
    return false;
  }
}

实现自定义 ServiceAuthorizationManager 后,必须通知服务主机关于要使用的 ServiceAuthorizationManager 的信息。 此过程已完成,如下代码所示。

<behavior>
    ...
    <serviceAuthorization serviceAuthorizationManagerType="Microsoft.ServiceModel.Samples.MyServiceAuthorizationManager, service">
        ...
    </serviceAuthorization>
</behavior>

要实现的主要IAuthorizationPolicy方法是Evaluate(EvaluationContext, Object)方法。

public class MyAuthorizationPolicy : IAuthorizationPolicy
{
    string id;

    public MyAuthorizationPolicy()
    {
    id =  Guid.NewGuid().ToString();
    }

    public bool Evaluate(EvaluationContext evaluationContext,
                                            ref object state)
    {
        bool bRet = false;
        CustomAuthState customstate = null;

        if (state == null)
        {
            customstate = new CustomAuthState();
            state = customstate;
        }
        else
            customstate = (CustomAuthState)state;
        Console.WriteLine("In Evaluate");
        if (!customstate.ClaimsAdded)
        {
           IList<Claim> claims = new List<Claim>();

           foreach (ClaimSet cs in evaluationContext.ClaimSets)
              foreach (Claim c in cs.FindClaims(ClaimTypes.Name,
                                         Rights.PossessProperty))
                  foreach (string s in
                        GetAllowedOpList(c.Resource.ToString()))
                  {
                       claims.Add(new
               Claim("http://example.com/claims/allowedoperation",
                                    s, Rights.PossessProperty));
                            Console.WriteLine("Claim added {0}", s);
                      }
                   evaluationContext.AddClaimSet(this,
                           new DefaultClaimSet(this.Issuer,claims));
                   customstate.ClaimsAdded = true;
                   bRet = true;
                }
         else
         {
              bRet = true;
         }
         return bRet;
     }
...
}

前面的代码演示了 Evaluate(EvaluationContext, Object) 方法如何检查没有添加影响处理的新声明,并添加特定的声明。 允许的声明是从 GetAllowedOpList 方法获取的,该方法实现以返回允许用户执行的特定作列表。 授权策略添加了访问特定操作的声明。 ServiceAuthorizationManager 然后使用此授权策略执行访问检查决策。

实现自定义 IAuthorizationPolicy 后,必须通知服务主机要使用的授权策略。

<serviceAuthorization>
       <authorizationPolicies>
            <add policyType='Microsoft.ServiceModel.Samples.CustomAuthorizationPolicy.MyAuthorizationPolicy, PolicyLibrary' />
       </authorizationPolicies>
</serviceAuthorization>

运行示例时,操作请求和响应将显示在客户端控制台窗口中。 客户端成功调用 Add、Subtract 和 Multiple 方法,并在尝试调用 Divide 方法时收到“访问被拒绝”消息。 在客户端窗口中按 Enter 关闭客户端。

设置批处理文件

通过运行此示例随附的 Setup.bat 批处理文件,可以用相关的证书将服务器配置为运行需要基于服务器证书的安全性的自承载应用程序。

下面简要概述了批处理文件的不同部分,以便修改它们以在适当的配置中运行:

  • 创建服务器证书。

    Setup.bat 批处理文件中的以下行创建要使用的服务器证书。 %SERVER_NAME% 变量指定服务器名称。 更改此变量可以指定您自己的服务器名称。 默认值为 localhost。

    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
    
  • 将服务器证书安装到客户端的受信任证书存储中。

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

    certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r CurrentUser -s TrustedPeople
    
  • 创建客户端证书。

    Setup.bat 批处理文件中的以下行创建要使用的客户端证书。 %USER_NAME% 变量指定服务器名称。 此值设置为“test1”,因为 IAuthorizationPolicy 查找的名称是这样。 如果更改 %USER_NAME% 的值,则必须更改方法中的 IAuthorizationPolicy.Evaluate 相应值。

    证书存储在 CurrentUser 存储位置下的“我的(个人”)存储中。

    echo ************
    echo making client cert
    echo ************
    makecert.exe -sr CurrentUser -ss MY -a sha1 -n CN=%CLIENT_NAME% -sky exchange -pe
    
  • 将客户端证书安装到服务器的受信任证书存储中。

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

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

设置和生成示例

  1. 要生成解决方案,请按照生成 Windows Communication Foundation 示例中的说明进行操作。

  2. 若要用单一计算机配置或跨计算机配置来运行示例,请按照下列说明进行操作。

注释

如果使用 Svcutil.exe 重新生成此示例的配置,请确保修改客户端配置中的终结点名称以匹配客户端代码。

在同一计算机上运行示例

  1. 使用管理员权限打开 Visual Studio 开发人员命令提示符,并从示例安装文件夹运行 Setup.bat 。 这会安装运行示例所需的所有证书。

    注释

    Setup.bat 批处理文件旨在从 Visual Studio 开发人员命令提示符运行。 Visual Studio 开发人员命令提示符中设置的 PATH 环境变量指向包含 Setup.bat 脚本所需的可执行文件的目录。

  2. 从 service\bin 启动 Service.exe。

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

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

跨计算机运行示例

  1. 在服务计算机上创建目录。

  2. 将服务程序文件从 \service\bin 复制到服务计算机上的目录。 此外,将 Setup.bat、Cleanup.bat、GetComputerName.vbs 和 ImportClientCert.bat 文件复制到服务计算机。

  3. 在客户端计算机上为客户端二进制文件创建目录。

  4. 将客户端程序文件复制到客户端计算机上的客户端目录。 此外,将 Setup.bat、Cleanup.bat和 ImportServiceCert.bat 文件复制到客户端。

  5. 在服务器上,使用管理员权限在 Visual Studio 的开发人员命令提示符下运行 setup.bat service 。

    使用setup.bat参数运行service会创建具有计算机完全限定域名的服务证书,并将服务证书导出到名为Service.cer的文件。

  6. 编辑 Service.exe.config 以反映新证书名称(findValue 属性在 <serviceCertificate>中),该名称与计算机的完全限定域名相同。 此外,将 `service</>baseAddresses<` 元素中的 `>` 从 localhost 更改为服务计算机的完全限定名称。

  7. 将 Service.cer 文件从服务目录复制到客户端计算机上的客户端目录。

  8. 在客户端上,使用管理员权限在 Visual Studio 的开发人员命令提示符下运行 setup.bat client 。

    使用setup.bat参数运行client将创建名为 test1 的客户端证书,并将客户端证书导出到名为Client.cer的文件。

  9. 在客户端计算机上的 Client.exe.config 文件中,更改终结点的地址值以匹配服务的新地址。 为此,将 localhost 替换为服务器的完全限定域名。

  10. 将Client.cer文件从客户端目录复制到服务器上的服务目录。

  11. 在客户端上,使用管理员权限打开的 Visual Studio 开发人员命令提示符中运行 ImportServiceCert.bat 。

    这会将服务证书从 Service.cer 文件导入 CurrentUser - TrustedPeople 存储中。

  12. 在服务器上,使用管理员权限打开的 Visual Studio 开发人员命令提示符中运行 ImportClientCert.bat 。

    这会将客户端证书从 Client.cer 文件导入 LocalMachine - TrustedPeople 存储中。

  13. 在服务器计算机上,从命令提示符窗口启动 Service.exe。

  14. 在客户端计算机上,从命令提示符窗口启动 Client.exe。

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

示例完成后的清理

若要在示例后进行清理,请在示例文件夹中运行完示例后 运行Cleanup.bat 。 这会从证书存储中删除服务器和客户端证书。

注释

在跨计算机运行此示例时,此脚本不会删除客户端上的服务证书。 如果已运行跨计算机使用证书的 WCF 示例,请确保清除已在 CurrentUser - TrustedPeople 存储中安装的服务证书。 为此,请使用以下命令:certmgr -del -r CurrentUser -s TrustedPeople -c -n <Fully Qualified Server Machine Name> 例如:certmgr -del -r CurrentUser -s TrustedPeople -c -n server1.contoso.com。