WCF版的PetShop之三:實現分布式的Membership和上下文傳遞
系列文章導航:
WCF版的PetShop之三:實現分布式的Membership和上下文傳遞
通過上一篇了解了模塊內基本的層次劃分之后,接下來我們來聊聊PetShop中一些基本基礎功能的實現,以及一些設計、架構上的應用如何同WCF進行集成。本篇討論兩個問題:實現分布式的Membership和客戶端到服務端上下文(Context)的傳遞。
一、 如何實現用戶驗證
對登錄用戶的驗證是大部分應用所必需的,對于ASP.NET來說,用戶驗證及帳號管理實現在成員資格(Membership)模塊中。同ASP.NET的其他模塊一樣,微軟在設計Membership的時候,為了實現更好地可擴展性,采用了策略(Strategy)設計模式:將模塊相關的功能定義在被稱為Provider的抽象類型中,并通過繼承它提供具體的Provider。如果這些原生的Provider不能滿足你的需求,你也可以通過繼承該抽象的Provider,創建自定義的Provider。通過ASP.NET提供的配置,你可以很輕易地把自定義的Provider應用到你的應用之中。在一般情況下,最終的編程人員并不通過Provider調用相關的功能,而是通過一個外觀(Facade)類實現對相關功能的調用。
ASP.NET成員資格模塊的設計基本上可以通過下面的類圖1反映出來:最終的編程人員通過外觀類型(Façade Class)Membership調用成員資格相關的功能,比如用戶認證、用戶注冊、修改密碼等;Membership通過抽象類MembershipProvider提供所有的功能,至于最終的實現,則定義在一個個具體的MembershipProvider中。基于成員資格信息不同的存儲方式,ASP.NET提供了兩個原生的MembershipProvider:SqlMembershipProvider和ActiveDirectoryMembershipProvider,前者基于SQL Server數據庫,后者基于AD。如果這兩個MembershipProvider均不能滿足需求,我們還可以自定義MembershipProvider。
圖1 ASP.NET Membership 設計原理
我們的案例并不會部署于AD之中,所以不能使用ActiveDirectoryMembershipProvider;直接通過Web服務器進行數據庫的存取又不符合上述物理部署的要求(通過應用服務器進行數據庫訪問),所以SqlMembershipProvider也不能為我們所用。為此需要自定義MembershipProvider,通過WCF服務調用的形式提供成員資格所有功能的實現。我們將該自定義MembershipProvider稱為RemoteMembershipProvider。圖2揭示了RemoteMembershipProvider實現的原理:RemoteMembershipProvider通過調用WCF服務MembershipService提供對成員資格所有功能的實現;MembershipService則通過調用Membership實現服務;最終的實現還是落在了SqlMembershipProvider這個原生的MembershipProvider上。
圖2 RemoteMembershipProvider實現原理
1、服務契約和服務實現
首先來看看MembershipService實現的服務契約的定義。由于MembershipService最終是為RemoteMembershipProvider這個自定義MembershipProvider服務的,所以服務操作的定義是基于MembershipProvider的API定義。MembershipProvider包含兩種類型的成員:屬性和方法,簡單起見,我們可以為MembershipProvider每一個抽象方法定義一個匹配的服務操作;而對于所有屬性,完全采用服務端(應用服務器)的MembershipProvider相關屬性。在RemoteMembershipProvider初始化的時候通過調用MembershipService獲取所有服務端MembershipProvider的配置信息。為此,我們為MembershipProvider的所有屬性定義了一個數據契約:MembershipConfigData。在PetShop中,MembershipConfigData和服務契約一起定義在Infrastructures.Service.Interface項目中。
1: using System.Runtime.Serialization;
2: using System.Web.Security;
3: namespace Artech.PetShop.Infrastructures.Service.Interface
4: {
5: [DataContract(Namespace = "http://www.artech.com/")]
6: public class MembershipConfigData
7: {
8: [DataMember]
9: public string ApplicationName
10: { get; set; }
11:
12: [DataMember]
13: public bool EnablePasswordReset
14: { get; set; }
15:
16: [DataMember]
17: public bool EnablePasswordRetrieval
18: { get; set; }
19:
20: [DataMember]
21: public int MaxInvalidPasswordAttempts
22: { get; set; }
23:
24: [DataMember]
25: public int MinRequiredNonAlphanumericCharacters
26: { get; set; }
27:
28: [DataMember]
29: public int MinRequiredPasswordLength
30: { get; set; }
31:
32: [DataMember]
33: public int PasswordAttemptWindow
34: { get; set; }
35:
36: [DataMember]
37: public MembershipPasswordFormat PasswordFormat
38: { get; set; }
39:
40: [DataMember]
41: public string PasswordStrengthRegularExpression
42: { get; set; }
43:
44: [DataMember]
45: public bool RequiresQuestionAndAnswer
46: { get; set; }
47:
48: [DataMember]
49: public bool RequiresUniqueEmail
50: { get; set; }
51: }
52: }
在服務契約中,定義了一個額外的方法GetMembershipConfigData獲取服務端MembershipProvider的所有配置信息,而對于服務操作的定義,則與MembershipProvider同名抽象方法相對應。
1: using System.ServiceModel;
2: using System.Web.Security;
3: namespace Artech.PetShop.Infrastructures.Service.Interface
4: {
5: [ServiceContract(Namespace="http://www.artech.com/")]
6: public interface IMembershipService
7: {
8: [OperationContract]
9: bool ChangePassword(string username, string oldPassword, string newPassword);
10: [OperationContract]
11: bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer);
12: [OperationContract]
13: MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status);
14: [OperationContract]
15: bool DeleteUser(string username, bool deleteAllRelatedData);
16: [OperationContract]
17: MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords);
18: [OperationContract]
19: MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords);
20: [OperationContract]
21: MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords);
22: [OperationContract]
23: int GetNumberOfUsersOnline();
24: [OperationContract]
25: string GetPassword(string username, string answer);
26: [OperationContract(Name="GetUserByName")]
27: MembershipUser GetUser(string username, bool userIsOnline);
28: [OperationContract(Name="GetUserByID")]
29: MembershipUser GetUser(object providerUserKey, bool userIsOnline);
30: [OperationContract]
31: string GetUserNameByEmail(string email);
32: [OperationContract]
33: string ResetPassword(string username, string answer);
34: [OperationContract]
35: bool UnlockUser(string userName);
36: [OperationContract]
37: void UpdateUser(MembershipUser user);
38: [OperationContract]
39: bool ValidateUser(string username, string password);
40: [OperationContract]
41: MembershipConfigData GetMembershipConfigData();
42: }
43: }
服務的實現,則異常簡單,我們須要做的僅僅是通過Membership.Provider獲得當前的MembershipProvider,調用同名的屬性或方法即可。MembershipService定義在Infrastructures.Service中,定義如下:
1: using System.Web.Security;
2: using Artech.PetShop.Infrastructures.Service.Interface;
3: namespace Artech.PetShop.Infrastructures.Service
4: {
5: public class MembershipService : IMembershipService
6: {
7: #region IMembershipService Members
8:
9: public bool ChangePassword(string username, string oldPassword, string newPassword)
10: {
11: return Membership.Provider.ChangePassword(username, oldPassword, newPassword);
12: }
13:
14: public bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
15: {
16: return Membership.Provider.ChangePasswordQuestionAndAnswer(username, password, newPasswordQuestion, newPasswordAnswer);
17: }
18: //其他成員
19: public MembershipConfigData GetMembershipConfigData()
20: {
21: return new MembershipConfigData
22: {
23: ApplicationName = Membership.Provider.ApplicationName,
24: EnablePasswordReset = Membership.Provider.EnablePasswordReset,
25: EnablePasswordRetrieval = Membership.Provider.EnablePasswordRetrieval,
26: MaxInvalidPasswordAttempts = Membership.Provider.MaxInvalidPasswordAttempts,
27: MinRequiredNonAlphanumericCharacters = Membership.Provider.MinRequiredNonAlphanumericCharacters,
28: MinRequiredPasswordLength = Membership.Provider.MinRequiredPasswordLength,
29: PasswordAttemptWindow = Membership.Provider.PasswordAttemptWindow,
30: PasswordFormat = Membership.Provider.PasswordFormat,
31: PasswordStrengthRegularExpression = Membership.Provider.PasswordStrengthRegularExpression,
32: RequiresQuestionAndAnswer = Membership.Provider.RequiresQuestionAndAnswer,
33: RequiresUniqueEmail = Membership.Provider.RequiresUniqueEmail
34: };
35: }
36:
37: #endregion
38: }
39: }
2、RemoteMembershipProvider的實現
由于RemoteMembershipProvider完全通過調用WCF服務的方式提供對所有成員資格功能的實現,所以進行RemoteMembershipProvider配置時,配置相應的終結點就可以了。
1: xml version="1.0"?>
2: <configuration>
3: <system.web>
4: <membership defaultProvider="RemoteProvider">
5: <providers>
6: <add name="RemoteProvider" type="Artech.PetShop.Infrastructures.RemoteMembershipProvider,Artech.PetShop.Infrastructures, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" endpoint="membershipservice"/>
7: providers>
8: membership>
9: system.web>
10: <system.serviceModel>
11: <client>
12: <endpoint address="http://localhost/PetShop/Infrastructures/MembershipService.svc" behaviorConfiguration="petShopBehavior" binding="ws2007HttpBinding" contract="Artech.PetShop.Infrastructures.Service.Interface.IMembershipService" name="membershipservice"/>
13: client>
14: system.serviceModel>
15: configuration>
在RemoteMembershipProvider中,通過Initialize方法獲取配置的終結點名稱并創建服務代理。通過該代理調用GetMembershipConfigData操作獲取服務端MembershipProvider的配置信息,并對RemoteMembershipProvider進行初始化,RemoteMembershipProvider定義如下:
1: using System.Collections.Specialized;
2: using System.Configuration;
3: using System.Linq;
4: using System.Web.Security;
5: using Artech.PetShop.Common;
6: using Artech.PetShop.Infrastructures.Service.Interface;
7:
8: namespace Artech.PetShop.Infrastructures
9: {
10: public class RemoteMembershipProvider : MembershipProvider
11: {
12: private bool _enablePasswordReset;
13: private bool _enablePasswordRetrieval;
14: //其他字段成員
15:
16: public IMembershipService MembershipProxy
17: { get; private set; }
18:
19: public override int MaxInvalidPasswordAttempts
20: {
21: get { return this._maxInvalidPasswordAttempts; }
22: }
23:
24: //其他屬性成員
25: public override void Initialize(string name, NameValueCollection config)
26: {
27: if (!config.AllKeys.Contains<string>("endpoint"))
28: {
29: throw new ConfigurationErrorsException("Missing the mandatory \"endpoint\" configuraiton property.");
30: }
31:
32: this.MembershipProxy = ServiceProxyFactory.Create(config["endpoint"]);
33: base.Initialize(name, config);
34: MembershipConfigData configData = this.MembershipProxy.GetMembershipConfigData();
35: this.ApplicationName = configData.ApplicationName;
36: this._enablePasswordReset = configData.EnablePasswordReset;
37: this._enablePasswordRetrieval = configData.EnablePasswordRetrieval;
38: //......
39: }
40: }
41: }
對于其他抽象方法的實現,僅僅須要通過上面創建的服務代理,調用相應的服務操作即可。
注: 為了避免在服務操作調用后頻繁地進行服務代理的關閉(Close)和終止(Abort)操作,我們采用基于AOP的方式實現服務的調用,將這些操作封裝到一個自定義的RealProxy中,并通過ServiceProxyFactory創建該RealProxy的TransparentProxy。相關實現可以參考《WCF技術剖析(卷1)》第九章。
二、 上下文的共享及跨域傳遞
在進行基于N-Tier的應用開發中,我們往往需要在多個層次之間共享一些上下文(Context)信息,比如當前用戶的Profile信息;在進行遠程服務調用時,也經常需要進行上下文信息的跨域傳遞。比如在PetShop中,服務端進行審核(Audit)的時候,須要獲取當前登錄的用戶名。而登錄用戶名僅僅對于Web服務器可得,所以在每次服務調用的過程中,需要從客戶端向服務端傳遞。
1、ApplicationContext
基于上下文的共享,我創建了一個特殊的類型:ApplicationContext。ApplicationContext定義在Common項目中,簡單起見,直接將其定義成字典的形式。至于上下文數據的真正存儲,如果當前HttpContext存在,將其存儲與HttpSessionState中,否則將其存儲于CallContext中。
注: 由于CallConext將數據存儲于當前線程的TLS(Thread Local Storage)中,實際上HttpContext最終也采用這樣的存儲方式,所以ApplicaitonContext并不提供上下文信息跨線程的傳遞。
1: using System.Collections.Generic;
2: using System.Runtime.Remoting.Messaging;
3: using System.Web;
4: namespace Artech.PetShop.Common
5: {
6: public class ApplicationContext:Dictionary<string, object>
7: {
8: public const string ContextKey = "Artech.PetShop.Infrastructures.ApplicationContext";
9: public const string ContextHeaderLocalName = "ApplicationContext";
10: public const string ContextHeaderNamespace = "http://www.artech.com/petshop/";
11: public static ApplicationContext Current
12: {
13: get
14: {
15: if (HttpContext.Current != null)
16: {
17: if (HttpContext.Current.Session[ContextKey] == null)
18: {
19: HttpContext.Current.Session[ContextKey] = new ApplicationContext();
20: }
21:
22: return HttpContext.Current.Session[ContextKey] as ApplicationContext;
23: }
24:
25: if (CallContext.GetData(ContextKey) == null)
26: {
27: CallContext.SetData(ContextKey, new ApplicationContext());
28: }
29:
30: return CallContext.GetData(ContextKey) as ApplicationContext;
31: }
32: set
33: {
34: if (HttpContext.Current != null)
35: {
36: HttpContext.Current.Session[ContextKey] = value; ;
37: }
38: else
39: {
40: CallContext.SetData(ContextKey, value);
41: }
42: }
43: }
44: public string UserName
45: {
46: get
47: {
48: if (!this.ContainsKey("__UserName" ))
49: {
50: return string.Empty;
51: }
52:
53: return (string)this["__UserName"];
54: }
55: set
56: {
57: this["__UserName"] = value;
58: }
59: }
60: }
61: }
2、ApplicationContext在WCF服務調用中的傳遞
下面我們來介紹一下如何實現上下文信息在WCF服務調用過程中的“隱式”傳遞。在PetShop中,我們通過WCF的擴展實現此項功能。上下文傳遞的實現原理很簡單:在客戶端,將序列化后的當前上下文信息置于出棧(Outgoing)消息的SOAP報頭中,并為報頭指定一個名稱和命名空間;在服務端,在服務操作執行之前,通過報頭名稱和命名空間將上下文SOAP報頭從入棧(Incoming)消息中提取出來,進行反序列化,并將其設置成服務端當前的上下文。
所以,上下文的傳遞實際上包含兩個方面:SOAP報頭的添加和提取。我們通過兩個特殊的WCF對象來分別實現這兩個功能:ClientMessageInspector和CallContextInitializer,前者在客戶端將上下文信息封裝成SOAP報頭,并將其添加到出棧消息報頭集合;后者則在服務端實現對上下文SOAP報頭的提取和當前上下文的設置。關于ClientMessageInspector和CallContextInitializer,本書的下一卷關于客戶端和服務端處理流程,以及WCF擴展的部分,還將進行詳細的介紹。自定義的ClientMessageInspector和CallContextInitializer定義在Infrastructures項目中,下面是相關代碼實現:
ContextSendInspector:
1: using System.ServiceModel;
2: using System.ServiceModel.Channels;
3: using System.ServiceModel.Dispatcher;
4: using System.Threading;
5: using Artech.PetShop.Common;
6: namespace Artech.PetShop.Infrastructures
7: {
8: public class ContextSendInspector: IClientMessageInspector
9: {
10: public void AfterReceiveReply(ref Message reply, object correlationState)
11: {}
12:
13: public object BeforeSendRequest(ref Message request, IClientChannel channel)
14: {
15: if (string.IsNullOrEmpty(ApplicationContext.Current.UserName))
16: {
17: ApplicationContext.Current.UserName = Thread.CurrentPrincipal.Identity.Name;
18: }
19: request.Headers.Add(new MessageHeader(
20: ApplicationContext.Current).GetUntypedHeader(
21: ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace));
22:
23: return null;
24: }
25: }
26: }
ContextReceivalCallContextInitializer:
1: using System.ServiceModel;
2: using System.ServiceModel.Channels;
3: using System.ServiceModel.Dispatcher;
4: using Artech.PetShop.Common;
5: namespace Artech.PetShop.Infrastructures
6: {
7: public class ContextReceivalCallContextInitializer : ICallContextInitializer
8: {
9: public void AfterInvoke(object correlationState)
10: {
11: ApplicationContext.Current.Clear();
12: }
13:
14: public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
15: {
16: ApplicationContext.Current = message.Headers.GetHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace);
17: return null;
18: }
19: }
20: }
和應用大部分自定義擴展對象一樣,上面自定義的ClientMessageInspector和CallContextInitializer可以通過相應的WCF行為(服務行為、終結點行為、契約行為或者操作行為)應用到WCF執行管道中。在這里我定義了一個行為類型:ContextPropagationBehaviorAttribute,它同時實現了IServiceBehavior和 IEndpointBehavior,所以既是一個服務行為,也是一個終結點行為。同時ContextPropagationBehaviorAttribute還繼承自Attribute,所以可以通過特定的方式應用該行為。自定義ClientMessageInspector和CallContextInitializer分別通過ApplyClientBehavior和ApplyDispatchBehavior方法應用到WCF客戶端運行時和服務端運行時。ContextPropagationBehaviorAttribute定義如下:
1: using System;
2: using System.ServiceModel.Description;
3: using System.ServiceModel.Dispatcher;
4: namespace Artech.PetShop.Infrastructures
5: {
6: public class ContextPropagationBehaviorAttribute:Attribute, IServiceBehavior,IEndpointBehavior
7: {
8: #region IServiceBehavior Members
9: public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
10: {
11: }
12:
13: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
14: {
15: foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
16: {
17: foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
18: {
19: foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
20: {
21: operation.CallContextInitializers.Add(new ContextReceivalCallContextInitializer());
22: }
23: }
24: }
25: }
26:
27: public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
28: {
29: }
30:
31: #endregion
32:
33: #region IEndpointBehavior Members
34:
35: public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
36: {
37: }
38:
39: public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
40: {
41: clientRuntime.MessageInspectors.Add(new ContextSendInspector());
42: }
43:
44: public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
45: {
46: foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
47: {
48: operation.CallContextInitializers.Add(new ContextReceivalCallContextInitializer());
49: }
50: }
51:
52: public void Validate(ServiceEndpoint endpoint)
53: {
54: }
55:
56: #endregion
57: }
58: }
對于服務行為,我們既可以通過自定義特性的方式,也可以通過配置的方式進行行為的應用;而終結點行為的應用方式則僅限于配置(通過編程的形式除外)。為此我們還需要為行為定義一個特殊的類型:BehaviorExtensionElement。
1: using System;
2: using System.ServiceModel.Configuration;
3: namespace Artech.PetShop.Infrastructures
4: {
5: public class ContextPropagationBehaviorElement: BehaviorExtensionElement
6: {
7: public override Type BehaviorType
8: {
9: get { return typeof(ContextPropagationBehaviorAttribute); }
10: }
11:
12: protected override object CreateBehavior()
13: {
14: return new ContextPropagationBehaviorAttribute();
15: }
16: }
17: }
那么ContextPropagationBehaviorAttribute就可以通過下面的配置應用到具體的服務或終結點上了。
服務端(ServiceBehavior):
1: xml version="1.0"?>
2: <configuration>
3: <system.serviceModel>
4: <behaviors>
5: <serviceBehaviors>
6: <behavior name="petshopbehavior">
7: <contextPropagation/>
8: <unity/>
9: behavior>
10: serviceBehaviors>
11: behaviors>
12: <extensions>
13: <behaviorExtensions>
14: <add name="contextPropagation" type="Artech.PetShop.Infrastructures.ContextPropagationBehaviorElement, Artech.PetShop.Infrastructures, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
15: behaviorExtensions>
16: extensions>
17: <services>
18: <service behaviorConfiguration="petshopbehavior" name="Artech.PetShop.Products.Service.ProductService">
19: <endpoint binding="ws2007HttpBinding" contract="Artech.PetShop.Products.Service.Interface.IProductService"/>
20: service>
21: services>
22: system.serviceModel>
23: configuration>
客戶端(EndpointBehavior)
1: xml version="1.0"?>
2: <configuration>
3: <system.serviceModel>
4: <behaviors>
5: <endpointBehaviors>
6: <behavior name="petShopBehavior">
7: <contextPropagation/>
8: behavior>
9: endpointBehaviors>
10: behaviors>
11: <extensions>
12: <behaviorExtensions>
13: <add name="contextPropagation" type="Artech.PetShop.Infrastructures.ContextPropagationBehaviorElement, Artech.PetShop.Infrastructures, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
14: behaviorExtensions>
15: extensions>
16: <client>
17: <endpoint address="http://localhost/PetShop/Products/productservice.svc" behaviorConfiguration="petShopBehavior" binding="ws2007HttpBinding" contract="Artech.PetShop.Products.Service.Interface.IProductService" name="productservice"/>
18: client>
19: system.serviceModel>
20: configuration>
出處:http://artech.cnblogs.com
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。