top
Loading...
ASP.NET2.0應用中定制安全憑證之實踐篇
一、方案架構

本方案架構很簡單——它用一個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 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類型了,因此你可以只取得這個實例。

三、憑證管理器應用程序

本文相應的源代碼包含了這個憑證管理器應用程序-一個具有豐富的用戶接口的Windows表單應用程序,它使用在上一步描述的Web服務接口來為任何數目的應用程序管理安全憑證存儲。

該應用程序導入五個Web接口定義,并且它獨占地使用那些接口。該應用程序有一個稱為AspNetSqlProviderService的Web服務代理類-它用于定位該服務。你需要從導入的接口手工地把它添加到該服務上。


圖4.Applications選項卡:這個選項卡讓你選擇要配置的應用程序。

partial class AspNetSqlProviderService :
SoapHttpClientProtocol,IMembershipManager,
IUserManager,IPasswordManager,
IApplicationManager,IRoleManager
{
public AspNetSqlProviderService (){
Credentials = CredentialCache.DefaultCredentials;
Url = Settings.Default.AspNetSqlProviderService ;
}
//其它的執行
}

為了支持集成的Windows認證,這個代理類的構造器使用CredentialCache的靜態屬性DefaultCredentials設置憑證屬性-它只是簡單地讀取當前線程的安全標志。另外,這個構造器還使用設計器生成的Settings類從應用程序配置類中讀取Web服務地址。


圖5.Users選項卡:該選項卡列出在選定的應用程序中的所有用戶。

這個應用程序的使用相當直觀,所以我只介紹一下主要屏幕和選項。Applications選項卡(見圖4)允許你選擇要配置的應用程序。

在此,選擇一應用程序將影響所有其它的選項卡。你可以創建和刪除一個應用程序或刪除所有應用程序。Users選項卡列舉出在選擇的應用程序中的所有用戶。

你可以創建或刪除一用戶。如果你刪除一用戶但是不選擇"All Data"復選框的話,它將刪除該用戶但是維持它的角色身份信息。你可以更新一用戶帳戶或刪除所有用戶。根據從AspNetSqlProviderService Web服務返回的口令策略的不同,你能夠或不能夠改變或重置口令,而且可以或不可以需要回答該口令。在Users選項卡的按鈕和它所顯示的對話框也相應地啟動或禁止。

在Users選項卡的右邊是統計信息,如當前用戶的在線數。Roles選項卡允許你把角色添加到應用程序。


圖6:Roles選項卡:這個選項卡讓你把角色添加到應用程序。

當刪除一個角色時,如果你選擇了"Fail if populated"復選框,那么如果它有任何成員的話,就不會讓你刪掉該角色。左邊的列表視圖顯示在該應用程序中的所有用戶。你可以從一個角色添加或刪除一用戶,或從所有角色中刪除一用戶。在底部,"Users in role"列表框顯示了在上面選定的角色中的所有用戶,而"Roles for User"列表框顯示了在上面選定的用戶中的所有角色(見《理論篇》之圖3)。

Passwords選項卡顯示在圖7中,它列出已配置的口令策略并且允許你生成一與指定的口令強度策略相匹配的新口令。


圖7.Passwords選項卡:你可以使用這個選項卡生成一口令。


圖8.Credentials Service選項卡:使用這個選項卡來選擇使用的Web服務。

該選項卡讓你選擇要使用的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 Predicate<T>(T obj);
public abstract class Array : ...
{
public static bool TrueForAll<
T>(T[] array,Predicate<
T> match);
}

ContainsInterface()提供給TrueForAll()一個MethodInfo對象數組和匿名方法形式的Predicate。只有在該內容中找到所有的方法時,TrueForAll<T>()才返回true。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗