Udostępnij za pośrednictwem


Obsługa tokenów

W przykładzie Tokeny pomocnicze pokazano, jak dodać dodatkowe tokeny do komunikatu korzystającego z usługi WS-Security. W przykładzie dodano binarny token zabezpieczający X.509 oprócz tokenu zabezpieczającego nazwy użytkownika. Token jest przekazywany w nagłówku komunikatu WS-Security od klienta do usługi, a część komunikatu jest podpisana przy użyciu klucza prywatnego skojarzonego z tokenem zabezpieczającym X.509, aby udowodnić posiadanie certyfikatu X.509 odbiorcy. Jest to przydatne w przypadku, gdy istnieje wymóg posiadania wielu oświadczeń skojarzonych z komunikatem w celu uwierzytelnienia lub autoryzowania nadawcy. Usługa implementuje kontrakt, który definiuje wzorzec komunikacji typu żądanie-odpowiedź.

Demonstracje

W przykładzie pokazano:

  • Jak klient może przekazać dodatkowe tokeny zabezpieczające do usługi.

  • Jak serwer może uzyskiwać dostęp do oświadczeń skojarzonych z dodatkowymi tokenami zabezpieczającymi.

  • Sposób użycia certyfikatu X.509 serwera do ochrony klucza symetrycznego używanego do szyfrowania i podpisu komunikatów.

Uwaga

Procedura instalacji i instrukcje kompilacji dla tego przykładu znajdują się na końcu tego tematu.

Klient uwierzytelnia się przy użyciu tokenu nazwy użytkownika i pomocniczego tokenu zabezpieczającego X.509

Usługa uwidacznia pojedynczy punkt końcowy do komunikowania się, który jest programowo tworzony przy użyciu BindingHelper klas i EchoServiceHost . Punkt końcowy składa się z adresu, powiązania i kontraktu. Powiązanie jest konfigurowane przy użyciu powiązania niestandardowego przy użyciu metod SymmetricSecurityBindingElement i HttpTransportBindingElement. W tym przykładzie ustawiono SymmetricSecurityBindingElement certyfikat X.509 usługi w celu ochrony klucza symetrycznego podczas transmisji i przekazywania UserNameToken elementu wraz z obsługą X509SecurityToken w nagłówku komunikatu WS-Security. Klucz symetryczny służy do szyfrowania treści komunikatu i tokenu zabezpieczającego nazwy użytkownika. Token pomocniczy jest przekazywany jako dodatkowy binarny token zabezpieczający w nagłówku komunikatu WS-Security. Autentyczność tokenu pomocniczego jest udowodniona przez podpisanie części komunikatu przy użyciu klucza prywatnego skojarzonego z pomocniczym tokenem zabezpieczającym 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);
}

Zachowanie określa poświadczenia usługi, które mają być używane do uwierzytelniania klienta, a także informacje o certyfikacie X.509 usługi. W przykładzie użyto CN=localhost nazwy podmiotu w certyfikacie X.509 usługi.

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

Kod usługi:

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

Punkt końcowy klienta jest skonfigurowany w podobny sposób do punktu końcowego usługi. Klient używa tej samej BindingHelper klasy do utworzenia powiązania. Pozostała część konfiguracji znajduje się w Client klasie . Klient ustawia informacje o tokenie zabezpieczającym nazwy użytkownika, pomocniczy token zabezpieczający X.509 i informacje o certyfikacie X.509 usługi w kodzie konfiguracji kolekcji zachowań punktu końcowego klienta.

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

Wyświetlanie informacji o obiektach wywołujących

Aby wyświetlić informacje elementu wywołującego, możesz użyć elementu ServiceSecurityContext.Current.AuthorizationContext.ClaimSets , jak pokazano w poniższym kodzie. Zawiera ServiceSecurityContext.Current.AuthorizationContext.ClaimSets oświadczenia autoryzacji skojarzone z bieżącym obiektem wywołującym. Oświadczenia te są dostarczane automatycznie przez program Windows Communication Foundation (WCF) dla każdego tokenu otrzymanego w komunikacie.

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

Uruchamianie przykładu

Po uruchomieniu przykładu klient najpierw wyświetli monit o podanie nazwy użytkownika i hasła dla tokenu nazwy użytkownika. Pamiętaj, aby podać poprawne wartości dla konta systemowego, ponieważ program WCF w usłudze mapuje wartości podane w tokenie nazwy użytkownika do tożsamości dostarczonej przez system. Następnie klient wyświetla odpowiedź z usługi. Naciśnij klawisz ENTER w oknie klienta, aby zamknąć klienta.

Konfigurowanie pliku wsadowego

Plik wsadowy Setup.bat dołączony do tego przykładu umożliwia skonfigurowanie serwera z odpowiednimi certyfikatami w celu uruchamiania aplikacji hostowanej przez usługi Internet Information Services (IIS), która wymaga zabezpieczeń opartych na certyfikatach serwera. Ten plik wsadowy musi zostać zmodyfikowany w celu pracy między maszynami lub pracy w przypadku nieobsługiwanej.

Poniżej przedstawiono krótkie omówienie różnych sekcji plików wsadowych, dzięki czemu można je zmodyfikować w celu uruchomienia w odpowiedniej konfiguracji.

Tworzenie certyfikatu klienta

Następujące wiersze z pliku wsadowego Setup.bat tworzą certyfikat klienta do użycia. Zmienna %CLIENT_NAME% określa temat certyfikatu klienta. W tym przykładzie użyto nazwy podmiotu "client.com".

Certyfikat jest przechowywany w magazynie Moje (osobiste) w CurrentUser lokalizacji magazynu.

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

Instalowanie certyfikatu klienta w zaufanym magazynie serwera

Następujący wiersz w pliku wsadowym Setup.bat kopiuje certyfikat klienta do magazynu zaufanych osób serwera. Ten krok jest wymagany, ponieważ certyfikaty generowane przez Makecert.exe nie są niejawnie zaufane przez system serwera. Jeśli masz już certyfikat, który jest root w zaufanym certyfikacie głównym klienta — na przykład certyfikat wystawiony przez firmę Microsoft — ten krok wypełniania magazynu certyfikatów klienta przy użyciu certyfikatu serwera nie jest wymagany.

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

Tworzenie certyfikatu serwera

Następujące wiersze z pliku wsadowego Setup.bat tworzą certyfikat serwera do użycia. Zmienna %SERVER_NAME% określa nazwę serwera. Zmień tę zmienną, aby określić własną nazwę serwera. Wartość domyślna w tym pliku wsadowym to localhost.

Certyfikat jest przechowywany w magazynie My (Personal) w lokalizacji magazynu LocalMachine. Certyfikat jest przechowywany w magazynie LocalMachine dla usług hostowanych przez usługi IIS. W przypadku usług hostowanych samodzielnie należy zmodyfikować plik wsadowy tak, aby przechowywał certyfikat serwera w lokalizacji magazynu CurrentUser, zastępując ciąg LocalMachine bieżącymużytkownikiem.

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

Instalowanie certyfikatu serwera w zaufanym magazynie certyfikatów klienta

Następujące wiersze w pliku wsadowym Setup.bat skopiuj certyfikat serwera do magazynu zaufanych osób klienta. Ten krok jest wymagany, ponieważ certyfikaty generowane przez Makecert.exe nie są niejawnie zaufane przez system kliencki. Jeśli masz już certyfikat, który jest root w zaufanym certyfikacie głównym klienta — na przykład certyfikat wystawiony przez firmę Microsoft — ten krok wypełniania magazynu certyfikatów klienta przy użyciu certyfikatu serwera nie jest wymagany.

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

Włączanie dostępu do klucza prywatnego certyfikatu

Aby umożliwić dostęp do klucza prywatnego certyfikatu z usługi hostowanej przez usługi IIS, konto użytkownika, w ramach którego działa proces hostowany przez usługi IIS, musi mieć odpowiednie uprawnienia dla klucza prywatnego. Jest to realizowane przez ostatnie kroki skryptu 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
Aby skonfigurować, skompilować i uruchomić przykład
  1. Upewnij się, że wykonano procedurę instalacji jednorazowej dla przykładów programu Windows Communication Foundation.

  2. Aby skompilować rozwiązanie, postępuj zgodnie z instrukcjami w temacie Building the Windows Communication Foundation Samples (Tworzenie przykładów programu Windows Communication Foundation).

  3. Aby uruchomić przykład w konfiguracji pojedynczej lub między maszynami, skorzystaj z poniższych instrukcji.

Aby uruchomić przykład na tej samej maszynie
  1. Uruchom Setup.bat z folderu przykładowej instalacji w wierszu polecenia programu Visual Studio uruchom polecenie z uprawnieniami administratora. Spowoduje to zainstalowanie wszystkich certyfikatów wymaganych do uruchomienia przykładu.

    Uwaga

    Plik wsadowy Setup.bat jest przeznaczony do uruchamiania z poziomu wiersza polecenia programu Visual Studio. Zmienna środowiskowa PATH ustawiona w wierszu polecenia programu Visual Studio wskazuje katalog zawierający pliki wykonywalne wymagane przez skrypt Setup.bat. Pamiętaj, aby usunąć certyfikaty, uruchamiając Cleanup.bat po zakończeniu pracy z przykładem. Inne przykłady zabezpieczeń używają tych samych certyfikatów.

  2. Uruchom Client.exe z \client\bin. Działanie klienta jest wyświetlane w aplikacji konsolowej klienta.

  3. Jeśli klient i usługa nie mogą się komunikować, zobacz Rozwiązywanie problemów Wskazówki dla przykładów programu WCF.

Aby uruchomić przykład między maszynami
  1. Utwórz katalog na maszynie usługi. Utwórz aplikację wirtualną o nazwie servicemodelsamples dla tego katalogu przy użyciu narzędzia do zarządzania usługami Internet Information Services (IIS).

  2. Skopiuj pliki programu usługi z folderu \inetpub\wwwroot\servicemodelsamples do katalogu wirtualnego na maszynie usługi. Upewnij się, że skopiujesz pliki w podkatalogu \bin. Skopiuj również pliki Setup.bat, Cleanup.bat i ImportClientCert.bat na maszynę usługi.

  3. Utwórz katalog na maszynie klienckiej dla plików binarnych klienta.

  4. Skopiuj pliki programu klienckiego do katalogu klienta na komputerze klienckim. Skopiuj również pliki Setup.bat, Cleanup.bat i ImportServiceCert.bat do klienta.

  5. Na serwerze uruchom polecenie setup.bat service w wierszu polecenia dla deweloperów dla programu Visual Studio otwarte z uprawnieniami administratora. Uruchomienie setup.bat z argumentem service powoduje utworzenie certyfikatu usługi z w pełni kwalifikowaną nazwą domeny maszyny i eksportuje certyfikat usługi do pliku o nazwie Service.cer.

  6. Edytuj plik Web.config, aby odzwierciedlić nową nazwę certyfikatu (w atrybucie findValue serviceCertificate>), która jest taka sama jak w <pełni kwalifikowana nazwa domeny maszyny.

  7. Skopiuj plik Service.cer z katalogu usługi do katalogu klienta na komputerze klienckim.

  8. Na kliencie uruchom polecenie setup.bat client w wierszu polecenia dla deweloperów dla programu Visual Studio otwarte z uprawnieniami administratora. Uruchomienie setup.bat z argumentem client powoduje utworzenie certyfikatu klienta o nazwie client.com i wyeksportowanie certyfikatu klienta do pliku o nazwie Client.cer.

  9. W pliku Client.exe.config na komputerze klienckim zmień wartość adresu punktu końcowego, aby odpowiadała nowemu adresowi usługi. W tym celu należy zastąpić hosta lokalnego w pełni kwalifikowaną nazwą domeny serwera.

  10. Skopiuj plik Client.cer z katalogu klienta do katalogu usługi na serwerze.

  11. Na kliencie uruchom ImportServiceCert.bat. Spowoduje to zaimportowanie certyfikatu usługi z pliku Service.cer do magazynu CurrentUser — Trusted Osoby.

  12. Na serwerze uruchom ImportClientCert.bat. Spowoduje to zaimportowanie certyfikatu klienta z pliku Client.cer do magazynu LocalMachine — Trusted Osoby.

  13. Na komputerze klienckim uruchom Client.exe z okna wiersza polecenia. Jeśli klient i usługa nie mogą się komunikować, zobacz Rozwiązywanie problemów Wskazówki dla przykładów programu WCF.

Aby wyczyścić po próbce
  • Uruchom Cleanup.bat w folderze samples po zakończeniu uruchamiania przykładu.

Uwaga

Ten skrypt nie usuwa certyfikatów usługi na kliencie podczas uruchamiania tego przykładu na maszynach. Jeśli uruchomiono przykłady programu WCF korzystające z certyfikatów na różnych maszynach, wyczyść certyfikaty usługi, które zostały zainstalowane w magazynie CurrentUser — Trusted Osoby Store. W tym celu użyj następującego polecenia: certmgr -del -r CurrentUser -s TrustedPeople -c -n <Fully Qualified Server Machine Name> na przykład: certmgr -del -r CurrentUser -s TrustedPeople -c -n server1.contoso.com.