通过上一篇了解了模块内基本的层次划分之后,接下来我们来聊聊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<IMembershipService>(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<T>创建该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<ApplicationContext>(
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>(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<ServiceEndpoint> 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>
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。