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
Upewnij się, że wykonano procedurę instalacji jednorazowej dla przykładów programu Windows Communication Foundation.
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).
Aby uruchomić przykład w konfiguracji pojedynczej lub między maszynami, skorzystaj z poniższych instrukcji.
Aby uruchomić przykład na tej samej maszynie
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.
Uruchom Client.exe z \client\bin. Działanie klienta jest wyświetlane w aplikacji konsolowej klienta.
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
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).
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.
Utwórz katalog na maszynie klienckiej dla plików binarnych klienta.
Skopiuj pliki programu klienckiego do katalogu klienta na komputerze klienckim. Skopiuj również pliki Setup.bat, Cleanup.bat i ImportServiceCert.bat do klienta.
Na serwerze uruchom polecenie
setup.bat service
w wierszu polecenia dla deweloperów dla programu Visual Studio otwarte z uprawnieniami administratora. Uruchomieniesetup.bat
z argumentemservice
powoduje utworzenie certyfikatu usługi z w pełni kwalifikowaną nazwą domeny maszyny i eksportuje certyfikat usługi do pliku o nazwie Service.cer.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.Skopiuj plik Service.cer z katalogu usługi do katalogu klienta na komputerze klienckim.
Na kliencie uruchom polecenie
setup.bat client
w wierszu polecenia dla deweloperów dla programu Visual Studio otwarte z uprawnieniami administratora. Uruchomieniesetup.bat
z argumentemclient
powoduje utworzenie certyfikatu klienta o nazwie client.com i wyeksportowanie certyfikatu klienta do pliku o nazwie Client.cer.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.
Skopiuj plik Client.cer z katalogu klienta do katalogu usługi na serwerze.
Na kliencie uruchom ImportServiceCert.bat. Spowoduje to zaimportowanie certyfikatu usługi z pliku Service.cer do magazynu CurrentUser — Trusted Osoby.
Na serwerze uruchom ImportClientCert.bat. Spowoduje to zaimportowanie certyfikatu klienta z pliku Client.cer do magazynu LocalMachine — Trusted Osoby.
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
.