Partilhar via


Tokens de suporte

O exemplo de Tokens de Suporte demonstra como adicionar tokens adicionais a uma mensagem que usa WS-Security. O exemplo adiciona um token de segurança binário X.509 além de um token de segurança de nome de usuário. O token é passado em um cabeçalho de mensagem WS-Security do cliente para o serviço e parte da mensagem é assinada com a chave privada associada ao token de segurança X.509 para provar a posse do certificado X.509 para o recetor. Isso é útil no caso em que há um requisito para ter várias declarações associadas a uma mensagem para autenticar ou autorizar o remetente. O serviço implementa um contrato que define um padrão de comunicação solicitação-resposta.

Demonstra

A amostra demonstra:

  • Como um cliente pode passar tokens de segurança adicionais para um serviço.

  • Como o servidor pode acessar declarações associadas a tokens de segurança adicionais.

  • Como o certificado X.509 do servidor é usado para proteger a chave simétrica usada para criptografia e assinatura de mensagens.

Nota

O procedimento de configuração e as instruções de compilação para este exemplo estão localizados no final deste tópico.

Cliente autentica com token de nome de usuário e suporte X.509 Security Token

O serviço expõe um único ponto de extremidade para comunicação que é criado programaticamente usando as BindingHelper classes and EchoServiceHost . O ponto de extremidade consiste em um endereço, uma vinculação e um contrato. A associação é configurada com uma associação personalizada usando SymmetricSecurityBindingElement e HttpTransportBindingElement. Este exemplo define o SymmetricSecurityBindingElement para usar um certificado X.509 de serviço para proteger a chave simétrica durante a transmissão e para passar um UserNameToken junto com o suporte X509SecurityToken em um cabeçalho de mensagem WS-Security. A chave simétrica é usada para criptografar o corpo da mensagem e o token de segurança do nome de usuário. O token de suporte é passado como um token de segurança binário adicional no cabeçalho da mensagem WS-Security. A autenticidade do token de suporte é comprovada assinando parte da mensagem com a chave privada associada ao token de segurança X.509 de suporte.

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);
}

O comportamento especifica as credenciais de serviço que devem ser usadas para autenticação de cliente e também informações sobre o certificado X.509 de serviço. O exemplo usa CN=localhost como um nome de assunto no certificado X.509 do serviço.

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();
}

Código de serviço:

[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;
                }
            }
        }
    }
}

O ponto de extremidade do cliente é configurado de maneira semelhante ao ponto de extremidade do serviço. O cliente usa a mesma BindingHelper classe para criar uma ligação. O resto da configuração está localizado na Client classe. O cliente define informações sobre o token de segurança de nome de usuário, o token de segurança X.509 de suporte e informações sobre o certificado X.509 de serviço no código de instalação para a coleção de comportamentos de ponto de extremidade do cliente.

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();
}

Exibindo informações dos chamadores

Para exibir as informações do chamador, você pode usar o ServiceSecurityContext.Current.AuthorizationContext.ClaimSets conforme mostrado no código a seguir. O ServiceSecurityContext.Current.AuthorizationContext.ClaimSets contém declarações de autorização associadas ao chamador atual. Essas declarações são fornecidas automaticamente pelo Windows Communication Foundation (WCF) para cada token recebido na mensagem.

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;
            }
        }
    }
}

Executando o exemplo

Quando você executa o exemplo, o cliente primeiro solicita que você forneça nome de usuário e senha para o token de nome de usuário. Certifique-se de fornecer valores corretos para sua conta do sistema, porque o WCF no serviço mapeia os valores fornecidos no token de nome de usuário para a identidade fornecida pelo sistema. Depois disso, o cliente exibe a resposta do serviço. Pressione ENTER na janela do cliente para desligar o cliente.

Arquivo em lote de instalação

O arquivo em lotes de Setup.bat incluído neste exemplo permite configurar o servidor com certificados relevantes para executar o aplicativo hospedado do IIS (Serviços de Informações da Internet) que requer segurança baseada em certificado de servidor. Esse arquivo em lotes deve ser modificado para funcionar entre máquinas ou para funcionar em um caso não hospedado.

A seguir apresentamos uma breve visão geral das diferentes seções dos arquivos em lote para que eles possam ser modificados para serem executados na configuração apropriada.

Criando o certificado do cliente

As linhas a seguir do arquivo em lotes Setup.bat criam o certificado de cliente a ser usado. A %CLIENT_NAME% variável especifica o assunto do certificado do cliente. Este exemplo usa "client.com" como o nome do assunto.

O certificado é armazenado em Minha loja (Pessoal) sob o local da CurrentUser loja.

echo ************
echo making client cert
echo ************
makecert.exe -sr CurrentUser -ss MY -a sha1 -n CN=%CLIENT_NAME% -sky exchange -pe

Instalando o certificado do cliente no armazenamento confiável do servidor

A linha a seguir no arquivo em lote Setup.bat copia o certificado do cliente para o armazenamento de pessoas confiáveis do servidor. Esta etapa é necessária porque os certificados gerados por Makecert.exe não são implicitamente confiáveis pelo sistema do servidor. Se você já tiver um certificado enraizado em um certificado raiz confiável do cliente, por exemplo, um certificado emitido pela Microsoft, esta etapa de preencher o armazenamento de certificados do cliente com o certificado do servidor não será necessária.

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

Criando o certificado do servidor

As linhas a seguir do arquivo em lotes Setup.bat criam o certificado do servidor a ser usado. A %SERVER_NAME% variável especifica o nome do servidor. Altere essa variável para especificar seu próprio nome de servidor. O padrão neste arquivo em lotes é localhost.

O certificado é armazenado em Meu armazenamento (Pessoal) no local de armazenamento LocalMachine. O certificado é armazenado no armazenamento LocalMachine para os serviços hospedados no IIS. Para serviços auto-hospedados, você deve modificar o arquivo em lotes para armazenar o certificado do servidor no local de armazenamento CurrentUser substituindo a cadeia de caracteres LocalMachine por 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

Instalando o certificado do servidor no armazenamento de certificados confiáveis do cliente

As linhas a seguir no arquivo em lote Setup.bat copiam o certificado do servidor para o armazenamento de pessoas confiáveis do cliente. Esta etapa é necessária porque os certificados gerados por Makecert.exe não são implicitamente confiáveis pelo sistema cliente. Se você já tiver um certificado enraizado em um certificado raiz confiável do cliente, por exemplo, um certificado emitido pela Microsoft, esta etapa de preencher o armazenamento de certificados do cliente com o certificado do servidor não será necessária.

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

Habilitando o acesso à chave privada do certificado

Para habilitar o acesso à chave privada do certificado do serviço hospedado no IIS, a conta de usuário sob a qual o processo hospedado no IIS está sendo executado deve receber permissões apropriadas para a chave privada. Isso é feito pelas últimas etapas no script 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
Para configurar, compilar e executar o exemplo
  1. Certifique-se de ter executado o procedimento de instalação única para os exemplos do Windows Communication Foundation.

  2. Para criar a solução, siga as instruções em Criando os exemplos do Windows Communication Foundation.

  3. Para executar o exemplo em uma configuração de máquina única ou cruzada, use as instruções a seguir.

Para executar a amostra na mesma máquina
  1. Execute Setup.bat a partir da pasta de instalação de exemplo dentro de um prompt de comando do Visual Studio executado com privilégios de administrador. Isso instala todos os certificados necessários para executar o exemplo.

    Nota

    O arquivo em lotes Setup.bat foi projetado para ser executado a partir de um prompt de comando do Visual Studio. A variável de ambiente PATH definida no prompt de comando do Visual Studio aponta para o diretório que contém executáveis exigidos pelo script Setup.bat. Certifique-se de remover os certificados executando Cleanup.bat quando terminar a amostra. Outros exemplos de segurança usam os mesmos certificados.

  2. Inicie Client.exe a partir de \client\bin. A atividade do cliente é exibida no aplicativo de console do cliente.

  3. Se o cliente e o serviço não puderem se comunicar, consulte Dicas de solução de problemas para exemplos de WCF.

Para executar a amostra em máquinas
  1. Crie um diretório na máquina de serviço. Crie um aplicativo virtual chamado servicemodelsamples para esse diretório usando a ferramenta de gerenciamento do IIS (Serviços de Informações da Internet).

  2. Copie os arquivos de programa de serviço de \inetpub\wwwroot\servicemodelsamples para o diretório virtual na máquina de serviço. Certifique-se de copiar os arquivos no subdiretório \bin. Copie também os arquivos Setup.bat, Cleanup.bat e ImportClientCert.bat para a máquina de serviço.

  3. Crie um diretório na máquina cliente para os binários do cliente.

  4. Copie os arquivos de programa do cliente para o diretório do cliente na máquina cliente. Copie também os arquivos Setup.bat, Cleanup.bat e ImportServiceCert.bat para o cliente.

  5. No servidor, execute setup.bat service em um prompt de comando do desenvolvedor para Visual Studio aberto com privilégios de administrador. A execução setup.bat com o service argumento cria um certificado de serviço com o nome de domínio totalmente qualificado da máquina e exporta o certificado de serviço para um arquivo chamado Service.cer.

  6. Edite Web.config para refletir o novo nome do certificado (no findValue atributo no <serviceCertificate>), que é o mesmo que o nome de domínio totalmente qualificado da máquina.

  7. Copie o arquivo Service.cer do diretório de serviço para o diretório do cliente na máquina cliente.

  8. No cliente, execute setup.bat client em um prompt de comando do desenvolvedor para Visual Studio aberto com privilégios de administrador. A execução setup.bat com o client argumento cria um certificado de cliente chamado client.com e exporta o certificado de cliente para um arquivo chamado Client.cer.

  9. No arquivo Client.exe.config na máquina cliente, altere o valor de endereço do ponto de extremidade para corresponder ao novo endereço do seu serviço. Faça isso substituindo localhost pelo nome de domínio totalmente qualificado do servidor.

  10. Copie o arquivo Client.cer do diretório do cliente para o diretório de serviço no servidor.

  11. No cliente, execute ImportServiceCert.bat. Isso importa o certificado de serviço do arquivo Service.cer para o repositório CurrentUser - TrustedPeople.

  12. No servidor, execute ImportClientCert.bat, Isso importa o certificado do cliente do arquivo Client.cer para o armazenamento LocalMachine - TrustedPeople.

  13. Na máquina cliente, inicie Client.exe a partir de uma janela de prompt de comando. Se o cliente e o serviço não puderem se comunicar, consulte Dicas de solução de problemas para exemplos de WCF.

Para limpar após a amostra
  • Execute Cleanup.bat na pasta de exemplos assim que terminar de executar o exemplo.

Nota

Esse script não remove certificados de serviço em um cliente ao executar este exemplo em máquinas. Se você tiver executado exemplos de WCF que usam certificados entre máquinas, certifique-se de limpar os certificados de serviço que foram instalados no repositório CurrentUser - TrustedPeople. Para fazer isso, use o seguinte comando: certmgr -del -r CurrentUser -s TrustedPeople -c -n <Fully Qualified Server Machine Name> Por exemplo: certmgr -del -r CurrentUser -s TrustedPeople -c -n server1.contoso.com.