[WCF安全系列]談談WCF的客戶端認證[使用者名稱/密碼認證]

行者武松發表於2017-10-26

對於基於Internet的應用,基於使用者名稱和密碼的認證方式是最為常用的,而WCF為你提供了不同模式的使用者名稱認證方式。首先還是從使用者憑證的表示說起。

一、使用者名稱/密碼認證的三種模式

基於使用者名稱/密碼的使用者憑證通過型別UserNamePasswordClientCredential表示。而在ClientCredentials中,只讀屬性UserName表示這樣一個使用者憑證。你可以按照Windows憑證的方式為ChannelFactory<TChannel>或者ClientBase<TChannel>基於使用者名稱/密碼憑證。

   1: public class ClientCredentials
   2: {
   3:      //其他成員
   4:      public UserNamePasswordClientCredential UserName { get; }
   5: } 
   6: public sealed class UserNamePasswordClientCredential
   7: {
   8:     //其他成員
   9:     public string Password {get; set; }
  10:     public string UserName { get; set; }
  11: }

使用者名稱/密碼憑證在客戶端的設定很容易,但是我們關心的是服務端採用怎樣的機制來驗證這個憑證。WCF為你提供瞭如下三種方式來驗證憑證中使用者名稱是否和密碼相符:

  • Windows:將使用者名稱和密碼對映為Windows帳號和密碼,採用Windows認證;
  • MembershipProvider:利用配置的MembershipProvider驗證使用者名稱和密碼;
  • 自定義:通過繼承抽象類UsernamePasswordValidator,自定義使用者名稱/密碼驗證器進行驗證。

WCF通過列舉UserNamePasswordValidationMode定了上述三種使用者名稱/密碼認證模式。該列舉定義如下,其中Windows是預設選項。

   1: public enum UserNamePasswordValidationMode
   2: {
   3:     Windows,
   4:     MembershipProvider,
   5:     Custom
   6: }

上述三種認證模式的設定最終通過之前提到過的ServiceCredentials這一服務行為進行設定的。從下面的定義我們可以看出,ServiceCredentials定義了只讀屬性UserNameAuthentication用於基於使用者名稱/密碼認證的相關設定。屬性的型別為UserNamePasswordServiceCredential,定義其中的UserNamePasswordValidationMode屬性表示採用的認證模式。如果選擇了需要通過屬性MembershipProvider設定採用的MembershipProvider。如果選擇了Custom,則需要通過CustomUserNamePasswordValidator屬性指定你自定義的UserNamePasswordValidator物件。

   1: public class ServiceCredentials: SecurityCredentialsManager, IServiceBehavior
   2: {
   3:     //其他成員
   4:      public UserNamePasswordServiceCredential UserNameAuthentication { get; }
   5: }
   6: public sealed class UserNamePasswordServiceCredential
   7: {
   8:     //其他成員
   9:     public UserNamePasswordValidator CustomUserNamePasswordValidator { get; set; }
  10:     public MembershipProvider MembershipProvider { get; set; }
  11:     public UserNamePasswordValidationMode UserNamePasswordValidationMode { get; set; }
  12: }

接下來我們通過例項演示的方式來如何通過MembershipProvider進行基於使用者名稱/密碼認證,而對於自定義UserNamePasswordValidator的例項我會在介紹安全會話的時候進行演示。

二、例項演示:通過MembershipProvider進行使用者名稱/密碼的認證

Membership是ASP.NET中一個重要的模組,旨在進行基於使用者名稱/密碼的認證和對應的帳號管理。Membership採用策略設計模式,所有的API通過幾個靜態Membership類暴露出來,而相應的功能實現在具體的Membership提供者中。所有的提供者繼承自同一個抽象類MembershipProvider。ASP.NET提供了兩種型別的提供者:SqlMembershipProviderActiveDirectoryMembershipProvider。前者將使用者儲存於SQL Server資料庫中,而後者則直接建立在AD之上,本例項採用SqlMembershipProvider,在前面一個例項演示中,我們建立了以計算服務為場景的解決方案,現在我們直接沿用它。

我們首要的任務是在用於儲存帳戶資訊的SQL Server資料庫,為此你可以先在本地SQL
Server建立一個空的資料庫(假設起名為AspNetDb)。你接著需要在該資料庫中建立SqlMembershipProvider所需的資料表和相應的儲存過程。這些資料庫物件的建立,需要藉助aspnet_regsql.exe這個工具。你只需要以命令列的方式執行如下aspnet_regsql.exe(無需任何引數),相應的嚮導就會出現。

在嚮導彈出的前兩個窗體中保持預設設定,直接點選“下一步”後,會出現一個資料庫選擇窗體。此時你需要選擇我們剛剛建立的資料庫,點選“確認”後,相關的資料庫物件會為你建立出來。

這些建立出來的資料表可以同時服務於多個應用,所有每一個表中都具有一個名稱為ApplicationId的欄位來明確該條記錄對應的應用。而所有應用記錄維護在aspnet_Applications這麼一個表中。現在我們需要通過執行下面一段SQL指令碼在該表中新增一條表示我們應用的記錄。我們應用起名為MembershipAuthenticationDemo。

   1: INSERT INTO [aspnet_Applications]
   2:            ([ApplicationName]
   3:            ,[LoweredApplicationName]
   4:            ,[ApplicationId]
   5:            ,[Description])
   6: VALUES
   7:            (
   8:              `MembershipAuthenticationDemo`
   9:              ,`membershipauthenticationdemo`
  10:              ,NEWID()
  11:              ,``
  12:         )

現在資料庫方面已經準備就緒,我們接著來完成程式設計和配置方面的工作。我們不打算從新建立一個解決方案,而是直接對之前演示的例項進行改造。我們採用自我寄宿的方式,由於Membership隸屬於ASP.NET,所以我們需要新增System.Web.dll的引用,如果你採用的是.NET
Frameowrk 4.0(本例所示的配置也是基於該版本),你還需額外新增對System.Web.ApplicationServices.dll的引用。接下來,我們需要在服務寄宿方面所做的工作就是將下面一段配置整個拷貝到app.config中。

   1: <?xml version="1.0"?>
   2: <configuration>
   3:   <connectionStrings>
   4:     <add name="AspNetDb" connectionString="Server=.; Database=AspNetDb; Uid=sa; Pwd=password"/>
   5:   </connectionStrings>
   6:   <system.web>
   7:     <membership defaultProvider="myProvider">
   8:       <providers>
   9:         <add name="myProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" 
  10:              connectionStringName="AspNetDb" applicationName="MembershipAuthenticationDemo" requiresQuestionAndAnswer="false"/>
  11:       </providers>
  12:     </membership>
  13:   </system.web>
  14: <system.serviceModel>
  15:   <bindings>
  16:     <ws2007HttpBinding>
  17:       <binding name="userNameCredentialBinding">
  18:         <security mode="Message">
  19:           <message clientCredentialType="UserName"/>
  20:         </security>
  21:       </binding>
  22:     </ws2007HttpBinding>
  23:   </bindings>
  24:   <services>
  25:     <service name="Artech.WcfServices.Services.CalculatorService" behaviorConfiguration="membershipAuthentication">
  26:       <endpoint address="http://127.0.0.1/calculatorservice" binding="ws2007HttpBinding" bindingConfiguration="userNameCredentialBinding" contract="Artech.WcfServices.Contracts.ICalculator"/>
  27:     </service>
  28:   </services>
  29:   <behaviors>
  30:     <serviceBehaviors>
  31:       <behavior  name="membershipAuthentication">
  32:         <serviceCredentials>
  33:           <serviceCertificate storeLocation="LocalMachine" storeName ="My" x509FindType="FindBySubjectName" findValue="Jinnan-PC"/>
  34:           <userNameAuthentication userNamePasswordValidationMode="MembershipProvider" membershipProviderName="myProvider"/>
  35:         </serviceCredentials>
  36:       </behavior>
  37:     </serviceBehaviors>
  38:   </behaviors>
  39:   </system.serviceModel>
  40: </configuration>

考慮到有些人可能對ASP.NET下的Membership相關配置不太瞭解,在這裡我對上述這段配置進行以下簡單的說明。

  • 配置名稱為AspNetDb的連線字串連線的是我們剛剛建立的資料庫,並通過aspnet_regsql.exe工具在該資料庫中建立了所需的資料庫物件;
  • 表示Membership配置節的<system.web>/<membership>節點下配置了唯一的SqlMembershipProvider,配置名稱為myProvider。上面配置的連線字元創名稱AspNetDb配置在connectionStringName屬性中,意味著該SqlMembershipProvider會將我們建立的資料庫作為使用者帳號儲存;
  • 服務終結點採用WS2007HttpBinding,採用Message安全模式,客戶端憑證型別被設定為UserName
  • 服務應用了一個配置名稱為membershipAuthentication的服務行為,該行為中通過<serviceCertificate>節點設定了服務證照。在表示使用者名稱/密碼認證配置的<userNameAuthentication>節點中,將認證模式設定成MembershipProvider,而membershipProviderName屬性的值為我們在<system.web>/<membership>中設定的MembershipProvider的名稱。

到目前為止,在我們建立的資料庫中並沒有使用者帳戶記錄。為了演示認證的效果,我們必須建立相關使用者帳戶記錄。為了省事,我直接將相關的程式碼寫在了服務寄宿的程式碼中。如下面的程式碼片斷所示,在對服務進行寄宿之前,我通過呼叫Membership的靜態方法CreateUser建立了一個使用者名稱、密碼和Email分別為Zhansan、Pass@word和zhanshan@gmail.com的帳號。

   1: if (Membership.FindUsersByName("Zhansan").Count == 0)
   2: {
   3:     Membership.CreateUser("Zhansan", "Pass@word", "zhanshan@gmail.com");
   4: }
   5: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
   6: {
   7:     host.Open();
   8:     Console.Read();
   9: }

接下來我們需要對客戶端的配置進行相應的調整,整個配置內容如下面的XML片斷所示。對於這段配置有一點需要注意的是:終結點應用了一個名稱為peerTrustSvcCertValidation的行為,該行為中將服務證照認證模式設定成PeerTrust,所以你需要通過MMC證照管理單元的匯出/匯入功能將Jinnan-PC證照匯入到“受信任人(Trusted People)”儲存區

   1: <?xml version="1.0"?>
   2: <configuration>
   3:   <system.serviceModel>
   4:     <bindings>
   5:       <ws2007HttpBinding>
   6:         <binding name="userNameCredentialBinding">
   7:           <security mode="Message">
   8:             <message clientCredentialType="UserName"/>
   9:           </security>
  10:         </binding>
  11:       </ws2007HttpBinding>
  12:     </bindings>
  13:     <client>
  14:       <endpoint name="calculatorService" behaviorConfiguration="peerTrustSvcCertValidation"  address="http://127.0.0.1/calculatorservice" binding="ws2007HttpBinding" bindingConfiguration="userNameCredentialBinding" contract="Artech.WcfServices.Contracts.ICalculator">
  15:         <identity>
  16:           <certificateReference storeLocation="LocalMachine" storeName ="My" x509FindType="FindBySubjectName" findValue="Jinnan-PC"/>
  17:         </identity>
  18:       </endpoint>
  19:     </client>
  20:     <behaviors>
  21:       <endpointBehaviors>
  22:         <behavior name="peerTrustSvcCertValidation">
  23:           <clientCredentials>
  24:             <serviceCertificate>
  25:               <authentication certificateValidationMode="PeerTrust"/>
  26:             </serviceCertificate>
  27:           </clientCredentials>
  28:         </behavior>
  29:       </endpointBehaviors>
  30:     </behaviors>
  31:   </system.serviceModel>
  32: </configuration>

最後,我麼來編寫如下一段客戶端進行服務呼叫的程式。在下面的程式碼中,我進行了兩次服務呼叫。但是建立服務代理物件的ChannelFactory<ICalculator>被設定了不同的使用者名稱憑證。其中第一個是正確的使用者名稱和密碼,後一個卻指定了一個根本不存在的使用者名稱。

   1: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorService"))
   2: {
   3:     UserNamePasswordClientCredential credential = channelFactory.Credentials.UserName;
   4:     credential.UserName     = "Zhansan";
   5:     credential.Password     = "Pass@word";
   6:     ICalculator calculator  = channelFactory.CreateChannel();
   7:     calculator.Add(1, 2);
   8:     Console.WriteLine("服務呼叫成功...");
   9: }
  10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorService"))
  11: {
  12:     UserNamePasswordClientCredential credential = channelFactory.Credentials.UserName;
  13:     credential.UserName     = "lisi";
  14:     credential.Password     = "Pass@word";
  15:     ICalculator calculator  = channelFactory.CreateChannel();
  16:     try
  17:     {
  18:         calculator.Add(1, 2);
  19:     }
  20:     catch
  21:     {
  22:         Console.WriteLine("服務呼叫失敗...");
  23:     }
  24: }

輸出結果:

   1: 服務呼叫成功...
   2: 服務呼叫失敗...
作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的訊息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。


相關文章