作者:Rockford Lhotka
Magenic Technologies
2003 年 10 月 6 日
摘要:Rocky Lhotka 說明如何在 Windows 與 Web 應用程式中,使用 .NET Principal 和 Identity 的觀念來實作自訂驗證與授權。(列印共 20 頁)
安全性是軟體開發中一項永無止盡的挑戰。我們大都不會想要成為安全專家,但身為應用程式的開發人員,我們經常面對處理與實作各種安全性機制的需求。
安全性包含許多細節,範圍從加密和編碼一直到使用者身份的管理,且更常用的驗證與授權也包含許多細節。由於驗證與授權幾乎是每一個應用程式的一部份,因此將其作為本文的主題。好在 Microsoft .NET Framework 中所含的功能可協助我們實作這兩項功能。
「驗證」是確認使用者就是其自稱的那一位的程序。 這可以要求使用者提供某些憑證 (通常為使用者名稱和密碼),我們再加以與一串已知資料比對來完成。如果憑證符合,該使用者即通過驗證,否則無法通過驗證。
使用者通過驗證之後,才能進行「授權」。這項程序就是決定該使用者在此應用程式中所能做和不能做的事。例如,他們將看到哪些畫面或哪些欄位,而在那些畫面上具有唯讀權限或是可讀寫的權限等等。
雖然授權通常被視為一項安全性功能,但是事實上,決定使用者能存取哪些功能卻是基於商務上的決定。所以授權實作的規則就是商務的規則,最後導致授權程式碼就會以依商務流程來決定,而與安全性流程完全無關。
亦即,安全性基礎結構必須讓商務流程能夠存取使用者的身份,並決定該使用者是否屬於特定群組或角色。為了要實作授權商務規則,我們必須假設使用者已通過驗證,且相關使用者資料已載入供使用。
定義角色、群組或其他授權條件的工作就是企業裡業務與安全團隊必須合力完成的。這兩個團隊必須對商務應用程式所需的角色達成協議,並合力將這些角色對應到企業裡其他應用程式所用的現有角色。
如果每一個團隊都建立各自的角色,則管理企業裡所有應用程式的使用者和角色會變得非常複雜。
[圖 1] 說明驗證、授權與應用程式之間的關係。
[圖 1] 驗證、授權與商務應用程式
System.Security 命名空間
Microsoft .NET Framework 包含 System.Security 命名空間。此命名空間包含各種安全性相關的功能,包括:加密、管理 .NET Runtime 本身的安全性,以及使用者的驗證與授權。
在處理使用者驗證與授權的時候,我們會在 System.Security.Principal 中發現一些有趣的功能。.NET Framework 可將 Principal 物件與每一個執行緒建立關聯,以管理使用者身份。因為所有程式碼都是在執行緒上執行,所以所有程式碼都能存取 Principal 物件。
在 ASP.NET 裡我們能夠存取兩個 Principal 物件。有一個 Principal 物件與目前的執行緒有關聯,而第二個 Principal 物件則透過其 Current.User 屬性,與目前的 HttpComtext 有關聯。依據預設,目前執行緒的 Principal 物件設定為與 HttpContext 相同。
根據與目前使用者相關聯的角色清單,利用 Principal 物件可在程式碼中輕鬆地完成授權實作。它還能讓我們決定使用者的身分,這是透過存取 Identity 物件來完成的。[圖 2] 中的 UML 說明目前執行緒與其 Principal 和 Identity 物件之間的關係。
[圖 2] 目前執行緒、Principal 與 Identity 之間的關係
在典型的 Windows Form 或 Web Form 應用程式裡,我們只有一個執行緒,且該執行緒擁有相關聯的 Principal 和 Identity 物件。
共有數種類型的 Principal 和 Identity 類別,可支援不同類型的驗證功能。如果需要提供自訂的驗證功能,我們甚至可以自行建立自訂的 Principal 和 Identity 類別。
Principal 類別可實作 IPrincipal 介面。此介面能提供下列基本的授權服務:
方法 | 說明 |
---|---|
Identity | 傳回與 Principal 有關聯的 Identity 物件 |
IsInRole | 傳回一布林值,表示該使用者是否為指定角色的成員 |
Identity 類別可實作 IIdentity 介面。此介面包含下列屬性,我們可用來檢查使用者的身份,以及如何 (或是否已經) 驗證它們:
方法 | 說明 |
---|---|
AuthenticationType | 傳回一個字串 (String),表示用來驗證目前使用者的驗證類型 |
IsAuthenticated | 傳回一布林值,表示目前使用者是否已通過驗證 |
名稱 | 傳回目前使用者的名稱或使用者識別碼 |
藉由結合 Identity 物件的屬性以及 Principal 物件的 IsInRole 方法,我們就能夠在商務應用程式中輕易地實作授權程式碼。
實作授權
只要利用 Principal 和 Identity 物件,我們就能撰寫授權程式碼。
利用執行緒的 Principal
在 Windows Form 應用程式裡,我們將利用與執行緒相關聯的 Principal 物件。這意謂我們就能撰寫如下所示的程式碼:
If Thread.CurrentPrincipal.Identity.IsAuthenticated Then lblUserName.Text = Thread.CurrentPrincipal.Identity.Name If Thread.CurrentPrincipal.IsInRole("Manager") Then ' enable manager-level features End If Else ' deny access End If
此程式碼假設我們已匯入 System.Threading 命名空間,以便輕易地存取 Thread 類別。
首先,它會檢查目前使用者是否已成功地通過驗證。 如果已通過,則會繼續執行授權的工作。否則會拒絕存取該應用程式。在某些情況下,我們可以選擇提供低層次的訪客式存取予未經驗證的使用者,而不必完全拒絕存取。
如果使用者已通過驗證,則我們可以在 Label 控制項中顯示使用者名稱,並檢查該使用者是否屬於 Manager 角色。如此便能夠依據使用者的角色,選擇要啟用或停用某些應用程式功能。於應用程式開始時,或於程式碼中商業行為會依使用者的角色而改變之處,我們都可以利用 IsInRole 方法。
使用 HttpContext 的 Principal
在 Web Form 應用程式裡,我們可以撰寫下列類似的程式碼:
If HttpContext.Current.User.Identity.IsAuthenticated Then lblUserName.Text = HttpContext.Current.User.Identity.Name If HttpContext.Current.User.IsInRole("Manager") Then ' enable manager-level features End If Else ' deny access End If
第一個範例係採用 Thread.CurrentPrincipal,這對於 Windows Form 和 Web Form 來說都是正確的程式碼。第二個範例係採用 HttpContext.Current.User,僅適用於 HttpContext 存在的 ASP.NET 應用程式中。
作為一般的規則,類別程式庫中的程式碼應使用 Thread.CurrentPrincipal 來存取目前的 Principal 物件,而不應嘗試使用 HttpContext.Current.User。如此一來,類別程式庫不僅可以用於 Web 應用程式中,也可以用於任何類型的 .NET 應用程式中。於 Web 應用程式中執行時,Thread.CurrentPrincipal 依據預設即擁有與 HttpContext.Current.User 相同的數值。
使用者驗證
我們至今所看到的程式碼係假設有一正確的 Principal 與執行緒或 HttpContext 相關聯。我們一起來討論此 Principal 是如何誕生的。
了解 Microsoft .NET Framework 根據預設會為每一個執行緒提供一 Principal 物件,這一點非常重要。有一組規則能管制如何 (或是否已經) 建立此預設的 Principal 物件。
事件的流程會依我們是在建立 ASP.NET 應用程式或是非 Web 應用程式而異。我們將會仔細探討這些情況。
針對非 Web 應用程式
在 ASP.NET 外部執行時 (例如於 Windows Form 或 Console 應用程式中),執行緒將會有一個預設的 Principal,其類型為 GenericPrincipal,而且是空的。任何時候呼叫 IsInRole 都會傳回 False,因為 Principal 沒有正確的角色。
預設的 GenericPrincipal 擁有類型為 GenericIdentity 的預設 Identity。 此物件基本上也是空的,對於 IsAuthenticated 會傳回 False,對於 AuthenticationType 與 Name 兩者都會傳回空的字串 (String)。
SetPrincipalPolicy 方法
藉由利用目前 AppDomain 的 SetPrincipalPolicy 方法,我們即可控制執行緒之 Principal 的預設行為。預設為:
AppDomain.CurrentDomain.SetPrincipalPolicy( _ PrincipalPolicy.UnauthenticatedPrincipal)
選項計有:
參數 | 說明 |
---|---|
PrincipalPolicy.NoPrincipal | 不提供執行緒預設的 Principal 物件。也就是說,CurrentPrincipal 方法將會傳回無 (Nothing),除非自行將其設定為正確的 Principal 物件。 |
PrincipalPolicy.UnauthenticatedPrincipal | (預設) 具有未經驗證 GenericIdentity 之尚未驗證的 GenericPrincipal 與執行緒有關聯。 |
PrincipalPolicy.WindowsPrincipal | 對應到目前 Windows 使用者的識別的 WindowsPrincipal 和 WindowsIdentity 物件與執行緒有關聯。 |
SetPrincipalPolicy 方法通常會在應用程式開始執行的時候呼叫。將會根據原則建立適當的預設 Principal (如果有的話)。
請注意,基於效能上的理由,一直到第一次以存取執行緒的 CurrentPrincipal 方法來要求 Principal 物件時,才會建立預設的 Principal 物件。 從效能的角度來看,建立某些 Principal 物件 (特別是 WindowsPrincipal) 可能費時甚久,所以只有在我們存取該物件時才會建立 .NET Framework。
如果自行實作自訂的驗證功能,則我們可以全權決定要如何及何時建立適當的 Principal 和 Identity 物件。我們將在本文後面再討論。
現在,我們一起來討論使用每一個原則選項的原因。
NoPrincipal
實作自訂的驗證功能時,NoPrincipal 選項很有用。NoPrincipal 選項主要是為了確保每一個執行緒都有指定一個正確的 Principal 物件。將原則設定為 NoPrincipal,我們就知道執行緒將永遠無法取得預設的 Principal,因此這些執行緒會取得我們指定的 Principal,或者一個也沒有。
如此就能協助確保未通過驗證的使用者都不能進入我們的系統。若不希望有任何訪客或匿名存取應用程式,我們可以利用 NoPrincipal 選項。
UnauthenticatedPrincipal
實作自訂的驗證功能時,UnauthenticatedPrincipal 選項很有用。這是預設的選項,它能確保每一個執行緒都有一正確但未經驗證的 Principal 物件。這也是最單純的選項,因為我們可以撰寫授權程式碼,而不必擔心 CurrentPrincipal 會成為無 (Nothing)。
實作自訂的驗證功能時,我們會特地將一通過驗證的 Principal 指定給執行緒,來取代預設未經驗證的 Principal。
WindowsPrincipal
如果想要利用 Windows 整合式或網域安全性,則 WindowsPrincipal 選項很有用。設定具有此選項之後,任何執行緒第一次要求 Principal 的時候,.NET Framework 會建立 WindowsPrincipal 和有關聯的 WindowsIdentity 物件,且二者皆對應到目前程序的使用者的識別。
於 Windows Form 應用程式裡,這就是登入電腦的使用者。對於 Windows 服務來說,這就是該服務所用來執行的使用者帳戶。
WindowsPrincipal 物件採用使用者的 NT 群組作為 IsInRole 方法的角色。如此一來,我們就可以利用下列程式碼來決定使用者是否屬於 Power Users NT 群組,或者是否屬於網域會計 (Accounting) 群組:
AppDomain.CurrentDomain.SetPrincipalPolicy( _ PrincipalPolicy.WindowsPrincipal) If Thread.CurrentPrincipal.IsInRole("BUILTIN\Power Users") ' they are a power user End If If Thread.CurrentPrincipal.IsInRole("MYDOMAIN\Accounting") ' they are in accounting End If
請注意,需要用本機電腦名稱或網域名稱,來識別特定的 NT 群組。對於由 Windows 預先定義好的內建群組,我們可以使用特殊的 BUILTIN 網域。
請注意,我們已瞭解 SetPrincipalPolicy 是如何管制執行緒的預設 Principal 物件,以及 Windows 驗證是如何運作的,讓我們一起來探討如何實作自訂的驗證功能。
實作自訂的驗證功能
由於整合式 Windows 安全性並非在所有環境裡都很實用,因此自訂的驗證功能就更形重要。在許多情況下,使用者並非全部都屬於同一個網域,或者企業驗證資料庫也不是 Windows 或 Active Directory。使用者驗證資料有時候是儲存在資料庫或 LDAP 伺服器中,或是我們必須據以查驗憑證的其他某些位置。
如果主體 (Principal) 原則設定為 UnauthenticatedPrincipal 或是 NoPrincipal,則我們可以實作自訂的驗證功能。在任一情況下,我們可以實作程式碼來蒐集使用者的憑證、將其在驗證資料庫裡查驗,並為使用者核發我們自己的 Principal 和 Identity 物件。此自訂的 Principal 和 Identity 可與執行緒相關聯,並且甚至可以納入 AppDomain 裡任何新執行緒的預設識別中。
我們自訂的 Principal 和 Identity 物件可以是 GenericPrincipal 和 GenericIdentity 的特定執行個體,或者我們可以自行建立自訂的類別。在大部分情況下,GenericPrincipal 類別能提供足夠的功能來滿足我們的需求。
自行建立自訂的 Principal 和 Identity 類別的主要理由是:如果想要提供除了 IPrincipal 和 IIdentity 所定義之外的額外資訊或功能。例如,如果想要在 Identity 物件上使用 Name 屬性,不僅提供使用者的名稱,則我們可以建立自訂的 Identity 類別,來呈現使用者的電子郵件地址、部門號碼,或其他有用的資訊。
由於 GenericPrincipal 和 GenericIdentity 通常已經夠用了,因此本文在實作自訂驗證程式碼時會使用它們。
如果要實作自訂的驗證功能,我們必須建立 Login 方法,來接納使用者的憑證作為參數。然後再利用這些憑證來驗證使用者。如果使用者是正確的,則我們可以為該使用者建立合適的 Principal 和 Identity 物件,反之就必須確認執行緒擁有未經驗證的 Principal 和 Identity 物件配對:
Public Shared Sub Login( _ ByVal username As String, ByVal password As String) Dim principal As GenericPrincipal ' make sure we're set for custom authentication AppDomain.CurrentDomain.SetPrincipalPolicy( _ PrincipalPolicy.UnauthenticatedPrincipal) Dim valid As Boolean ' find out if the credentials are valid If username = "rocky" AndAlso password = "lhotka" Then valid = True Else valid = False End If If valid Then ' load the user's roles Dim roles() As String = {"Authors", "Speakers"} ' create the Principal and Identity objects Dim identity As New GenericIdentity(username, "Custom") principal = New GenericPrincipal(identity, roles) Else ' the credentials were not valid ' so create an unauthenticated Principal/Identity Dim identity As New GenericIdentity("", "") principal = New GenericPrincipal(identity, New String() {}) End If ' set the thread's current principal Thread.CurrentPrincipal = principal End Sub
如果要自訂此程式碼供自己使用,則只要將查驗使用者名稱與密碼的程式碼,更改為在資料庫表格、LDAP 伺服器,或其他使用者憑證清單中檢查。然後將載入角色清單的程式碼修改為從自己的資料來源載入使用者的角色。
目前應用程式已可透過登入表單或其他機制來蒐集使用者的憑證,然後就可以再呼叫 Login 方法。此程式碼看起來可能如下所示:
Login(txtUsername.Text, txtPassword.Text) If Not Thread.CurrentPrincipal.Identity.IsAuthenticated Then MsgBox("Incorrect username/password") End If
我們還可以建立 SignOut 方法,再呼叫它從應用程式中登出該使用者:
Public Shared Sub SignOut() Dim identity As New GenericIdentity("", "") Dim principal As New GenericPrincipal(identity, New String() {}) ' set the thread's current principal Thread.CurrentPrincipal = principal End Sub
呼叫此方法的時候,執行緒的 Principal 物件即設定成未經驗證的 Principal 和 Identity 物件配對。
在背景執行緒上設定 Principal
Login 和 SignOut 方法提供了基本的架構,讓使用者藉以登入和登出 Windows 應用程式。這就是一般單一執行緒應用程式的所有需求。如果建立的是多重執行緒應用程式,則我們可能會選擇多執行一些工作。
如本文前面所述,建立新執行緒的任何時候,其預設 Principal 即由 AppDomain 的主體 (Principal) 原則決定。如果我們使用的是自訂安全性,則主體原則就是 UnAuthenticatedPrincipal,且在 AppDomain 中所建立的任何新執行緒將會取得預設未經驗證的 GenericPrincipal 物件。
這可能不是我們想要的,因為我們可能希望新背景執行緒與 AppDomain 的主要執行緒一樣,擁有相同驗證過的 Principal。
解決此問題的方法是:確保每一個執行緒在建立的時候,即利用目前執行緒的 Principal 物件加以初始化:
Dim t As New Thread(AddressOf DoWork) t.CurrentPrincipal = Thread.CurrentPrincipal t.Start()
這種辦法對於另行建立的執行緒很有效,但是對於在 AppDomain 的執行緒集區中建立的執行緒卻不可行。在委派或其他方法上使用 BeginInvoke 呼叫的任何時後,我們都是使用執行緒集區。對於 .NET Framework 裡在物件上運用的其他 BeginXYZ 方法來說,情況都是一樣的。我們還可以呼叫 ThreadPool.QueueUserWorkItem,另行使用執行緒集區。
由於執行緒集區中的執行緒會自動建立起來,因此我們沒有機會建立其 Principal 物件。在這種情況下,我們有兩個選項。
第一個選項是將在背景執行緒上執行的程式碼設定為 Thread.CurrentPrincipal 的值。這並不是理想的辦法,因為其容易產生錯誤。我們必須自行將程式碼加入任一工作方法來執行此項工作,而且並不是永遠都有執行此項工作的方法。例如,如果在 TCP 通訊端物件上呼叫 BeginConnect,則我們就無法變更通訊端物件工作的方式,因此它便不會設定 Principal 物件。
第二種方法比較好。我們可以為整個 AppDomain 設定預設的 Principal 物件。從那一刻起,在 AppDomain 裡建立的任何執行緒都會自動使用該 Principal 物件。這種情況對另行建立的執行緒以及執行緒集區中的所有執行緒都是一樣的。
這種辦法的缺點就是 AppDomain 的預設主體 (Principal) 只能設定一次。如果應用程式讓使用者能夠登入和登出,而不必關閉應用程式,則在 AppDomain 上設定預設的 Principal 可能沒有什麼價值。
如果要在 AppDomain 上設定預設的 Principal,我們可以使用下列程式碼:
AppDomain.CurrentDomain.SetThreadPrincipal(Thread.CurrentPrincipal)
通常在確認使用者已正確地驗證過後,我們會立即執行此呼叫。例如:
Login(txtUsername.Text, txtPassword.Text) If Not Thread.CurrentPrincipal.Identity.IsAuthenticated Then MsgBox("Invalid username/password") Else AppDomain.CurrentDomain.SetThreadPrincipal(Thread.CurrentPrincipal) End If
於 AppDomain 的執行期間,嘗試呼叫 SetThreadPrincipal 不只一次會導致產生例外狀況。
呼叫過 SetThreadPrincipal 之後,所建立的任何新執行緒都會自動指派我們指定的 Principal 物件。此舉會處理將 Principal 物件指派給執行緒集區中的所有執行緒。
在 Windows 應用程式中如何實作自訂安全性,現在我們有一個好辦法。我們一起來觀察其在用 ASP.NET 的 Web 應用程式中是如何運作的。
ASP.NET 裡的程式碼
當程式碼在 ASP.NET 裡執行時 (例如,於 Web Form 或 Web 服務中),ASP.NET 與 IIS 會指示執行緒和 HttpContext 要如何取得其預設的 Principal 物件。前面章節中的 SetPrincipalPolicy 方法通常不會在 ASP.NET 應用程式中使用。
於 ASP.NET 裡共有三種主要的驗證情況。我們可以使用 Windows 驗證、以表單為基礎的驗證,或外部驗證。
使用 Windows 驗證
Windows 驗證要依靠使用者的 Windows 身份。此種身份可以利用通過式 (Pass-Through) 安全性,直接從用戶端傳送到 Web 伺服器,或者 Web 伺服器可以要求瀏覽器彈出一登入對話框,讓使用者得以輸入其使用者名稱和密碼給伺服器。
不論採用何種方式,在程式碼執行之前,ASP.NET 環境即已設定好,因此程式碼就會用使用者的身份執行。在這種情況下,HttpContext 與執行緒都會擁有指向使用者的 WindowsPrincipal 和 WindowsIdentity 物件的參照。
在非 ASP.NET 環境裡,我們可以利用 SetPrincipalPolicy 方法,來指定使用 Windows 驗證。在 ASP.NET 環境裡,我們可以變更網站的虛擬根目錄的安全性選項,來啟用 Windows 驗證。
明確地說,我們可以利用 IIS 管理主控台,來停用網站的匿名存取,並啟用整合式 Windows 驗證,如 [圖 3] 所示。
[圖 3] 啟用整合式 Windows 驗證
我們還必須確定 Web.config 檔案中擁有 Windows 的預設驗證值:
<authentication mode="Windows" />
此時,HttpContext.Current.User 和 Thread.CurrentPrincipal 的值都會自動包含指向適當的 WindowsPrincipal 物件的參照。然後我們即可在下列應用程式碼中使用此物件進行授權:
If HttpContext.Current.User.IsAuthenticated Then lblUserName.Text = HttpContext.Current.User.Identity.Name If HttpContext.Current.User.IsInRole("Manager") Then ' enable manager-level features End If Else ' deny access End If
基本上,這個程式碼與 Windows 應用程式中用來授權的程式碼相同,可是在 ASP.NET 程式碼中我們通常會透過 HttpContext 來存取 Principal 物件,而不會透過目前的執行緒 (Thread)。
使用以表單為基礎的驗證
雖然 Windows 驗證易於使用,但是對 Web 應用程式來說往往並不實用。多數 Web 應用程式服務的使用者在我們的網域或 Active Directory 內都沒有帳戶,因此我們必須實作一套自訂的安全性配置。
內建於 ASP.NET 以表單為基礎的驗證功能提供了方便的解決方案,但是我們卻發現它並沒有自動處理所有的細節。
若要使用以表單為基礎的驗證,IIS 必須允許匿名存取我們的網站。此為新虛擬根目錄 (Virtual Root) 的預設行為,可在 IIS 管理主控台中設定。
[圖 4] 啟用匿名存取
然後我們必須在 Web.config 檔案中,設定應用程式使用以表單為基礎的安全性。要完成這項設定,可變更檔案中 <authentication> 和 <authorization> 兩個項目,如下所示:
<authentication mode="Forms"> <forms name="login" loginUrl="login.aspx" protection="All" timeout="60" /> </authentication> <authorization> <deny users="?" /> <!-- Block unauthorized users --> </authorization>
在 <forms> 項目中,我們指定 loginUrl 屬性,以指向應用程式中特定的網頁。此網頁是要提示使用者提供憑證,並根據憑證驗證使用者。直到使用者經過驗證後,ASP.NET 才會自動將使用者傳送到任何存取我們網站的登入頁面。
也就是說我們需要實作 Login.aspx。此網頁需提示使用者提供憑證,一般是指使用者名稱和密碼,然後根據這項資訊來驗證使用者。請注意,由瀏覽器張貼到 Web 伺服器的資料是以純文字方式傳遞,所以必須使用 SSL 保護此頁面,將使用者密碼先進行加密,然後才從瀏覽器傳送到伺服器。
若要驗證使用者,我們可以實作一種 Login 方法,類似於用來驗證 Windows 應用程式所建立的方法,如下所示:
Public Shared Sub Login( _ ByVal username As String, ByVal password As String) Dim principal As GenericPrincipal Dim valid As Boolean ' find out if the credentials are valid If username = "rocky" AndAlso password = "lhotka" Then valid = True Else valid = False End If If valid Then ' load the user's roles Dim roles() As String ' create the Principal and Identity objects Dim identity As New GenericIdentity(username, "Custom") principal = New GenericPrincipal(identity, roles) Else ' the credentials were not valid ' so create an unauthenticated Principal/Identity Dim identity As New GenericIdentity("", "") principal = New GenericPrincipal(identity, New String() {}) End If ' set the current principal HttpContext.Current.User = principal End Sub
較之於先前的實作方法,有兩個地方不同。第一,我們並未設定 AppDomain 的主要原則,而是根據 Web.config 設定值而自動設定的。第二,我們是設定 HttpContext.Current.User,而非 Thread.CurrentPrincipal。設定 HttpContext.Current.User,可讓 ASP.NET 程式碼使用 Principal 物件,並自動設定 Thread.CurrentPrincipal,因此我們使用的任何類別庫程式碼也將有權存取相同的 Principal 物件。
如此一來,我們就可以使用 HttpContext.Current.User 判定使用者是否已經驗證過。如果已驗證過,即可讓使用者存取我們的網站。
但是,還要克服另外一個大障礙。Web 的真正本質是無狀態的,而 Principal 和 Identity 物件則是有狀態的。也就是說,我們並不需要做什麼,只要使用者一離開瀏覽 Login.aspx 頁面,這些物件就不見了。在應用程式中我們必須讓所有頁面都可使用 Principal 物件,因為該物件是我們實作授權邏輯的機制。
我們可以將每一頁的 Principal 物件儲存起來,就像儲存其他的使用者狀態一樣。主要的選項利用 Cookie 有將資料儲存在暫存的資料庫表格、ASP.NET Session 物件或瀏覽器中。而無論選擇哪個選項,在網頁載入至應用程式前,我們必須先確定 Principal 物件已設定為 HttpContext.Current.User。
最簡單的方法就是將 Principal 物件儲存在用戶端的 Cookie 中。以表單為基礎的安全性已替我們管理加密過的 Cookie,而且提供基礎結構可讓我們加入額外資料到 Cookie 中。
現在就讓我們建立一套方法,以正確地設定安全性 Cookie,並在將使用者重新導向至 Login.aspx 之前,重新導向至原先要求的網頁。
Private Sub RedirectFromLogin() ' serialize the Principal into a String value Dim principalText As String Dim buffer As New IO.MemoryStream Dim formatter As _ New Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter.Serialize(buffer, HttpContext.Current.User) buffer.Position = 0 principalText = Convert.ToBase64String(buffer.GetBuffer) ' create the ticket Dim ticket As New FormsAuthenticationTicket( _ 1, HttpContext.Current.User.Identity.Name, _ Now, DateAdd(DateInterval.Minute, 20, Now), _ False, principalText) ' Encrypt the ticket. Dim encTicket As String = FormsAuthentication.Encrypt(ticket) ' Create the cookie. Response.Cookies.Add( _ New HttpCookie(FormsAuthentication.FormsCookieName, _ encTicket)) ' Redirect back to original URL. Response.Redirect( _ FormsAuthentication.GetRedirectUrl(txtUsername.Text, False)) End Sub
我們首先要將 Principal 和 Identity 物件序列化為 MemoryStream,然後再將該資料轉換為可列印的文字。我們必須這樣做才可將 Principal 物件的資料載入至安全性 Cookie 中。
接下來要建立以表單為基礎的安全性票證。此票證包含以表單為基礎之安全性設定所需的資料,並可選擇性的加入我們提供的自訂資料。在此例中,我們提供了序列化 Principal 物件的資料。請注意,我們不僅提供 Principal 物件的資料,而且還設定票證的發出日期和時間,以及到期日期和時間。
一旦建立好票證,我們就需為它進行加密,以防止用戶端修改。請記住,票證將會儲存在用戶端電腦的 Cookie 中,所以很可能會有惡意的使用者嘗試存取或操控該票證。也請注意,密碼資料不會在 Principal、Identity 或票證物件中。
最後,我們使用加密過的票證資料建立 Cookie,並將它加入 Cookie 集合,作為此頁面的輸出。
到此,安全性物件都已設定好,我們可以使用 Response.Redirect 將使用者重新導向至原先要求的頁面。請注意,因為我們已自訂安全性 Cookie,所以必須使用 Response.Redirect 而非 FormsAuthentication.RedirectFromLoginPage。
此方法完成後,我們可以實作 Login.aspx 中 Login 按鈕的程式碼,以呼叫 Login 和 RedirectFromLogin 方法:
Private Sub btnLogin_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnLogin.Click Login(txtUsername.Text, txtPassword.Text) If HttpContext.Current.User.Identity.IsAuthenticated Then RedirectFromLogin() End If End Sub
我們取得使用者提供的憑證,並呼叫 Login 方法。接著,我們檢查使用者是否已經過驗證。如果已驗證過,我們就呼叫 RedirectFromLogin 方法來建立安全性 Cookie,並重新導向使用者至其目的地。否則,使用者將會自動停留在 Login.aspx 頁面。
到此幾乎已經完成。剩下就是要確定已從 Cookie 取得 Principal 物件,並在應用程式內執行任何頁面前,先將它設定為 HttpContext.Current.User。
若要這麼做,我們得將程式碼加入 Global.asax,以控制全域的 AcquireRequestState 事件。在執行任何頁面時就會先引發此事件,所以很適合在此事件中設定 HttpContext 使用者值。
Private Sub Global_AcquireRequestState(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.AcquireRequestState ' get the security cookie Dim cookie As HttpCookie = _ Request.Cookies.Get(FormsAuthentication.FormsCookieName) If Not cookie Is Nothing Then ' we got the cookie, so decrypt the value Dim ticket As FormsAuthenticationTicket = _ FormsAuthentication.Decrypt(cookie.Value) If ticket.Expired Then ' the ticket has expired - force user to login FormsAuthentication.SignOut() Response.Redirect("login.aspx") Else ' ticket is valid, set HttpContext user value Dim buffer As _ New IO.MemoryStream(Convert.FromBase64String(ticket.UserData)) Dim formatter As _ New Runtime.Serialization.Formatters.Binary.BinaryFormatter HttpContext.Current.User = _ CType(formatter.Deserialize(buffer), IPrincipal) End If End If End Sub
我們首先要從 Request 物件中擷取安全性 Cookie。如果順利取得 Cookie,接著就可進行票證解密,並檢查票證是否已過期。如果無法取得 Cookie,會由 ASP.NET 自動將使用者重新導向至登入頁面;如果取得的票證已過期,我們需自行將使用者重新導向至登入頁面。
假設已取得有效的票證,即可擷取 String 值,其中含有序列化的 Principal 物件。String 值會回到其二進位的形式,可用來初始化 MemoryStream 物件。接著會還原序列化資料,重新建立 Principal 和 Identity 物件,並將這兩個二物件載入至 HttpContext。
完成之後,頁面程式碼的安全性內容已備妥,我們即可使用 HttpContext.Current.User 和 Thread.CurrentPrincipal 存取授權邏輯的 Principal 物件。
使用外部驗證
雖然大多數人都使用 Windows 或自訂驗證,但仍有一些企業使用協力廠商的安全性工具 (例如 Oblix 和 SiteMinder),提供其所有網站的單一登入控制。
這些工具會將尚未驗證過的使用者,重新導向至登入伺服器進行驗證。使用者必須經過協力廠商的安全性機制驗證,才可存取我們的網站。請參考您協力廠商的產品說明,瞭解如何設定 IIS 和 ASP.NET 以正確操作其產品。
如果使用外部協力廠商的產品,我們就不必擔心有關使用 IIS 或 ASP.NET 執行使用者驗證的問題。但是,我們仍需設計一種方法,替使用者取得有效的 Principal 物件,這樣我們才可以在應用程式內撰寫授權程式碼。
通常協力廠商的產品在驗證過使用者後,會將額外的資訊加入 HTTP 標頭中。例如,根據每個頁面的要求,它們通常會加入使用者名稱值作為自訂的 HTTP 標頭值。我們可以使用這個值載入使用者的角色和身份資訊。
請記住,我們並不需要驗證使用者,因為協力廠商的產品已完成了驗證工作。我們所要做的只是載入已驗證過的使用者設定檔資料,以建立 Principal 和 Identity 物件。假使我們可以根據使用者名稱載入其資料,我們就可將如下的程式碼加入 Global.asax:
Private Sub Global_AcquireRequestState(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.AcquireRequestState Dim principal As GenericPrincipal Dim username As String ' get the username from the HTTP header username = Request.Headers.Get("USERNAME") If Len(username) > 0 Then ' load the user's roles Dim roles() As String ' create the Principal and Identity objects Dim identity As New GenericIdentity(username, "Custom") principal = New GenericPrincipal(identity, roles) ' set the current principal HttpContext.Current.User = principal End If End Sub
同樣地,在執行任何頁面程式碼之前,我們使用 AcquireRequestState 事件來設定安全性。
在此例中,我們從 HTTP 標頭擷取使用者名稱值。您將來可能需要自訂標頭名稱,以符合您協力廠商安全性產品提供的適當名稱。
如果我們取得使用者名稱值,便會利用它載入使用者角色清單。如同前面的範例,您必須自訂此程式碼,以讀取資料庫、LDAP 伺服器或其他使用者設定檔存放區中的值。
之後,可以用使用者設定檔資料來建立 GenericIdentity 和 GenericPrincipal 物件,同時可將 HttpContext.Current.User 值設定為新的 Principal 物件。現在,當頁面執行我們的程式碼時,就有權存取使用者的安全性內容。
結論
Microsoft .NET Framework 包括彈性的驗證和授權基礎,可在應用程式中用來實作安全性。我們可以選擇適合應用程式的 Windows 或自訂安全性選項。但不管選擇哪一項,透過內建在基本類別庫中的基本功能,執行授權的應用程式碼必須保持一致。
無論您是建置 Windows 或 Web 應用程式,都必須充分瞭解 Principal 和 Identity 物件,才能好好利用它們。