支援令牌範例示範如何將其他令牌新增至使用 WS-Security 的訊息。 此範例除了用戶名稱安全性令牌之外,還新增 X.509 二進位安全性令牌。 令牌會從客戶端傳遞至服務 WS-Security 訊息標頭,而訊息的一部分會使用與 X.509 安全性令牌相關聯的私鑰簽署,以證明擁有 X.509 憑證給接收者。 當要求有多個與訊息相關聯的宣告來驗證或授權寄件者時,這非常有用。 服務會實作定義要求-回復通訊模式的合約。
演示
此範例示範:
用戶端如何將其他安全性令牌傳遞至服務。
伺服器如何存取與其他安全性令牌相關聯的宣告。
如何使用伺服器的 X.509 憑證來保護用於訊息加密和簽章的對稱密鑰。
備註
此範例的安裝程式和建置指示位於本主題結尾。
用戶端使用使用者名稱令牌和支援 X.509 安全性令牌進行驗證
服務會公開單一端點,以程序設計方式使用 BindingHelper
和 EchoServiceHost
類別來通訊。 端點是由位址、系結和合約所組成。 系結是使用 SymmetricSecurityBindingElement
和 HttpTransportBindingElement
來設定自定義系結。 這個範例將 SymmetricSecurityBindingElement
設定為在傳輸期間使用服務 X.509 憑證來保護對稱金鑰,並在 WS-Security 訊息標頭中傳遞 UserNameToken
及其支援的 X509SecurityToken
。 對稱金鑰是用來加密訊息本文和使用者名稱安全性令牌。 支援令牌會以 WS-Security 訊息標頭中的其他二進位安全性令牌的形式傳遞。 支援令牌的真實性是藉由使用與支援 X.509 安全性令牌相關聯的私鑰簽署訊息的一部分來證明。
public static Binding CreateMultiFactorAuthenticationBinding()
{
HttpTransportBindingElement httpTransport = new HttpTransportBindingElement();
// the message security binding element will be configured to require 2 tokens:
// 1) A username-password encrypted with the service token
// 2) A client certificate used to sign the message
// Instantiate a binding element that will require the username/password token in the message (encrypted with the server cert)
SymmetricSecurityBindingElement messageSecurity = SecurityBindingElement.CreateUserNameForCertificateBindingElement();
// Create supporting token parameters for the client X509 certificate.
X509SecurityTokenParameters clientX509SupportingTokenParameters = new X509SecurityTokenParameters();
// Specify that the supporting token is passed in message send by the client to the service
clientX509SupportingTokenParameters.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;
// Turn off derived keys
clientX509SupportingTokenParameters.RequireDerivedKeys = false;
// Augment the binding element to require the client's X509 certificate as an endorsing token in the message
messageSecurity.EndpointSupportingTokenParameters.Endorsing.Add(clientX509SupportingTokenParameters);
// Create a CustomBinding based on the constructed security binding element.
return new CustomBinding(messageSecurity, httpTransport);
}
行為會指定要用於客戶端驗證的服務認證,以及服務 X.509 憑證的相關信息。 此範例會使用 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 會將用戶名稱令牌中提供的值對應至系統所提供的身分識別。 之後,客戶端會顯示來自服務的回應。 在客戶端視窗中按 ENTER 鍵以關閉用戶端。
設定批次處理檔
此範例隨附的 Setup.bat 批處理檔可讓您設定具有相關憑證的伺服器,以執行需要伺服器證書式安全性的 Internet Information Services (IIS) 裝載應用程式。 必須修改此批次檔案,才能在不同電腦上運作,或在非托管的情況下運作。
下列提供批處理檔不同區段的簡短概觀,以便修改它們以在適當的設定中執行。
建立客戶端憑證
Setup.bat 批處理檔中的下列幾行會建立要使用的用戶端憑證。 變數 %CLIENT_NAME%
會指定客戶端憑證的主體。 此範例使用 「client.com」 作為主體名稱。
憑證會儲存在 CurrentUser
存放區位置下的「個人」(我的)存放區中。
echo ************
echo making client cert
echo ************
makecert.exe -sr CurrentUser -ss MY -a sha1 -n CN=%CLIENT_NAME% -sky exchange -pe
將客戶端憑證安裝到伺服器的信任存放區
Setup.bat 批處理檔中的下一行會將客戶端憑證複製到伺服器的受信任人員存放區。 這是必要步驟,因為伺服器系統不會隱含信任 Makecert.exe 所產生的憑證。 如果您已經有根植於用戶端受信任的根證書的憑證,例如,Microsoft 發行的憑證,則不需要執行使用伺服器證書填入用戶端證書存儲的此步驟。
echo ************
echo copying client cert to server's CurrentUserstore
echo ************
certmgr.exe -add -r CurrentUser -s My -c -n %CLIENT_NAME% -r LocalMachine -s TrustedPeople
建立伺服器證書
Setup.bat 批處理檔中的下列幾行會建立要使用的伺服器證書。 變數 %SERVER_NAME%
會指定伺服器名稱。 變更此變數以指定您自己的伺服器名稱。 此批處理檔中的預設值為localhost。
憑證會儲存在 LocalMachine 存放區位置下的 My (Personal) 存放區中。 憑證會儲存在 IIS 裝載服務的 LocalMachine 存放區中。 針對自我裝載服務,您應該修改批處理檔,將伺服器證書儲存在 CurrentUser 存放區位置,方法是將字串 LocalMachine 取代為 CurrentUser。
echo ************
echo Server cert setup starting
echo %SERVER_NAME%
echo ************
echo making server cert
echo ************
makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=%SERVER_NAME% -sky exchange -pe
將伺服器證書安裝到用戶端的受信任證書存儲
Setup.bat 批處理檔中的下列幾行會將伺服器證書複製到用戶端信任的人員存放區。 這是必要步驟,因為客戶端系統不會隱含信任 Makecert.exe 所產生的憑證。 如果您已經有根植於用戶端受信任的根證書的憑證,例如,Microsoft 發行的憑證,則不需要執行使用伺服器證書填入用戶端證書存儲的此步驟。
echo ************
echo copying server cert to client's TrustedPeople store
echo ************certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r CurrentUser -s TrustedPeople
啟用憑證私鑰的存取
若要從 IIS 裝載的服務啟用憑證私鑰的存取權,該使用者帳戶必須被授與適當的權限,才能在 IIS 上執行的過程中使用私鑰。 這是由 Setup.bat 程式碼中的最後步驟來完成。
echo ************
echo setting privileges on server certificates
echo ************
for /F "delims=" %%i in ('"%ProgramFiles%\ServiceModelSampleTools\FindPrivateKey.exe" My LocalMachine -n CN^=%SERVER_NAME% -a') do set PRIVATE_KEY_FILE=%%i
set WP_ACCOUNT=NT AUTHORITY\NETWORK SERVICE
(ver | findstr /C:"5.1") && set WP_ACCOUNT=%COMPUTERNAME%\ASPNET
echo Y|cacls.exe "%PRIVATE_KEY_FILE%" /E /G "%WP_ACCOUNT%":R
iisreset
要設定、建置和執行範例,請執行以下步驟:
若要建置解決方案,請遵循 建置 Windows Communication Foundation 範例中的指示。
若要在單一或跨計算機組態中執行範例,請使用下列指示。
在同一台電腦上運行範例
從 Visual Studio 命令提示字元內範例安裝資料夾執行 Setup.bat 以系統管理員許可權執行。 這會安裝執行範例所需的所有憑證。
備註
Setup.bat 批處理文件的設計目的是要從 Visual Studio 命令提示字元執行。 Visual Studio 命令提示字元內設定的PATH環境變數會指向包含 Setup.bat 腳本所需可執行檔案的目錄。 範例完成後,請務必執行 Cleanup.bat 以移除憑證。 其他安全性範例使用相同的憑證。
從 \client\bin 啟動 Client.exe。 用戶端活動會顯示在用戶端主控台應用程式上。
如果客戶端和服務無法通訊,請參閱 WCF 範例 的疑難解答秘訣。
要在多台電腦上執行範例
在服務電腦上建立目錄。 使用 Internet Information Services (IIS) 管理工具,為此目錄建立名為 servicemodelsamples 的虛擬應用程式。
將服務程式檔案從 \inetpub\wwwroot\servicemodelsamples 複製到服務計算機上的虛擬目錄。 請確定您複製 \bin 子目錄中的檔案。 同時將 Setup.bat、Cleanup.bat和 ImportClientCert.bat 檔案複製到服務計算機。
在用戶端電腦上建立用戶端二進位檔的目錄。
將用戶端程式檔案複製到用戶端電腦上的客戶端目錄。 同時將 Setup.bat、Cleanup.bat和 ImportServiceCert.bat 檔案複製到用戶端。
在伺服器上,以系統管理員許可權開啟的 Visual Studio 開發人員命令提示字元中運行
setup.bat service
。 使用setup.bat
和service
參數執行會建立具有計算機完整域名的服務憑證,並將服務憑證匯出至名為 Service.cer 的檔案。編輯 Web.config 以反映新憑證名稱(在
findValue
<serviceCertificate> 中的屬性),該名稱與機器的完整網域名稱相同。將Service.cer檔案從服務目錄複製到用戶端電腦上的客戶端目錄。
在用戶端上,以系統管理員許可權開啟的 Visual Studio 開發人員命令提示字元中執行
setup.bat client
。 使用setup.bat
自變數執行client
會建立名為 client.com 的用戶端憑證,並將客戶端憑證導出至名為 Client.cer 的檔案。在用戶端電腦上的 Client.exe.config 檔案中,變更端點的位址值,以符合您服務的新位址。 將localhost替換為伺服器的完整網域名稱。
將Client.cer檔案從客戶端目錄複製到伺服器上的服務目錄。
在用戶端上,執行 ImportServiceCert.bat。 這會將服務憑證從 Service.cer 檔案匯入 CurrentUser - TrustedPeople 存放區。
在伺服器上執行 ImportClientCert.bat,這會從 Client.cer 檔案將用戶端憑證匯入 LocalMachine - TrustedPeople 存放區。
在用戶端電腦上,從命令提示字元窗口啟動 Client.exe。 如果客戶端和服務無法通訊,請參閱 WCF 範例 的疑難解答秘訣。
在範例之後清除
- 在您完成執行範例之後,請在samples資料夾中執行 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
。