一、方案架构
本方案架构很简单——它用一个Web服务来包装ASP.NET 2.0提供者并且为远程客户暴露该凭证管理,你甚至还能在该架构中加上一些失去的功能。然后,在提供一个丰富的用户接口和全面凭证管理经验的同时,使用一个Windows表单应用程序来消费该Web服务。该Web服务配置文件将包含特定于该凭证存储的指令。然而,这的确意味着所有由该Web服务管理的应用程序都将可以共享这些指令。
尽管你能够从头到尾地构建该Web服务,也就是说,首先用静态方法Roles和Membership来包装它们并定义该Web服务,我却更喜欢一种契约驱动的方法:首先设计执行各种操作的最好接口将是什么,并且直到需要时才考虑怎样实现它们。这样做可以确保由Web服务暴露的接口支持所有要求的管理功能并且还将减少该客户应用程序与任何实现细节(例如包装提供者)之间的耦合。
ASP.NET 2.0的一个更好的特点是它支持Web服务接口,你可以定义并且让该Web服务暴露逻辑接口,就象类的表现一样。为此,你需要用WebServiceBinding属性修饰你的接口并且经由WebMethod属性来暴露单个的接口方法。然后,你将有一个派生于这个接口的类并实现该接口,而且编译器将要求你支持该接口的所有方法。
为了管理和交互于凭证存储和Web服务配置,我定义了5个接口-IApplicationManager,IMembershipManager,IPasswordManager,IroleManager和IUserManager。
(一) IApplicationManager
该IApplicationManager接口显示于所附源码中的列表2,允许管理员删除一指定的应用程序-也就是说,从数据库中删除所有到它的参考并且删除它的所有用户和角色。IApplicationManager允许从存储中删除所有的应用程序,并且它能返回在该存储中的所有应用程序的一个列表。注意,这个接口作为一个内部的接口被定义-public或internal可见性修饰词对Web服务接口都是无意义的。该接口上的每个方法用WebMethod属性加以修饰并有一个该方法的简短描述。此外,存取凭证存储的所有方法都被设置为使用事务处理。这样以来,两种操作-如删除一应用程序和创建一用户将在彼此完全隔离的情况下执行,从而保证了如删除所有用户等复杂操作的原子性。.NET 2.0中的Web服务只能启动一个新事务,而且它是由WebMethod属性的TransactionOption属性来控制的。最后一点是把WebServiceBinding属性应用于接口上。这就指定该接口是一个客户和服务都能绑定到的Web服务接口。为了把该接口以一个WSDL契约方式暴露给外界,你需要使用一个shim类。这个shim类的设计是必要的,因为你不能把一个接口作为一Web服务暴露,而且你也不能在其上应用WebService属性。这个shim类还将经由WebService属性为该接口命名空间定义。下面的代码显示了IApplicationManagerShim抽象类的定义。
[WebService(Name="IApplicationManager",
Namespace="http://CredentialsServices",
Description="IApplicationManager is used to manage
applications. This web service is only
the definition of the interface. You
cannot invoke method calls on it.")]
abstract class IApplicationManagerShim : IApplicationManager{
public abstract void DeleteApplication(string application);
public abstract string[] GetApplications();
public abstract void DeleteAllApplications();
}
因为IApplicationManagerShim是一个类,所以你可以把它暴露为一个Web服务。因为它是一抽象类且所有方法被定义为抽象方法,所以不需要(也不能)实现任何方法。为了使其看起来就象该接口,IapplicationManagerShim把WebService属性的属性名设置为IApplicationManager(代替缺省的类名)。现在,你可以使用IApplicationManager.asmx文件来暴露该接口。
<%@ WebService Language="C#"
CodeBehind="~/App_Code/IApplicationManagerShim.cs"
Class="IApplicationManagerShim"%>
现在,如果你浏览到IApplicationManager.asmx页面,你就会看到该接口定义。你可以使用WSDL.exe的serverInterface选项来把接口定义输入到客户端或任何其它想绑定到该接口定义上的服务。
(二) IMembershipManager
IMembershipManager接口(见所附源码中的列表3)允许你管理用户帐户的所有方面-创建和删除用户帐户,更新用户帐户,检索用户帐户细节以及检索在一应用程序中的所有用户。
(三) IRoleManager
IRoleManager接口允许你管理逻辑角色的所有方面-创建和删除角色,从角色中增加和删除用户以及检索在一应用程序中的所有角色。
[WebServiceBinding("IRoleManager")]
interface IRoleManager{
[WebMethod(...)]
void CreateRole(string application,string role);
[WebMethod(...)]
bool DeleteRole(string application,string role,bool throwOnPopulatedRole);
[WebMethod(...)]
void AddUserToRole(string application,string userName, string role);
[WebMethod(...)]
void DeleteAllRoles(string application,bool throwOnPopulatedRole);
[WebMethod(...)]
string[] GetAllRoles(string application);
[WebMethod(...)]
string[] GetRolesForUser(string application,string userName);
[WebMethod(...)]
string[] GetUsersInRole(string application, string role);
[WebMethod(...)]
void RemoveUserFromRole(string application,string userName, string roleName);
//更多成员
}
(四) IPasswordManager
这个IPasswordManager接口主要提供与应用程序口令策略相关的只读信息。
[WebServiceBinding("IPasswordManager")]
interface IPasswordManager{
[WebMethod(...)]
bool EnablePasswordReset(string application);
[WebMethod(...)]
bool EnablePasswordRetrieval(string application);
[WebMethod(...)]
string GeneratePassword(string application,int length,
int numberOfNonAlphanumericCharacters);
[WebMethod(...)]
bool RequiresQuestionAndAnswer(string application);
[WebMethod(...)]
string ResetPassword(string application,string userName);
[WebMethod(...)]
string GetPassword(string application,string userName,string passwordAnswer);
[WebMethod(...)]
void ChangePassword(string application,string userName,string newPassword);
//更多成员
}
典型地,该策略存储在应用程序的配置文件中。该策略包括是否启动口令重置和检索,口令强度和口令回答策略等。你也可以使用IpasswordManager来生成一相应于该口令强度策略的新口令。另外,IpasswordManager可用于重置、改变或检索一指定用户的口令。
(五) IUserManager
IUserManager接口允许校验用户凭证,检索角色身份以及获取指定用户是其成员之一的所有角色。该接口用于测试和分析目的。
[WebServiceBinding("IUserManager")]
public interface IUserManager{
[WebMethod(...)]
bool Authenticate(string applicationName,string userName, string password);
[WebMethod(...)]
bool IsInRole(string applicationName,string userName, string role);
[WebMethod(...)]
string[] GetRoles(string applicationName,string userName);
}
二、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 Actionpublic 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类型了,因此你可以只取得这个实例。
三、凭证管理器应用程序
本文相应的源代码包含了这个凭证管理器应用程序-一个具有丰富的用户接口的Windows表单应用程序,它使用在上一步描述的Web服务接口来为任何数目的应用程序管理安全凭证存储。
该应用程序导入五个Web接口定义,并且它独占地使用那些接口。该应用程序有一个称为AspNetSqlProviderService的Web服务代理类-它用于定位该服务。你需要从导入的接口手工地把它添加到该服务上。
partial class AspNetSqlProviderService :
SoapHttpClientProtocol,IMembershipManager,
IUserManager,IPasswordManager,
IApplicationManager,IRoleManager
{
public AspNetSqlProviderService (){
Credentials = CredentialCache.DefaultCredentials;
Url = Settings.Default.AspNetSqlProviderService ;
}
//其它的执行
}
为了支持集成的Windows认证,这个代理类的构造器使用CredentialCache的静态属性DefaultCredentials设置凭证属性-它只是简单地读取当前线程的安全标志。另外,这个构造器还使用设计器生成的Settings类从应用程序配置类中读取Web服务地址。
这个应用程序的使用相当直观,所以我只介绍一下主要屏幕和选项。Applications选项卡(见图4)允许你选择要配置的应用程序。
在此,选择一应用程序将影响所有其它的选项卡。你可以创建和删除一个应用程序或删除所有应用程序。Users选项卡列举出在选择的应用程序中的所有用户。
你可以创建或删除一用户。如果你删除一用户但是不选择"All Data"复选框的话,它将删除该用户但是维持它的角色身份信息。你可以更新一用户帐户或删除所有用户。根据从AspNetSqlProviderService Web服务返回的口令策略的不同,你能够或不能够改变或重置口令,而且可以或不可以需要回答该口令。在Users选项卡的按钮和它所显示的对话框也相应地启动或禁止。
在Users选项卡的右边是统计信息,如当前用户的在线数。Roles选项卡允许你把角色添加到应用程序。
当删除一个角色时,如果你选择了"Fail if populated"复选框,那么如果它有任何成员的话,就不会让你删掉该角色。左边的列表视图显示在该应用程序中的所有用户。你可以从一个角色添加或删除一用户,或从所有角色中删除一用户。在底部,"Users in role"列表框显示了在上面选定的角色中的所有用户,而"Roles for User"列表框显示了在上面选定的用户中的所有角色(见《理论篇》之图3)。
Passwords选项卡显示在图7中,它列出已配置的口令策略并且允许你生成一与指定的口令强度策略相匹配的新口令。
。
该选项卡让你选择要使用的Web服务。一旦启动,凭证管理器应用程序即从应用程序配置文件中读取这个地址。这个选项卡显示被选择的Web服务。如果地址是无效的,也就是说,该服务不支持所有要求的功能,那么在应用程序中的所有控件都将为空且是禁止的。你可以提供一个不同的地址,而下面的Web浏览器控件将会显示这一服务。然而,如果该服务支持要求的Web方法(一有效的Web服务)的话,你可以只选择一个Web服务地址(通过点击Select按钮)。如果该服务是无效的,那么将禁止Select按钮。
不幸的是,在.NET 2.0中没有提供校验某服务是否支持一特别绑定或Web接口的内置支持,因此我不得不手工实现。所附源码中的列表6显示出RefreshSelectButton()和ContainsInterface()助理方法。RefreshSelectButton()首先禁止Select按钮和相匹配的菜单项。然后,验证指定的地址是一个.NET Web服务的地址。然后,它存取显示在Web浏览器控件中的页面的内容并且验证它包含支持所有的接口的方法。为此,它要调用ContainsInterface()方法并把页面的内容和要验证的接口类型提供给它。ContainsInterface()验证该类型是一个接口的类型并且获得一个MethodInfo对象数组-标记在该接口上的每个方法。然后,它定义一个接收单个实例MethodInfo的匿名方法并且使用字符串类的Contains()方法来验证该内容包含那个方法。ContainsInterface()使用该数组类的静态TrueForAll<T>()方法。
public delegate bool Predicatepublic abstract class Array : ...
{
public static bool TrueForAll<
T>(T[] array,Predicate<
T> match);
}
ContainsInterface()提供给TrueForAll()一个MethodInfo对象数组和匿名方法形式的Predicate。只有在该内容中找到所有的方法时,TrueForAll<T>()才返回true。
|