二、AspNetSqlProviderService Web服务
显示在所附源码中的列表4中的AspNetSqlProviderService类实现了五个Web接口。其过程就象实现任何其它接口一样-你可以隐式或显式地派生并实现方法(见列表4)。我是通过把这些实现简单地代理到提供者的适当的方法来实现该Web接口上的大多数方法的。在每一次使用角色或身份之前,你必须为之作好准备-通过设置要使用的应用程序名。例如,为了实现IRoleManager.CreateRole(),你将需要建立:
void IRoleManager.CreateRole(string application,string role){ Roles.ApplicationName = application; Roles.CreateRole(role); } |
其中的一些方法在调用该提供者前后还要求一点工作。例如,如果启动口令检索,你只能检索用户口令,而AspNetSqlProviderService则用于判定它。
string IPasswordManager.GetPassword(string application,string userName, string passwordAnswer){ Membership.ApplicationName = application; Debug.Assert(Membership.EnablePasswordRetrieval); MembershipUser membershipUser =Membership.GetUser(userName); return membershipUser.GetPassword(passwordAnswer); } |
然而,还有一些方法并没有得到提供者的直接支持。有两种可能的解决办法-第一种是尝试并使用提供者的其它方法来完成所希望的操作。第二种是直接执行aspnetdb数据库。两种方法都存在利弊。例如,可以考虑实现MembershipManager.DeleteAllUsers()方法。你可以对该应用程序中的每个用户调用身份提供者的DeleteUser()方法,如列表4所示。首先你要调用IMembershipManager.GetAllUsers()方法来得到应用程序中的所有用户。这就是你通过实现该接口的类来使用该接口方法的显式实现方式。然后,你可以定义一个匿名方法来删除用户,把该匿名方法赋值到一个Action<string>代理,并且使用Array类的静态方法ForEach<T>()删除每个用户。
public delegate void Action<T>(T obj); public abstract class Array : ... { public static void ForEach<T>(T[] array,Action<T> action); } |
第一种方法的优点是任何与删除一个用户相关的内部活动(如也删除所有的角色身份)仍旧被执行。其不足是,你需要对该数据库做更多的调用。
正如刚才提到的,第二种方法是直接对aspnetdb数据库编程。当提供者没有提供任何方式来完成此任务时,这是最有用的。例如,提供者并不支持删除一应用程序,更不说删除所有的应用程序了。尽管你可以编写一个存储过程来做这件事情,但我的另一个目标是不动用aspnetdb,而是使用原始SQL命令来实现IApplicationManager.DeleteApplication()和IApplicationManager.DeleteAllApplications()。我已用一个AspNetDbTablesAdapter助理类(在此没有显示)包装了这些命令。直接访问数据库的优点是你仅执行一个命令;不足之处是,如果要改变数据库模式,你将需要更改你的代码。假定如删除所有的用户或一应用程序等操作是一般不涉及的并且超级用户的数目经常很小,那么我想最好尽可能让AspNetSqlProviderService使用ASP.NET 2.0提供者。
(一) 设置服务
由AspNetSqlProviderService Web服务使用的Web.Config文件中的设置影响它管理的所有应用程序。特别地,如口令策略这样的设置适用于所有的应用程序。该服务使用默认提供者(SQL SERVER),因此如果缺省的连接字符串(在文件machine.config中维护)已经足够的话,就不需要指定一个提供者甚至一个连接字符串。如果你需要一个不同的连接字符串,你需要包括一个connectionStrings标签(见所附源码中的列表5)。另外,为了使用Roles类,你必须通过下列指令来启动基于角色的安全。
<roleManager enabled="true" /> |
(二) 保护服务
尽管其凭证由AspNetSqlProviderService Web服务来管理的应用程序可能是基于互联网或基于内部网的,但是服务本身是被设计由一个管理员通过本地内部网来存取的。你应该认证和授权到该服务的调用。另外,你还应该通过加密通讯来提供秘密服务。这是要求的,因为该服务要处理如用户名和口令等敏感信息。保证秘密的最容易的方法是使用HTTPS。AspNetSqlProviderService在它的构造器中经由静态VerifySecureConnection()助理方法来进行验证是否使用了一个安全连接。VerifySecureConnection()使用当前请求的IsSecureConnection属性。为了支持开发或该服务的其它类型的非生产性发布,VerifySecureConnection()方法用Conditional属性加以修饰。只有定义编译符号HTTPS时该方法才会起作用。关于认证该服务的用户,既然Web服务是一本地内部网服务,那么使用Windows认证就不会有任何错误了。我选择了使用集成的Windows认证-这将省去了用户必须明确地登录的麻烦。集成的认证的另外一个优点是,它用一种专利方式来散列化发送过去的凭证。
为了配置集成的Windows认证,转到在IIS下的AspNetSqlProviderService Web服务属性,选择目录安全选项卡,并且点击"Edit…"按钮。不选择"Anonymous access"复选框并且保证选中"Integrated Windows authentication"复选框。AspNetSqlProviderService类被配置以要求认证(见列表4)-它使用PrincipalPermission属性并把被认证的属性设计为true。
[PrincipalPermission(SecurityAction.Demand,...,Authenticated=true)] |
一旦调用者通过IIS被认证,该服务缺省地将在IIS中以配置的身份仍旧运行。我想以调用者身份运行该服务。为此,Web.Config文件(见列表5)包含了一个identity标签-它把impersonate属性设置为true。
<identity impersonate="true"/> |
然后,你需要使用SQL SERVER管理工具来允许Web服务的调用者从aspnetdb数据库中进行读和写。
保护该Web服务的另一个重要地方是授权。我想要验证只有Windows超级用户组的成员才能存取这一服务。为此,AspNetSqlProviderService类上的PrincipalPermission属性要求只有超级用户角色的成员才被允许使用该服务。
[PrincipalPermission(SecurityAction.Demand, Role = "Administrators",...)] |
你可以用任何其它组(该服务的实际用户应该是其中的一员)来替换"Administrators"。
PrincipalPermission属性使用依附于该线程的安全负责人(principal)来验证调用者是否的确是指定角色中的一员。在依赖于NT组(如超级用户)时,这将强制你使用一个WindowsPrincipal的实例。
public class WindowsPrincipal : IPrincipal{ public WindowsPrincipal(WindowsIdentity ntIdentity); public virtual bool IsInRole(string role); //其它成员部分 } |
问题在于,为了使用Roles类,AspNetSqlProviderService Web.Config文件必须启动基于角色的安全策略。
<roleManager enabled="true" /> |
这反过来使得ASP.NET 2.0把一不同的principal依附到HttpContext和线程上,当然还有RolePrincipal类。
public sealed class RolePrincipal : IPrincipal{...} |
在NT超级用户角色中试图使用RolePrincipal和过分要求的身份将会失败,因为它将存取aspnetdb而不是Windows组来查找它。为补偿这一点,你必须手工地交换这些负责人并且在每次请求时把WindowsPrincipal的一个实例依附到该线程上。为此,最容易的办法是把一个Global.asax文件添加到该Web服务工程-通过指定在Global.cs文件中的Global类为类后的代码。
<%@ Application Language="C#" CodeBehind ="Global.cs" Inherits = "Global"%> |
这个Global类为应用程序授权请求提供一个处理器。
public class Global : HttpApplication{ protected void Application_AuthorizeRequest(object sender, EventArgs e){ if(HttpContext.Current.User.Identity.IsAuthenticated){ WindowsIdentity identity = HttpContext.Current.User.Identity as WindowsIdentity; Debug.Assert(identity != null); WindowsPrincipal principal; principal = new WindowsPrincipal(identity); Thread.CurrentPrincipal = principal; } } } |
如果调用者被认证,那么你需要实例化一新的WindowsPrincipal对象并且把它依附于当前线程。WindowsPrincipal构造器需要一个WindowsIdentity的实例。幸好,因为该服务正在使用Windows集成的认证,在成功认证后,与当前HTTP上下文相联系的身份已经是WindowsIdentity类型了,因此你可以只取得这个实例。