在充滿 Web 應用程式的世界中,除了系統層級的安全性之外,還需要其它額外的保護。應用程式層級安全性可使資源只提供給已授權使用者存取,而防止未驗證使用者存取,這種安全性也使得網頁能對個別使用者提供個人化的內容。
上個月,我在這兩篇系列文章的第一篇文章裡,說明了 ASP.NET 如何與 Windows® 驗證相結合,以提供堅若磐石的安全性。這個月我將繼續介紹表單驗證,這比較適合一班的網際網路用途,因為它不需要在伺服器網域上為使用者建立 Windows 登入帳戶即可進行驗證。
表單驗證
表單驗證是 ASP.NET 最酷的一項新功能。簡單的說,它是一種安全性機制,要求使用者在 Web 表單中輸入認證 (通常是使用者名稱和密碼) 來驗證他或她的身份。透過 Web.config 的項目,你可以驗證這個登入網頁,並且告訴 ASP.NET 哪些資源是由這個登入網頁所保護。當使用者第一次嘗試存取受保護的資源時,ASP.NET 會直接將他們重新導向到你的登入網頁。如果登入網頁成功,ASP.NET 會以 cookie 的形式發給每一個使用者一個驗證票證,然後將他們重新導向到他們原本要求的網頁。這個票證允許使用者可以重複瀏覽你的網站受保護的部分,而不需要一再地登入。你可以控制這個票證的存留時間,並由此決定一個登入的最佳有效期限。
表單驗證取代了在 ASP 應用程式中的每一個網頁頂端,加入用來檢查使用者是否已經登入的程式碼,如果使用者沒有登入,就將它們重新導向到登入網頁,在成功的登入後,就將他們重新導向到他們原本要求的網頁。這種驗證是最適合在類似 eBay 的網站上使用,在使用者檢視其個人網頁或是對某些拍賣項目出價之前,必須輸入其使用者名稱和密碼。另外,表單驗證也可以在鮮少使用 Windows 驗證的網際網路上正常運作。
簡單的範例應用程式
使用表單驗證到底有多簡單?看看在 [圖 1] 的應用程式,你就可以判斷。這個應用程式的使用者介面是由兩個網頁所組成:任何人都可以檢視的 PublicPage.aspx 與只有已驗證使用者能檢視的 ProtectedPage.aspx。已驗證使用者包括任何透過第三個網頁 LoginPage.aspx 登入的使用者,此網頁會要求輸入使用者名稱和密碼。這些有效的使用者名稱和密碼將會儲存在 Web.config。
在你潛心鑽研原始程式碼之前,先測試一下這個應用程式。下面顯示如何進行測試:
- 將 PublicPage.aspx、LoginPage.aspx 和 Web.config (應用程式根目錄) 複製到 wwwroot 或是一個虛擬目錄。
- 在這個虛擬的根目錄中建立一個叫做 Secret 子目錄,然後複製 ProtectedPage.aspx 和 Web.config (Secret 子目錄) 到該子目錄。
- 在瀏覽器中呼叫 PublicPage.aspx。如果你是將它複製到 wwwroot,這個 URL 應該是 http://localhost/publicpage.aspx。
- 按一下 [View Secret Message] 按鈕。
- 「View Secret Message」使用 Response.Redirect 導向到 Secret/ProtectedPage.aspx。但是因為 ProtectedPage.aspx 只能被已驗證使用者來檢視,ASP.NET 會在 LoginPage.aspx 顯示登入表單 (請參閱 [圖 2])。
![[圖 2] 登入表單](aspsec2fig02.gif)
[圖 2] 登入表單
- 在使用者名稱欄位中輸入「Jeff」,並在密碼欄位中輸入「imbatman」。
- 出現 ProtectedPage.aspx。因為你現在是一個已驗證的使用者,而且伴隨後續要求所發出的驗證票證,都會儲存在 cookie 裡。
- 回到 PublicPage.aspx。
- 再次按下 [View Secret Message] 按鈕。
- ProtectedPage.aspx 再次出現。但是這一次並不會要求你輸入使用者名稱和密碼。為什麼?因為與這項要求一起傳送的驗證 cookie 使得 ASP.NET 表單驗證模組 (在每項要求中接聽) 可以識別出你是一個已驗證的使用者,並且識別出你是 Jeff,注意這個個人化歡迎網頁 (請參閱 [圖 3])。
![[圖 3] 歡迎 Jeff 的網頁](aspsec2fig03.gif)
[圖 3] 歡迎 Jeff 的網頁
- 關閉瀏覽器,然後再重新啟動瀏覽器並呼叫 PublicPage.aspx。
- 再一次按下 [View Secret Message] 按鈕,你會再次被要求登入,因為這個包含你的驗證票證的 cookie 是一個工作階段 cookie,這表示當你關閉瀏覽器時,這個 cookie 會消失。
要防止未驗證使用者檢視 ProtectedPage.aspx,並在他們嘗試呼叫這個網頁時,將他們導向到登入網頁,需要做多少事情?真的並不多。這個訣竅在於 Web.config,更具體的說,是伴隨這個應用程式的那兩個 Web.config 檔案。應用程式根目錄的 Web.config 檔案能夠賦予表單驗證以及驗證登入網頁的能力:
<authentication mode="Forms">
<forms loginUrl="LoginPage.aspx">
...
</forms>
</authentication>
它也包含一個 <credentials> 區段來列出有效的使用者名稱和密碼:
<credentials passwordFormat="Clear">
<user name="Jeff" password="imbatman" />
<user name="John" password="redrover" />
<user name="Bob" password="mxyzptlk" />
<user name="Alice" password="nomalice" />
<user name="Mary" password="contrary" />
</credentials>
這個在 Secret 子目錄的 Web.config 檔案,在應用程式的安全性上扮演一個同等重要的角色。在這個檔案裡,下面的陳述式代表 URL 授權。
<authorization>
<deny users="?" />
</authorization>
這個陳述式指示 ASP.NET URL 授權模組 (System.Web.Security.UrlAuthorizationModule) 拒絕未驗證的使用者來存取任何在這個目錄的 ASP.NET 檔案。這裡的「?」代表匿名使用者,也就是未驗證的使用者。當有人嘗試檢視這個目錄中的檔案時,ASP.NET 會檢查這個要求是不是附帶一個有效的驗證。如果存在 cookie,ASP.NET 會將它解密並驗證它,以確保它沒有被竄改,並且擷取分派給目前這個要求的身份資訊 (加密與驗證可以關閉,但預設上是啟動的)。如果不存在 cookie,ASP.NET 會將這個要求重新導向到登入網頁。
實際上的驗證 (要求輸入一個使用者名稱和密碼,然後檢查它們的有效性) 是由 LoginPage.aspx 來完成的。下面的陳述式將這個使用者輸入的使用者名稱和密碼,傳遞到一個叫做 Authenticate 的靜態方法 System.Web.Security.FormsAuthentication,如果這個使用者名稱和密碼是有效的,這個方法的傳回值為「true」 (這表示,如果它們出現在 Web.config 的 < credentials > 區段中),如果它們不是有效的,則傳回值為「false」:
if(FormsAuthentication.Authenticate(UserName.Text,Password.Text))
如果 Authenticate 傳回值為「true」,下一個陳述式會建立一個驗證 cookie,並將它附加到發出的回應,然後將這個使用者重新導向到他們原本要求的網頁:
FormsAuthentication.RedirectFromLoginPage(UserName.Text,false);
傳遞 RedirectFromLoginPage 的第二個參數會指定這個驗證是否為一個工作階段 cookie (參數值為 False 時) ,還是一個永久 cookie (參數值為 True 時)。許多使用表單驗證的網站會對使用者顯示一個核取方塊,讓他或是她來決定使用哪一種類型的 cookie。如果你看到一個標示「保留我在此網站的登入狀態」或是其他有這種效用的核取方塊,選取這個選項通常會產生一個驗證 cookie,而這個 cookie 的存留時間與瀏覽工作階段是無關的。
ProtectedPage.aspx 是 Secret 子目錄中唯一的 ASPX 檔案,但是如果這個子目錄中還有其它的檔案,它們也會受到這個登入表單的保護。保護是以一個個的目錄為基礎而進行套用的,要在兩組檔案實施兩種不同的保護等級,你需要將這些檔案放進不同的目錄。在每一個目錄中的 Web.config 檔案會指定保護說明檔案的確切方式。
真實世界的表單驗證
在前面一段中的應用程式實際上並不可行,以純文字形式來儲存密碼是不合理的。ASP.NET 有一個修正檔可改善這個問題,但卻由於另外一個問題使得它仍然不切實際 - 那就是在 Web.config 中儲存數千個 (或是數百萬個) 名稱和密碼是非常不切實際的。在實際應用上,你可能將這些資訊儲存在資料庫中。這段文章是有關如何將使用者名稱和密碼儲存在資料庫裡,而且仍然使用表單驗證。
[圖 4]
顯示這個應用程式的修改版本,並將使用者名稱和密碼儲存在一個叫做 WebLogin 的 Microsoft® SQL 伺服器 資料庫。這個資料庫的 Users 表格包含所有有效使用者名稱與密碼的清單。(參閱 [圖 5]).
![[圖 5] SQL 伺服器資料庫](aspsec2fig05.gif)
[圖 5] SQL 伺服器資料庫
只有兩個原始碼檔案有改變 - LoginPage.aspx 與 Web.config (在應用程式根目錄的那一個),其它檔案完全保持相同,所以它們不會出現在程式碼中。Web.config 將不再有包含使用者名稱和密碼的 <credentials> 區段,LoginPage.aspx 也不再使用 FormsAuthentication.Authenticate 來驗證使用者的身份,它會呼叫一個叫做 CustomAuthenticate 的本機方法來替代,而這個方法會使用 SQL 查詢來判定這個身份是否有效。如果這個使用者在使用者名稱欄位中輸入「Jeff」,並在密碼欄位中輸入「imbatman」,這個查詢看起來會像是這樣:
select count (*) from users where username = 'Jeff' and
cast(rtrim(password) as varbinary)=cast('imbatman' as varbinary)
這個查詢會傳回若干筆記錄,包含 [使用者名稱] 欄位的 Jeff 與 [密碼] 欄位的 imbatman。傳回值 1 表示這個身份是有效的,傳回值 0 表示它們是無效的,因為在資料庫中沒有這筆記錄。
在這個查詢中使用 CAST 運算子的目的在於使密碼比能區分大小寫字母,依預設,大部分的 SQL 資料庫在執行字串比較時是忽略大小寫的。將字串轉換成 varbinary,使 SQL 將它們視為二進位值而不是字串,這是一個能對字串不區分大小寫比較的常見技巧。RTRIM 運算子用來去除 [密碼] 欄位值中尾端多餘的空白,SQL 在比較字串時,會忽略尾端多餘的空白,但在執行二進位比較時則不會。將這個密碼轉換成 varbinary 還要防止以實際的 SQL 命令來偽裝成密碼。(至少我認為這樣可以防止這樣的欺騙行為;你永遠不會知道做壞事的人可以想出多麼巧妙的因應措施。為了保險起見,將 RegularExpressionValidators 放在 TextBox 控制項來拒絕任何包含除了字母和數字以外的任何輸入。為了使表單有效,你也可以多丟幾個 RequiredFieldValidators。
這個版本的 LoginPage.aspx 還有一個在前一個版本沒有的特性:一個讓使用者決定驗證 cookie 是暫時的或是永久的核取方塊。LoginPage.aspx 會將這個核取方塊的 Checked 屬性傳遞到 RedirectFromLoginPage:
FormsAuthentication.RedirectFromLoginPage (UserName.Text,
Persistent.Checked);
選取這個選項能夠傳遞一個為 True 的值到 RedirectFromLoginPage,並產生一個永久的驗證,沒有選取這個選項則會透過傳回一個為 False 的值,來產生一個暫時的 (工作階段 ) 驗證 cookie。在登入前選取這個選項,然後你將不需要一而再地登入就能回到 ProtectedPage.aspx,甚至如果你關閉機器而不再使用幾天以後也是如此。
在測試新版的應用程式之前,你必須建立 WebLogin 資料庫。下載的程式碼包括一個叫做 WebLogin.sql 的指令檔,能夠替你建立這個資料庫,只要開啟命令列提示視窗,移到 WebLogin.sql 所在的目錄,然後輸入
osql -U sa -P -i weblogin.sql
依預設,這個指令檔所建立的資料庫在 C 磁碟上。如果你將 SQL 伺服器安裝在其他磁碟,並且希望這個資料庫同樣建立在該處,請用記事本開啟 WebLogin.sql,然後在下面的陳述式中修改磁碟機代號:
FILENAME = 'C:\program files\...\weblogin.mdf',
這個安裝的指令檔當然要在你的 PC 上安裝 SQL 伺服器才能運作。
驗證 Cookie 的存留時間
當你呼叫 RedirectFromLoginPage 並且傳遞第二個參數值為 False 時,ASP.NET 會發佈一個包含時間戳記的工作階段驗證,來限制這個 cookie 的有效期間為 30 分鐘,甚至是延續這個瀏覽工作階段超過此期限。30 分鐘的到期時間是由附加在 Machine.config 中 <forms> 元素的 timeout 屬性所控制:
<forms ... timeout="30">
你可以藉由修改 Machine.config 或是在本機的 Web.config 檔案中加入 timeout 屬性,來改變逾時時間。下面的 Web.config 檔案能啟動表單驗證,並且將驗證 cookie 的有效性延長到七天 (10,080 分鐘):
<configuration>
<system.web>
<authentication mode="Forms">
<forms loginUrl="/LoginPage.aspx" timeout="10080" />
</authentication>
</system.web>
</configuration>
當一個工作階段逾期時間 cookie 在隨後的要求中被傳回到 ASP.NET,如果這個 cookie 的存留時間超過一半,ASP.NET 會自動將它更新 (更新該時間戳記)。如此,即使預設的逾期時間是 30 分鐘,只要瀏覽器保持開啟,而且至少每隔15 分鐘就傳送這個 cookie 到 ASP.NET,你還是可以持續存取一個受到保護的網頁達數小時之久。
如果這個使用者在前一段應用程式的登入網頁中選取 [保留我的登入狀態] 」這個選項,LoginPage.aspx 會傳遞一個為真的值到 FormsAuthentication.RedirectFromLoginPage 來產生一個永久的驗證。這裡再一次列出該陳述式:
FormsAuthentication.RedirectFromLoginPage (UserName.Text,
Persistent.Checked);
這種產生一個永久驗證的方法的缺點之一是,這個 cookie 在 50 年後依然有效。此外,沒有讓你可以改變這個缺點的組態設定,timeout 屬性對一個永久驗證 cookie 是沒有效果的。假設你希望產生一個永久驗證 cookie,但是你也希望限制它的存留時間,比方說七天。你應該要怎麼做?
解決辦法是在回應被傳回之前,用程式修改這個驗證 cookie。[圖 6] 顯示一個修改過的 OnLogIn (在使用者按下 LoginPage.aspx 登入按鈕時所呼叫的處理常式) 可以將這個驗證 cookie 的存留時間設定成七天 (當然我們可以假設這個 cookie 是永久的)。
如果 CustomAuthenticate 傳回值為 True,表示這個使用者輸入有效的身份識別,這個版本的 OnLogIn 使用 FormsAuthentication.GetRedirectUrl 來擷取使用者原本要求的網頁的 URL,然後呼叫 FormsAuthentication.SetAuthCookie 來建立一個驗證 cookie,並將它加到在回應中所發出的 cookies。在呼叫 Response.Redirect 移到要求的網頁之前,OnLogIn 從這些回應的 Cookie 集合擷取這個 cookie,並將它的 Expires 屬性設定成從現在起七天來修改這個 cookie。這個簡單的修改能確保使用者在七天之後必須再一次登入網頁。當然,你可以修改 TimeSpan 的值,把存留時間設定成任何你想要的期間,在這篇文章最後的範例程式中,你將會看到這個技巧再次被使用,但這裡還有一些我必須先涵蓋的主題:角色安全性。
表單驗證與角色安全性
前面的程式範例說明了如何結合表單驗證與儲存在 SQL 伺服器資料庫的使用者名稱和密碼。下面這一個範例則顯示如何使用角色身份來允許某些使用者檢視 ProtectedPage.aspx 卻對其他人隱藏。
下面的陳述式在 Secret 目錄的 Web.config 檔案中,能防止未驗證的使用者存取這個目錄裡的 ASPX 檔案:
<deny users="?" />
這個陳述式的唯一問題是:它會允許任何已驗證的使用者來檢視 ProtectedPage.aspx。在某些情況下,你可能想要允許部份已驗證的使用者來檢視 ProtectedPage.aspx,而不是允許所有已驗證的使用者來檢視這個網頁,這並不是不可能。假設 John 和 Alice 是管理人員,他們應該能夠呼叫 ProtectedPage.aspx,但是 Jeff、Bob 和 Mary 是開發人員,他們應該不能呼叫這個網頁。排除 Jeff、Bob 和 Mary 的一個方法是拒絕所有使用者的存取 (users="*"),但卻明確地允許 John 和 Alice 來存取。這裡是完成這個需求的 Web.config 檔案:
<configuration>
<system.web>
<authorization>
<allow users="John, Alice" />
<deny users="*" />
</authorization>
</system.web>
</configuration>
你也可以明確地拒絕 Jeff、Bob 和 Mary 來存取:
<configuration>
<system.web>
<authorization>
<deny users="Jeff, Bob, Mary" />
<allow users="*" />
</authorization>
</system.web>
</configuration>
當你以這個方法使用 <allow> 和 <deny> 時要注意,這些項目是有順序性的。下面這個陳述式
<deny users="*" />
<allow users="John, Alice" />
不等於
<allow users="John, Alice" />
<deny users="*" />
因為 ASP.NET 會在<deny users="*"> 這一行停止,然後忽略任何出現在它之後的陳述式。
這些 Web.config 檔案可以正常運作,但它們對服務大量使用者的網站是不切實際的。只要想像每次有人進入公司、離職或是獲得晉升時,就得編輯數百萬位元大小的 Web.config 檔案,你就知道這實在是一場夢魘。對大型網站而言,角色提供一個實際可行的方案,能解決只允許某些已驗證的使用者來存取,而不是允許所有已驗證使用者來存取的問題。而且角色可以和表單驗證一起正常運作,假使你願意撰寫一些程式碼來解決問題的話。
讓我們再次看看為我的網站提供服務的 WebLogin 資料庫 (在 [圖 5] 中顯示)。除了可以儲存使用者名稱和密碼之外,Users 表格有一個 Role 欄位能儲存每一個使用者的角色成員,如果有的話,John 與 Alice 會被指派為管理人員角色,而 Jeff、Bob 與 Mary 則會被指派為開發人員角色。有可能使用這些角色成員允許 John 和 Alice (和任何被指派到管理人員角色的人) 來存取 ProtectedPage.aspx,而使其他人不能存取嗎?你可以猜到!只需在我已撰寫好的程式碼,做兩個簡單的修改就可以做到。
第一個步驟比較簡單,需要編輯 Secret 子目錄中的 Web.config 檔案來允許 manager 成員存取或是拒絕 developer 成員存取。下面是允許 manager 成員存取的 Web.config 檔案:
<configuration>
<system.web>
<authorization>
<allow roles="Manager" />
<deny users="*" />
</authorization>
</system.web>
</configuration>
角色屬性會取代使用者屬性,而且不僅是對個別使用者允許或是拒絕存取,對於被指派一個或多個角色的使用者群組來說也是如此。
第二個步驟比較複雜。你必須用某種方法將儲存在資料庫中的角色和每一個要求中的使用者帳戶來做對應,所以 ASP.NET 可以決定這個要求者是管理人員成員還是開發人員成員。放置這個對應動作最佳的地方是在每一個要求開始時啟動的 Application_AuthenticateRequest 事件中,你可以用自訂的 HTTP 模組或是在 Global.asax 中來處理 Application_AuthenticateRequest 事件。在 [圖 7] 中的程式碼顯示 Global.asax 檔案中的表單驗證是根據角色來進行驗證。
它是如何運作的呢?在確認使用者的確已經驗證過 (對表單驗證來說,「經過驗證」表示一個附屬於這個要求的有效驗證 cookie),而且這個驗證是由表單驗證所執行時,Application_AuthenticateRequest 會從這個 cookie 擷取使用者名稱,它並沒有直接地碰觸這個 cookie,相反的,它將 User.Identity 轉換成 FormsIdentity,只要我們使用表單驗證來驗證使用者,並且從這個 FormsIdentity 物件的 Name 屬性中讀取使用者名稱,這個轉換就可以正常運作。
如果這個使用者名稱是 Jeff,Application_AuthenticateRequest 會建立一個包含「開發人員」角色名稱的新的 GenericPrincipal 物件,然後將它指定給現在這個要求的 HttpContext 的 User 屬性。GenericPrincipal 是設計用來讓使用者身份不受正在使用的驗證通訊協定所支配。當在這個要求中執行的程式碼嘗試重新導向到 ProtectedPage.aspx,ASP.NET 會比較在 GenericPrincipal 的角色名稱和從 Web.config 得到的角色名稱。由於 Jeff 是一個開發人員成員,但 Secret 目錄的 Web.config 檔案卻只允許管理人員成員來存取這個目錄,Jeff 會被拒絕存取 ProtectedPage.aspx。但你只要將這個陳述式
app.Context.User = new GenericPrincipal (identity,
new string[] { "Developer" });
修改成為下面的陳述式,然後 Jeff 就可以正常的檢視 ProtectedPage.aspx。
app.Context.User = new GenericPrincipal (identity,
new string[] { "Manager" });
[圖 8] 包含 PublicPage/ProtectedPage 應用程式第三個最終的版本,它包括前面版本所沒有的三個特性:
- 它從 WebLogin 資料庫擷取角色,然後指派它們給後續傳入的要求 (假設這些要求是使用表單驗證來驗證)。
- 它的 Web.config 檔案 (在 Secrets 目錄的那一個) 允許管理人員成員來存取,但對他人則否。
- 如果選取登入網頁的 [保留我的登入狀態] 方塊,它將傳回一個存留期間為 7 天而不是 50 年的驗證 cookie。
要實際體驗運作中的角色
型安全性,按下 PublicPage.aspx 中的 [View Secret Message] 按鈕,並在登入網頁中輸入「Jeff」和「imbatman」。因為 Jeff 在資料庫中是一個開發人員,所以不能夠檢視 ProtectedPage.aspx。但如果以 John 來登入 (密碼是「redrover」) ,你將可以正常檢視 ProtectedPage.aspx。為什麼?因為 John 的角色是管理人員,而管理人員被特別允許來存取 Secrets 目錄中的資源。
這裡順便一提,如果你越過登入表單而直接按下 [View Secret Message] 按鈕,你將會被直接導回 ProtectedPage.aspx,這是因為當你測試前一版應用程式時所產生的 cookie,還是會將你識別成一個已驗證的使用者。如果它是一個工作階段 cookie,只要重新啟動你的瀏覽器來破壞該 cookie 而且讓你再次看到登入網頁。如果它是一個永久 cookie,你必須刪除它,要做到這樣最簡單的方法是使用瀏覽器刪除 Cookies 的命令。在 Microsoft Internet Explorer,你可以在[工具]的[Internet 選項]中找到它。
事實上,你可能寧願在最上層的 Web.config 檔案中合併所有的 URL 授權,而不是將它們分散在個別目錄中的 Web.config 檔案。ASP.NET 也支援這麼做。圖 9 包含一個在應用程式根目錄的 Web.config 檔案,它能夠啟動表單驗證,並且明確地指出只允許管理人員成員存取 Secret 子目錄中的資源。
在一個 Web.config 檔案中明確指出多個目錄的組態設定的能力並不限於 URL 授權,它也可以和其它組態設定一起運作。
多重角色
在組織裡遇到員工有 (或可以有) 多重角色並不是非常罕見。Alice 有可能是一個管理人員成員,但她也有可能是一個開發人員成員,或至少擁有開發人員成員可存取資源的權限。ASP.NET 特有的角色安全性是否能支援多重角色成員?它當然有支援。傳遞到 GenericPrincipal 建構函式的第二個參數並不是一個字串,它是一個字串所組成的陣列。要指出一個特定的安全性原則 (使用者) 到底是屬於兩個或是更多的角色,只需要傳送一個由角色名稱所組成的陣列,如下面所示:
app.Context.User = new GenericPrincipal (identity,
new string[] { "Developer", "Manager" });
Alice 現在可以存取任何管理人員或是開發人員成員所可以存取的資源。
你也可以使用 <allow> 和 <deny> 元素來允許或是拒絕多重角色來存取。例如,在下面 Web.config 檔案中的陳述式就能夠允許開發人員和管理人員成員存取,卻對其它任何人拒絕存取。
<allow roles="Manager, Developer" />
<deny users="*" />
登出
許多依賴表單形式驗證的網站允許使用者如同登入一般地來登出。呼叫任何 FormsAuthentication 方法替每一個回應附加上一個驗證 cookie,來有效地讓使用者登入。FormsAuthentication.SignOut 方法則剛好相反:它會將一個已驗證的使用者登出,並傳回一個 Set-Cookie 標頭來將這個 cookie 的內容值設定成一個空字串,並且將這個 cookie 的逾期日期設定成一個過去的日期,有效地終結這個驗證 cookie。這裡是目前使用者按下 Web 表單 Log Out 按鈕登出時的程式碼片段:
<asp:Button Text="Log Out" OnClick="OnLogOut" RunAt="server" />
<script language="C#" runat="server">
void OnLogOut (Object sender, EventArgs e)
{
FormsAuthentication.SignOut ();
}
</script>
這個結果表示這個使用者下一次檢視網站中受到保護的部分時,他或是她將必須再一次登入。
驗證 Cookie 的安全性
<forms> 元素支援在 [圖 10] 中所列出的五種屬性。這些屬性大部分是不解自明的,但保護屬性特別值得提起,它能夠設定你想要的保護等級來保護 ASP.NET 用來識別已驗證使用者的驗證 cookie。預設值是「All」,它會指示 ASP.NET 對驗證 cookie 做加密與驗證。驗證可以像檢視狀態下的驗證 cookie 一樣正常運作:<machineKey> 元素的 validationKey 會被附加在這個 cookie,這個結果值會經過雜湊計算並且被附加到 cookie 中。當這個 cookie 在一項要求中被傳回時,ASP.NET 會對此 cookie 重新做雜湊運算,並且與原來伴隨在這個 cookie 的雜湊值做比較,來核對它有沒有被竄改。加密是用 machineKey 的 decryptionKey 屬性對 cookie (雜湊值和全部) 加密來完成。
驗證比加密和防止竄改消耗較少的 CPU 時間,然而,它並不能防止某些人攔截驗證 cookie 並且讀取它的內容。儘管如此,如果你想要 ASP.NET 去驗證而不只是將驗證 cookie 加密,可以把 <forms> 元素的 protection 屬性設定如下:
<forms ... protection="Validation" />
加密也提供了雙重保障來對抗竄改和防止這個 cookie 的內容被讀取。如果你想要 ASP.NET 將驗證 cookie 加密但要略過這個驗證程序,應該要這麼做:
<forms ... protection="Encryption" />
最後,如果你不想要執行驗證,也不想要加密,可以這樣做:
<forms ... protection="None" />
當驗證 cookie 透過 HTTPS 傳遞時,這個內容值「None」是非常有用的。畢竟,我們並不需要將它們加密兩次。
說到 HTTPS,加密過的 cookie 並不能被讀取或是被修改,但它們可能會被竊取,並且被非法使用。逾期時間是 cookie 所能提供的唯一保護來預防被重新執行攻擊,而且它們只能被應用在工作階段 cookie。要防止某些人用偷來的驗證 cookie 來欺騙你的網站,幾乎所有可靠的方法都是使用一個加密的通訊連結。如果你不願意在網站所有地方使用加密通訊,至少應該要考慮使用 HTTPS 來傳送使用者名稱與密碼。(當你在商業網站上看到有 [使用安全連結登入] 的按鈕時,就是在做這件事情。) 下面的 <forms> 元素透過安全性通道來連結登入網頁,並且保護純文字形式的使用者名稱和密碼不被竊取:
<forms ...
loginUrl="https://www.wintellect.com/secure/login.aspx" />
當然,這必須假設你的伺服器支援 HTTPS,而且 Login.aspx 被儲存在設定使用 HTTPS 的目錄中。
這個路徑屬性也能夠在強化驗證 cookie 的安全性上扮演一個重要的角色。比如說,你將公用的檔案放置在虛擬的根目錄,而且將要保護的檔案放在一個設定使用 HTTPS 的子目錄。如果你接受預設的路徑 /,而不是 Secret 目錄,你所得到的這個驗證 cookie,是所有對此網站發出的要求來傳送的。入侵者可以在傳遞公用網頁的過程裡攔截 cookie,然後使用它來獲得存取受保護網頁的權限。這裡是解決方案:
<forms path="/Secret" />
現在這個 cookie 只會在 Secret 子目錄與它子目錄裡的資源所發出的要求中傳送,這意味者它只有能在安全通道中傳送。
注意事項
我將以有關表單驗證的警告字眼作結束—一些十分重要且需瞭解的事項, 但卻很容易被許多人所忽略。
表單驗證只能保護 ASP.NET 檔案。我再說一次:表單驗證只能保護 ASP.NET 檔案。它能夠保護 ASPX 檔案、ASMX 檔案與其他 ASP.NET 註冊的檔案類型,但它不會保護不屬於 ASP.NET 的檔案—例如,副檔名是 HTM 或是 HTML 的檔案。如果你想要親自證實這一點,你可以在表單驗證範例中的 Secret 目錄放置一個 ProtectedPage.html 檔案,你必須通過登入網頁才能檢視 ProtectedPage.aspx,但要檢視 ProtectedPage.html 卻不需要登入。為何要這樣?理由是因為如果要求的檔案並沒有在 ASP.NET 註冊,則 ASP.NET 決不會去查看這些檔案 (因此不能攔截和重新導向)。
一個可行的解決方案是將檔案的副檔名從 ASPX 改成 HTML,並使用表單驗證來保護其它你想要的非 ASPX.NET 檔案。當你存取這些檔案時,會帶來一些額外的工作,但至少你沒有將他們遺棄與不受保護。
結論
安全性是 Web 應用程式設計中不可或缺的要素,並不是一個事後的想法。如果你在公司內部網路上建立一個 ASP.NET 應用程式,而且希望某些網頁有安全性的存取,你可以留意 Windows 驗證和存取控制清單授權來提供你所需要的保護。如果它是一個你已經組合起來的 eBay 類型應用程式—也就是一個設計用來服務大量使用者的應用程式—表單驗證和 URL 授權就是這張入場券。無論你選擇哪一個,ASP.NET 都能勝任這個工作。
|