保護使用驗證和授權的應用程式
由 Microsoft 提供
這是免費的 "NerdDinner" 應用程式教學課程的第 9 個步驟,詳細介紹了如何使用 ASP.NET MVC 1 建置一個小型但完整的 Web 應用程式。
步驟 9 示範如何新增驗證和授權來保護 NerdDinner 應用程式,讓使用者必須註冊並登入網站以建立新的 Dinners,而且之後只有裝載 Dinner 的使用者可以進行編輯。
如果使用 ASP.NET MVC 3,建議遵循 MVC 3 使用者入門或 MVC Music 市集教學課程。
NerdDinner 步驟 9:驗證和授權
現在,我們的 NerdDinner 應用程式使造訪網站的任何人,都能夠建立和編輯任何 Dinner 的詳細資料。 讓我們變更此專案,使用者必須註冊和登入網站才能建立新的 Dinners,並新增限制,只有裝載 Dinner 的使用者以後才能進行編輯。
為實現此目的,我們將使用驗證和授權來保護應用程式的安全。
了解驗證和授權
驗證是識別及驗證存取應用程式之用戶端身分識別的流程。 更簡單地説,它是關於當終端使用者造訪網站時識別「他們是誰」。 ASP.NET 支援多種方式來驗證瀏覽器使用者。 如果是網際網路 Web 應用程式,最常使用的驗證方法稱為「表單驗證」。 「表單驗證」可讓開發人員在其應用程式中撰寫 HTML 登入表單,然後驗證終端使用者提交給資料庫或其他密碼認證存放區的使用者名稱/密碼。 如果使用者名稱/密碼組合正確,開發人員接著可以要求 ASP.NET 發出加密的 HTTP Cookie,以便在未來的要求中識別使用者。 我們將使用表單驗證搭配 NerdDinner 應用程式。
授權是判斷已驗證使用者是否有權存取特定 URL/資源或執行某些動作的流程。 例如,在我們的 NerdDinner 應用程式中,我們想要授權只有登入的使用者才能存取 /Dinners/Create URL 並建立新的 Dinners。 我們也想要新增授權邏輯,只有裝載 Dinner 的使用者才可進行編輯,並拒絕所有其他使用者的編輯存取權。
表單驗證和 AccountController
ASP.NET MVC 的預設 Visual Studio 專案範本,會在建立新的 ASP.NET MVC 應用程式時自動啟用表單驗證。 它也會自動將預先建置的帳戶登入頁面實作新增至專案,讓您輕鬆地整合網站內的安全性。
預設的 Site.master 主版頁面會在存取網站的使用者未通過驗證時,在網站的右上方顯示 [登入] 連結:
按一下 [登入] 連結會將使用者帶到 /Account/LogOn URL:
尚未註冊的訪客可以按一下 [註冊] 連結來執行此動作,並將其帶往 /Account/Register URL,並允許他們輸入帳戶詳細資料:
按一下 [註冊] 按鈕會在 ASP.NET 成員資格系統中建立新使用者,並使用表單驗證在網站中驗證使用者。
當使用者登入時,Site.master 會將頁面右上角變更為輸出 "Welcome [username]!" 訊息,並轉譯 [登出] 連結,而不是 [登入] 連結。 按一下 [登出] 連結會將使用者登出:
上述登入、登出和註冊功能是由 Visual Studio 在建立專案時新增的 AccountController 類別中實作的。 AccountController 的 UI 是使用 \Views\Account 目錄中的檢視範本實作的:
AccountController 類別會使用 ASP.NET Forms 驗證系統來發出加密的驗證 Cookie,以及 ASP.NET 成員資格 API 來儲存和驗證使用者名稱/密碼。 ASP.NET 成員資格 API 是可延伸的,並可讓您使用任何密碼認證存放區。 ASP.NET 隨附內建成員資格提供者實作,這些實作會將使用者名稱/密碼儲存在 SQL 資料庫或 Active Directory 內。
我們可以設定 NerdDinner 應用程式應該使用的成員資格提供者,方法是在專案的根目錄開啟 "web.config" 檔案,並尋找其中的<成員資格>區段。 建立專案時新增的預設 web.config 會註冊 SQL 成員資格提供者,並將它設定為使用名為 “ApplicationServices” 的連接字串來指定資料庫位置。
預設的 “ApplicationServices” 連接字串 (這是在 web.config 檔案的 <connectionStrings> 區段內指定) 被設定為使用 SQL Express。 它指向應用程式的 "App_Data" 目錄下名為 "ASPNETDB.MDF" 的 SQL Express 資料庫。 如果此資料庫在應用程式內第一次使用成員資格 API 時不存在,ASP.NET 會自動建立資料庫,並在其中布建適當的成員資格資料庫架構:
如果不是使用 SQL Express,而是要使用完整的 SQL Server 執行個體 (或連接到遠端資料庫),我們只需要更新 web.config 檔案內的 “ApplicationServices” 連接字串,並確定已將適當的成員資格架構新增至它所指向的資料庫。 您可以在 \Windows\Microsoft.NET\Framework\v2.0.50727\ 目錄中執行 “aspnet_regsql.exe” 公用程式,將成員資格和其他 ASP.NET 應用程式服務的適當架構新增至資料庫。
使用 [授權] 篩選器來授權 /Dinners/Create URL
我們無需編寫任何程式碼即可為 NerdDinner 應用程式啟用安全驗證和帳戶管理實作。 使用者可以使用我們的應用程式註冊新帳戶,以及登入/登出網站。
現在,我們可以將授權邏輯新增至應用程式,並使用訪客的驗證狀態和使用者名稱來控制他們在網站內無法執行的動作。 首先,我們將授權邏輯新增到 DinnersController 類別的 [建立] 動作方法中。 具體而言,我們將要求使用者必須登入才能存取 /Dinners/Create URL。 如果他們未登入,我們會將其重新導向至登入頁面,讓他們可以登入。
實作此邏輯相當簡單。 我們只需要將 [授權] 篩選器屬性新增至 [建立] 動作方法,如下所示:
//
// GET: /Dinners/Create
[Authorize]
public ActionResult Create() {
...
}
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(Dinner dinnerToCreate) {
...
}
ASP.NET MVC 支援建立 [動作篩選] 的功能,可用於實作可重複使用邏輯,並以宣告方式套用於動作方法。 [授權] 篩選器是 ASP.NET MVC 所提供的其中一個內建動作篩選器,它可讓開發人員以宣告方式將授權規則套用至動作方法和控制器類別。
在沒有任何參數的情況下套用時 (如上),[驗證] 篩選器強制發出動作方法要求的使用者必須登入,否則它會自動將瀏覽器重定向到登入 URL。 執行此重新導向時,原始要求的 URL 會以查詢字串引數的形式傳遞 (例如:/Account/LogOn?ReturnUrl=%2fDinners%2fCreate)。 接著,AccountController 會在使用者登入後,將其重新導向回原始要求的 URL。
[授權] 篩選器可以選擇支援指定「使用者」或「角色」屬性的功能,這些屬性可以用來要求使用者必須已登入,並且是允許的使用者清單中的成員或屬於允許的安全角色成員。 例如,下列程式碼只允許兩個特定使用者 "scottgu" 和 “billg” 存取 /Dinners/Create URL:
[Authorize(Users="scottgu,billg")]
public ActionResult Create() {
...
}
然而,在程式碼中嵌入特定的使用者名稱往往非常難以維護。 更好的方法是定義程式碼檢查的更高級別的 [角色],然後使用資料庫或 Active Directory 系統將使用者對應到角色 (使實際的使用者對應清單能夠從程式碼外部儲存)。 ASP.NET 包含內建角色管理 API,以及一組內建的角色提供者 (包括 SQL 和 Active Directory 的角色提供者),可協助執行此使用者/角色對應。 然後,我們可以更新程式碼,只允許特定 [系統管理員] 角色內的使用者存取 /Dinners/Create URL:
[Authorize(Roles="admin")]
public ActionResult Create() {
...
}
使用建立 Dinners 時的 User.Identity.Name 屬性
我們可以使用控制器基底類別上公開的 User.Identity.Name 屬性,擷取要求的目前登入的使用者名稱。
稍早,當我們實作 Create() 動作方法的 HTTP-POST 版本時,我們已將 Dinner 的 “HostedBy” 屬性硬式編碼為靜態字串。 我們現在可以將此程式碼更新為改用 User.Identity.Name 屬性,並自動為建立 Dinner 的主機新增回覆:
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(Dinner dinner) {
if (ModelState.IsValid) {
try {
dinner.HostedBy = User.Identity.Name;
RSVP rsvp = new RSVP();
rsvp.AttendeeName = User.Identity.Name;
dinner.RSVPs.Add(rsvp);
dinnerRepository.Add(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
}
}
return View(new DinnerFormViewModel(dinner));
}
由於我們已將 [授權] 屬性新增至 Create() 方法,ASP.NET MVC 可確保只有在造訪 /Dinners/Create URL 的使用者登入網站時,才會執行動作方法。 因此,User.Identity.Name 屬性值一律會包含有效的使用者名稱。
使用編輯 Dinners 時的 User.Identity.Name 屬性
現在,讓我們新增一些授權邏輯來限制使用者,以便他們只能編輯自己裝載的 Dinners 屬性。
為了協助解決此問題,我們會先將 “IsHostedBy(username)” 協助程式方法新增至 Dinner 物件 (在我們稍早建置的 Dinner.cs 部分類別內)。 根據提供的使用者名稱是否符合 Dinner HostedBy 屬性,這個協助程式方法會傳回 true 或 false,並封裝執行它們之間不區分大小寫的字串比較所需的邏輯:
public partial class Dinner {
public bool IsHostedBy(string userName) {
return HostedBy.Equals(userName, StringComparison.InvariantCultureIgnoreCase);
}
}
接著,我們將 [授權] 屬性新增至 DinnersController 類別內的 Edit() 動作方法。 這可確保使用者必須登入以要求 /Dinners/Edit/[id] URL。
然後,我們可以將程式碼新增至使用 Dinner.IsHostedBy(username) 協助程式方法的 [編輯] 方法,以確認登入的使用者是否符合 Dinner 主機。 如果使用者不是主機,我們將顯示 "InvalidOwner" 檢視並終止要求。 執行此動作的程式碼如下所示:
//
// GET: /Dinners/Edit/5
[Authorize]
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (!dinner.IsHostedBy(User.Identity.Name))
return View("InvalidOwner");
return View(new DinnerFormViewModel(dinner));
}
//
// POST: /Dinners/Edit/5
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit(int id, FormCollection collection) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (!dinner.IsHostedBy(User.Identity.Name))
return View("InvalidOwner");
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new {id = dinner.DinnerID});
}
catch {
ModelState.AddModelErrors(dinnerToEdit.GetRuleViolations());
return View(new DinnerFormViewModel(dinner));
}
}
然後,我們可以使用滑鼠右鍵按一下 \Views\Dinners 目錄,然後選擇 [新增 ->檢視] 功能表命令,以建立新的 "InvalidOwner" 檢視。 我們將使用以下錯誤訊息來填入它:
<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
You Don't Own This Dinner
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">
<h2>Error Accessing Dinner</h2>
<p>Sorry - but only the host of a Dinner can edit or delete it.</p>
</asp:Content>
現在,當使用者嘗試編輯不屬於他們的 Dinner 時,他們會收到一條錯誤訊息:
我們可以針對控制器內的 Delete() 動作方法重複相同的步驟,以鎖定刪除 Dinners 的權限,並確保只有 Dinner 的主機可以刪除它。
顯示/隱藏 [編輯] 和 [刪除] 連結
我們會從 [詳細資料] URL 連結到 DinnersController 類別的 [編輯] 和 [刪除] 動作方法:
目前,我們會顯示 [編輯] 和 [刪除] 動作連結,不論詳細資料 URL 的訪客是否為 Dinner 的主機。 讓我們變更此設定,以便僅當造訪使用者是 Dinner 的擁有者時才顯示連結。
DinnersController 內的 Details() 動作方法會擷取 Dinner 物件,然後將它當做模型物件傳遞給我們的檢視範本:
//
// GET: /Dinners/Details/5
public ActionResult Details(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (dinner == null)
return View("NotFound");
return View(dinner);
}
我們可以使用 Dinner.IsHostedBy() 協助程式方法更新檢視範本,以有條件地顯示/隱藏 [編輯] 和 [刪除] 連結,如下所示:
<% if (Model.IsHostedBy(Context.User.Identity.Name)) { %>
<%= Html.ActionLink("Edit Dinner", "Edit", new { id=Model.DinnerID }) %> |
<%= Html.ActionLink("Delete Dinner", "Delete", new {id=Model.DinnerID}) %>
<% } %>
後續步驟
現在讓我們看看如何讓經過驗證的使用者能夠使用 AJAX 回覆 Dinners。