支持令牌

支持令牌示例演示如何向使用 WS-Security 的消息添加其他令牌。 该示例除了添加用户名安全令牌外,还添加了 X.509 二进制安全令牌。 令牌在从客户端到服务的 WS-Security 消息标头中传递,并且该消息的一部分使用与 X.509 安全令牌关联的私钥进行签名,以向接收方证明 X.509 证书的所有权。 如果要求有多个声明与消息关联,以便对发件人进行身份验证或授权,则这样做非常有用。 该服务实现定义请求-回复通信模式的协定。

演示

此示例演示:

  • 客户端如何将其他安全令牌传递给服务。

  • 服务器如何访问与附加安全令牌相关的声明。

  • 服务器的 X.509 证书如何用于保护用于消息加密和签名的对称密钥。

注释

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

客户端使用用户名令牌和支持 X.509 安全令牌进行身份验证

该服务公开一个终结点,用于通信,该终结点是使用 BindingHelperEchoServiceHost 类以编程方式创建的。 终结点由地址、绑定和协定组成。 此绑定使用 SymmetricSecurityBindingElementHttpTransportBindingElement 按照自定义绑定进行配置。 本示例设置了 SymmetricSecurityBindingElement 以便使用服务 X.509 证书在传输过程中保护对称密钥和在 WS-Security 消息头中传递 UserNameToken 并支持 X509SecurityToken。 对称密钥用于加密消息正文和用户名安全令牌。 支持令牌作为附加的二进制安全令牌传递到 WS-Security 消息标头中。 支持令牌的真实性是通过使用与支持 X.509 安全令牌关联的私钥对消息的一部分进行签名来证明的。

public static Binding CreateMultiFactorAuthenticationBinding()
{
    HttpTransportBindingElement httpTransport = new HttpTransportBindingElement();

    // the message security binding element will be configured to require 2 tokens:
    // 1) A username-password encrypted with the service token
    // 2) A client certificate used to sign the message

    // Instantiate a binding element that will require the username/password token in the message (encrypted with the server cert)
    SymmetricSecurityBindingElement messageSecurity = SecurityBindingElement.CreateUserNameForCertificateBindingElement();

    // Create supporting token parameters for the client X509 certificate.
    X509SecurityTokenParameters clientX509SupportingTokenParameters = new X509SecurityTokenParameters();
    // Specify that the supporting token is passed in message send by the client to the service
    clientX509SupportingTokenParameters.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;
    // Turn off derived keys
    clientX509SupportingTokenParameters.RequireDerivedKeys = false;
    // Augment the binding element to require the client's X509 certificate as an endorsing token in the message
    messageSecurity.EndpointSupportingTokenParameters.Endorsing.Add(clientX509SupportingTokenParameters);

    // Create a CustomBinding based on the constructed security binding element.
    return new CustomBinding(messageSecurity, httpTransport);
}

该行为指定要用于客户端身份验证的服务凭据,以及有关服务 X.509 证书的信息。 本示例在服务 X.509 证书中将 CN=localhost 用作主题名称。

override protected void InitializeRuntime()
{
    // Extract the ServiceCredentials behavior or create one.
    ServiceCredentials serviceCredentials =
        this.Description.Behaviors.Find<ServiceCredentials>();
    if (serviceCredentials == null)
    {
        serviceCredentials = new ServiceCredentials();
        this.Description.Behaviors.Add(serviceCredentials);
    }

    // Set the service certificate
    serviceCredentials.ServiceCertificate.SetCertificate(
                                       "CN=localhost");

/*
Setting the CertificateValidationMode to PeerOrChainTrust means that if the certificate is in the 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.
*/
    serviceCredentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.PeerOrChainTrust;

    // Create the custom binding and add an endpoint to the service.
    Binding multipleTokensBinding =
         BindingHelper.CreateMultiFactorAuthenticationBinding();
    this.AddServiceEndpoint(typeof(IEchoService),
                          multipleTokensBinding, string.Empty);
    base.InitializeRuntime();
}

服务代码:

[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class EchoService : IEchoService
{
    public string Echo()
    {
        string userName;
        string certificateSubjectName;
        GetCallerIdentities(
            OperationContext.Current.ServiceSecurityContext,
            out userName,
            out certificateSubjectName);
            return $"Hello {userName}, {certificateSubjectName}";
    }

    public void Dispose()
    {
    }

    bool TryGetClaimValue<TClaimResource>(ClaimSet claimSet,
            string claimType, out TClaimResource resourceValue)
            where TClaimResource : class
    {
        resourceValue = default(TClaimResource);
        IEnumerable<Claim> matchingClaims =
            claimSet.FindClaims(claimType, Rights.PossessProperty);
        if(matchingClaims == null)
            return false;
        IEnumerator<Claim> enumerator = matchingClaims.GetEnumerator();
        if (enumerator.MoveNext())
        {
            resourceValue =
              (enumerator.Current.Resource == null) ? null :
              (enumerator.Current.Resource as TClaimResource);
            return true;
        }
        else
        {
            return false;
        }
    }

    // Returns the username and certificate subject name provided by
    //the client
    void GetCallerIdentities(ServiceSecurityContext
        callerSecurityContext,
        out string userName, out string certificateSubjectName)
    {
        userName = null;
        certificateSubjectName = null;

       // Look in all the claimsets in the authorization context
       foreach (ClaimSet claimSet in
               callerSecurityContext.AuthorizationContext.ClaimSets)
       {
            if (claimSet is WindowsClaimSet)
            {
                // Try to find a Name claim. This will have been
                // generated from the windows username.
                string tmpName;
                if (TryGetClaimValue<string>(claimSet, ClaimTypes.Name,
                                                      out tmpName))
                {
                    userName = tmpName;
                }
            }
            else if (claimSet is X509CertificateClaimSet)
            {
                // Try to find an X500DistinguishedName claim. This will
                // have been generated from the client certificate.
                X500DistinguishedName tmpDistinguishedName;
                if (TryGetClaimValue<X500DistinguishedName>(claimSet,
                               ClaimTypes.X500DistinguishedName,
                               out tmpDistinguishedName))
                {
                    certificateSubjectName = tmpDistinguishedName.Name;
                }
            }
        }
    }
}

客户端终结点的配置方式与服务终结点类似。 客户端使用相同的 BindingHelper 类来创建绑定。 设置的其余部分位于类中 Client 。 客户端将有关用户名安全令牌、支持的 X.509 安全令牌以及安装代码中服务 X.509 证书的信息设置为客户端终结点行为集合。

static void Main()
 {
     // Create the custom binding and an endpoint address for
     // the service.
     Binding multipleTokensBinding =
         BindingHelper.CreateMultiFactorAuthenticationBinding();
         EndpointAddress serviceAddress = new EndpointAddress(
         "http://localhost/servicemodelsamples/service.svc");
       ChannelFactory<IEchoService> channelFactory = null;
       IEchoService client = null;

       Console.WriteLine("Username authentication required.");
       Console.WriteLine(
         "Provide a valid machine or domain account. [domain\\user]");
       Console.WriteLine("   Enter username:");
       string username = Console.ReadLine();
       Console.WriteLine("   Enter password:");
       string password = "";
       ConsoleKeyInfo info = Console.ReadKey(true);
       while (info.Key != ConsoleKey.Enter)
       {
           if (info.Key != ConsoleKey.Backspace)
           {
               if (info.KeyChar != '\0')
               {
                   password += info.KeyChar;
                }
                info = Console.ReadKey(true);
            }
            else if (info.Key == ConsoleKey.Backspace)
            {
                if (password != "")
                {
                    password =
                       password.Substring(0, password.Length - 1);
                }
                info = Console.ReadKey(true);
            }
         }
         for (int i = 0; i < password.Length; i++)
            Console.Write("*");
         Console.WriteLine();
         try
         {
           // Create a proxy with the previously create binding and
           // endpoint address
              channelFactory =
                 new ChannelFactory<IEchoService>(
                     multipleTokensBinding, serviceAddress);
           // configure the username credentials, the client
           // certificate and the server certificate on the channel
           // factory
           channelFactory.Credentials.UserName.UserName = username;
           channelFactory.Credentials.UserName.Password = password;
           channelFactory.Credentials.ClientCertificate.SetCertificate(
           "CN=client.com", StoreLocation.CurrentUser, StoreName.My);
              channelFactory.Credentials.ServiceCertificate.SetDefaultCertificate(
           "CN=localhost", StoreLocation.LocalMachine, StoreName.My);
           client = channelFactory.CreateChannel();
           Console.WriteLine("Echo service returned: {0}",
                                           client.Echo());

           ((IChannel)client).Close();
           channelFactory.Close();
        }
        catch (CommunicationException e)
        {
         Abort((IChannel)client, channelFactory);
         // if there is a fault then print it out
         FaultException fe = null;
         Exception tmp = e;
         while (tmp != null)
         {
            fe = tmp as FaultException;
            if (fe != null)
            {
                break;
            }
            tmp = tmp.InnerException;
        }
        if (fe != null)
        {
           Console.WriteLine("The server sent back a fault: {0}",
         fe.CreateMessageFault().Reason.GetMatchingTranslation().Text);
        }
        else
        {
         Console.WriteLine("The request failed with exception: {0}",e);
        }
    }
    catch (TimeoutException)
    {
        Abort((IChannel)client, channelFactory);
        Console.WriteLine("The request timed out");
    }
    catch (Exception e)
    {
         Abort((IChannel)client, channelFactory);
          Console.WriteLine(
          "The request failed with unexpected exception: {0}", e);
    }
    Console.WriteLine();
    Console.WriteLine("Press <ENTER> to terminate client.");
    Console.ReadLine();
}

显示调用方的信息

若要显示调用方的信息,可以使用 ServiceSecurityContext.Current.AuthorizationContext.ClaimSets 以下代码所示。 ServiceSecurityContext.Current.AuthorizationContext.ClaimSets 包含与当前调用方相关的授权声明。 Windows Communication Foundation (WCF) 自动将这些声明提供给消息中接收的每个令牌。

bool TryGetClaimValue<TClaimResource>(ClaimSet claimSet, string
                         claimType, out TClaimResource resourceValue)
    where TClaimResource : class
{
    resourceValue = default(TClaimResource);
    IEnumerable<Claim> matchingClaims =
    claimSet.FindClaims(claimType, Rights.PossessProperty);
    if (matchingClaims == null)
          return false;
    IEnumerator<Claim> enumerator = matchingClaims.GetEnumerator();
    if (enumerator.MoveNext())
    {
        resourceValue = (enumerator.Current.Resource == null) ? null : (enumerator.Current.Resource as TClaimResource);
        return true;
    }
    else
    {
         return false;
    }
}

// Returns the username and certificate subject name provided by the client
void GetCallerIdentities(ServiceSecurityContext callerSecurityContext, out string userName, out string certificateSubjectName)
{
    userName = null;
    certificateSubjectName = null;

    // Look in all the claimsets in the authorization context
    foreach (ClaimSet claimSet in
      callerSecurityContext.AuthorizationContext.ClaimSets)
    {
        if (claimSet is WindowsClaimSet)
        {
            // Try to find a Name claim. This will have been generated
            //from the windows username.
            string tmpName;
            if (TryGetClaimValue<string>(claimSet, ClaimTypes.Name,
                                                     out tmpName))
            {
                userName = tmpName;
            }
        }
        else if (claimSet is X509CertificateClaimSet)
         {
            //Try to find an X500DistinguishedName claim.
            //This will have been generated from the client
            //certificate.
            X500DistinguishedName tmpDistinguishedName;
            if (TryGetClaimValue<X500DistinguishedName>(claimSet,
               ClaimTypes.X500DistinguishedName,
               out tmpDistinguishedName))
            {
                    certificateSubjectName = tmpDistinguishedName.Name;
            }
        }
    }
}

运行示例

运行示例时,客户端会首先提示你提供用户名令牌的用户名和密码。 请务必为系统帐户提供正确的值,因为服务上的 WCF 会将用户名令牌中提供的值映射到系统提供的标识中。 之后,客户端会显示来自服务的响应。 在客户端窗口中按 Enter 关闭客户端。

设置批处理文件

此示例随附的 Setup.bat 批处理文件允许你配置具有相关证书的服务器,以运行需要基于服务器证书的安全性的 Internet Information Services(IIS)托管应用程序。 必须修改此批处理文件才能跨计算机工作,或在非托管的情况下工作。

下面简要概述了批处理文件的不同部分,以便可以对其进行修改以在适当配置中运行。

创建客户端证书

Setup.bat 批处理文件中的以下行创建要使用的客户端证书。 该 %CLIENT_NAME% 变量指定客户端证书的主题。 此示例使用“client.com”作为主题名称。

证书存储在 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颁发的证书),则不需要使用服务器证书填充客户端证书存储区。

echo ************
echo copying client cert to server's CurrentUserstore
echo ************
certmgr.exe -add -r CurrentUser -s My -c -n %CLIENT_NAME% -r LocalMachine -s TrustedPeople

创建服务器证书

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

证书存储在 LocalMachine 存储位置下的 My(个人)存储区中。 证书存储在 IIS 托管服务的 LocalMachine 存储中。 对于自承载服务,应将批处理文件修改为将服务器证书存储在 CurrentUser 存储位置,方法是将字符串 LocalMachine 替换为 CurrentUser。

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颁发的证书),则不需要使用服务器证书填充客户端证书存储区。

echo ************
echo copying server cert to client's TrustedPeople store
echo ************certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r CurrentUser -s TrustedPeople

启用对证书私钥的访问

若要从 IIS 托管服务启用对证书私钥的访问权限,必须向运行 IIS 托管进程的用户帐户授予私钥的适当权限。 这将由 Setup.bat 脚本中的最后步骤来完成。

echo ************
echo setting privileges on server certificates
echo ************
for /F "delims=" %%i in ('"%ProgramFiles%\ServiceModelSampleTools\FindPrivateKey.exe" My LocalMachine -n CN^=%SERVER_NAME% -a') do set PRIVATE_KEY_FILE=%%i
set WP_ACCOUNT=NT AUTHORITY\NETWORK SERVICE
(ver | findstr /C:"5.1") && set WP_ACCOUNT=%COMPUTERNAME%\ASPNET
echo Y|cacls.exe "%PRIVATE_KEY_FILE%" /E /G "%WP_ACCOUNT%":R
iisreset
设置、生成和运行示例
  1. 请确保您已经为 Windows Communication Foundation 示例执行了 One-Time 设置程序

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

  3. 若要在单台计算机配置或跨计算机配置中运行示例,请使用以下说明。

在同一计算机上运行示例
  1. 使用管理员权限从 Visual Studio 命令提示符内的示例安装文件夹运行 Setup.bat。 这会安装运行示例所需的所有证书。

    注释

    Setup.bat 批处理文件设计为通过 Visual Studio 命令提示符运行。 在 Visual Studio 命令提示中设置的 PATH 环境变量指向包含 Setup.bat 脚本所需的可执行文件的目录。 完成示例后,请务必运行 Cleanup.bat 来删除证书。 其他安全示例使用相同的证书。

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

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

跨计算机运行示例
  1. 在服务计算机上创建目录。 使用 Internet Information Services (IIS) 管理工具为此目录创建名为 servicemodelsamples 的虚拟应用程序。

  2. 将服务程序文件从 \inetpub\wwwroot\servicemodelsamples 复制到服务计算机上的虚拟目录。 确保复制 \bin 子目录中的文件。 此外,将 Setup.bat、Cleanup.bat和 ImportClientCert.bat 文件复制到服务计算机。

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

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

  5. 在服务器上,使用管理员权限在 Visual Studio 的开发人员命令提示符下运行 setup.bat service 。 如果采用 setup.bat 参数运行 service,则使用计算机的完全限定域名创建一个服务证书,并将此服务证书导出到名为 Service.cer 的文件中。

  6. 编辑 Web.config,以便在 > 中的 属性中反映新证书名称,该名称与计算机的完全限定域名相同。

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

  8. 在客户端上,使用管理员权限在 Visual Studio 的开发人员命令提示符下运行 setup.bat client 。 使用 setup.bat 参数运行 client 将创建名为 client.com 的客户端证书,并将客户端证书导出到名为Client.cer的文件。

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

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

  11. 在客户端上运行 ImportServiceCert.bat。 这会将服务证书从 Service.cer 文件导入 CurrentUser - TrustedPeople 存储中。

  12. 在服务器上,运行 ImportClientCert.bat,这会将客户端证书从 Client.cer 文件导入 LocalMachine - TrustedPeople 存储。

  13. 在客户端计算机上,从命令提示符窗口启动 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