Поделиться через


Вспомогательные маркеры

В примере вспомогательных маркеров показано, как добавить дополнительные маркеры в сообщение, использующее WS-Security. В примере добавляется двоичный маркер безопасности X.509 в дополнение к маркеру безопасности имени пользователя. Маркер передается в заголовке сообщения WS-Security от клиента к службе, и часть сообщения подписана закрытым ключом, связанным с маркером безопасности X.509, для подтверждения получателю владения сертификатом X.509. Это полезно в случае, если требуется наличие нескольких утверждений, связанных с сообщением для проверки подлинности или авторизации отправителя. Служба реализует контракт, определяющий шаблон связи с запросом и ответом.

Демонстрирует

В примере показано следующее:

  • Как клиент может передавать дополнительные маркеры безопасности в службу.

  • Как сервер может получить доступ к утверждениям, связанным с дополнительными маркерами безопасности.

  • Как сертификат X.509 сервера используется для защиты симметричного ключа, используемого для шифрования сообщений и подписи.

Замечание

Процедура установки и инструкции по сборке для этого примера находятся в конце этого раздела.

Аутентификация клиента с токеном имени пользователя и поддерживающим токеном безопасности X.509

Служба предоставляет единую конечную точку для обмена данными, которая создается программным способом с помощью BindingHelper и EchoServiceHost классов. Конечная точка состоит из адреса, привязки и контракта. Привязка настраивается с помощью пользовательской привязки с использованием SymmetricSecurityBindingElement и HttpTransportBindingElement. В этом примере SymmetricSecurityBindingElement устанавливается для использования сертификата X.509 службы, чтобы защитить симметричный ключ во время передачи и передать UserNameToken вместе с поддерживающим X509SecurityToken в заголовке сообщения WS-Security. Симметричный ключ используется для шифрования текста сообщения и маркера безопасности имени пользователя. Вспомогательный маркер передается в качестве дополнительного двоичного маркера безопасности в заголовке сообщения 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 сервиса. В примере используется CN=localhost в качестве имени субъекта в сертификате X.509 для службы.

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 в службе сопоставляет значения, указанные в маркере имени пользователя, в удостоверение, предоставленное системой. После этого клиент отображает ответ от службы. Нажмите клавишу ВВОД в окне клиента, чтобы завершить работу клиента.

Настройка пакетного файла

Пакетный файл Setup.bat, включенный в этот пример, позволяет настроить сервер с соответствующими сертификатами для запуска размещенного приложения служб 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, не являются неявно доверенными системой сервера. Если у вас уже есть сертификат, основанный на доверенном корневом сертификате клиента, например, выданный корпорацией Майкрософт, этот шаг, в котором хранилище сертификатов клиента заполняется серверным сертификатом, не требуется.

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.

Сертификат хранится в хранилище «My store (Личном хранилище)» в расположении LocalMachine. Сертификат хранится в хранилище LocalMachine для служб, размещенных в IIS. Для самостоятельно размещаемых сервисов следует изменить пакетный файл, чтобы сохранить сертификат сервера в расположении хранилища 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, не являются неявно доверенными клиентской системой. Если у вас уже есть сертификат, основанный на доверенном корневом сертификате клиента, например, выданный корпорацией Майкрософт, этот шаг, в котором хранилище сертификатов клиента заполняется серверным сертификатом, не требуется.

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. Убедитесь, что вы выполнили процедуру настройкиOne-Time для примеров Windows Communication Foundation.

  2. Чтобы создать решение, следуйте инструкциям по созданию примеров Windows Communication Foundation.

  3. Чтобы запустить пример в конфигурации с одним или несколькими компьютерами, выполните следующие инструкции.

Запуск примера на том же компьютере
  1. Запустите Setup.bat из примера папки установки в командной строке Visual Studio с правами администратора. При этом устанавливаются все сертификаты, необходимые для выполнения примера.

    Замечание

    Пакетный файл Setup.bat предназначен для запуска из командной строки Visual Studio. Переменная среды PATH в командной строке Visual Studio указывает на каталог, содержащий исполняемые файлы, необходимые скрипту Setup.bat. Не забудьте удалить сертификаты, выполнив Cleanup.bat после завершения работы с примером. Другие примеры безопасности используют те же сертификаты.

  2. Запустите Client.exe из \client\bin. Действие клиента отображается в клиентском консольном приложении.

  3. Если клиент и служба не могут взаимодействовать, см. рекомендации по устранению неисправностей для примеров WCF.

Запуск примера на разных компьютерах
  1. Создайте каталог на компьютере службы. Создайте виртуальное приложение с именем servicemodelsamples для этого каталога с помощью средства управления службами IIS.

  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, чтобы отразить новое имя сертификата (в findValue атрибуте <в serviceCertificate>), которое совпадает с полным доменным именем компьютера.

  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.