ASP.NET MVC:窗體身份驗證及角色權限管理示例
前言
本來使用Forms Authentication進行用戶驗證的方式是最常見的,但系統地闡明其方法的文章並不多見,網上更多的文章都是介紹其中某一部分的使用方法或實現原理,而更多的朋友則發文詢問如何從頭到尾完整第實現用戶的註冊、登錄。因此,Anders Liu在這一系列文章中計劃通過一個實際的例子,介紹如何基於Forms Authentication實現:
l 用戶註冊(包括密碼的加密存儲)
l 用戶登錄(包括密碼的驗證、設置安全Cookie)
l 用戶實體替換(使用自己的類型作為HttpContext.User的類型)
有關Forms Authentication的原理等內容不屬於本文的討論範疇,大家可以通過在Google等搜索引擎中輸入“Forms Authentication”、“Forms身份驗證”、“窗體身份驗證”等關鍵詞來查看更多資源。本文僅從實用的角度介紹如何使用這一技術。
不使用Membership
本文介紹的實現方式不依賴ASP.NET 2.0提供的Membership功能。這主要是因為,如果使用Membership,就必須用aspnet_regsql.exe實用工具配置數據庫,否則就得自己寫自定義的MembershipProvider。
如果用aspnet_regsql.exe配置數據庫,就會導致數據庫中出現很多我們實際並不需要的表或字段。此外更重要的是,默認的SqlMembershipProvider給很多數據表添加了ApplicationID列,其初衷可能是希望可以將多個應用程序的用戶全部放在一個庫裏,但又能彼此隔離。但實際情況是,每個應用程序都在其自身的數據庫中保存用戶數據。因此,引入這個ApplicationID無端地在每次查找用戶時增加了額外的條件。
另一方面,如果考慮自己實現一個MembershipProvider,因為工作量巨大,有點得不償失。
但是,如果不使用Membership,也就無法享受ASP.NET 2.0中新增的Login等控件的便利了。
與Forms Authentication相關的配置
在web.config文件中,<system.web>/<authentication>配置節用於對驗證進行配置。為<authentication>節點提供mode="Forms"屬性可以啟用Forms Authentication。一個典型的<authentication>配置節如下所示:
<authentication mode="Forms">
<forms
name=".ASPXAUTH"
loginUrl="login.aspx"
defaultUrl="default.aspx"
protection="All"
timeout="30"
path="/"
requireSSL="false"
slidingExpiration="false"
enableCrossAppRedirects="false"
cookieless="UseDeviceProfile"
domain=""
/>
</authentication>
以上代碼使用的均是默認設置,換言之,如果你的哪項配置屬性與上述代碼一致,則可以省略該屬性例如<forms name="MyAppAuth" />。下面依次介紹一下各種屬性:
l name——Cookie的名字。Forms Authentication可能會在驗證後將用戶憑證放在Cookie中,name屬性決定了該Cookie的名字。通過FormsAuthentication.FormsCookieName屬性可以得到該配置值(稍後介紹FromsAuthentication類)。
l loginUrl——登錄頁的URL。通過FormsAuthentication.LoginUrl屬性可以得到該配置值。當調用FormsAuthentication.RedirectToLoginPage()方法時,客戶端請求將被重定向到該屬性所指定的頁面。loginUrl的默認值為“login.aspx”,這表明即便不提供該屬性值,ASP.NET也會嘗試到站點根目錄下尋找名為login.aspx的頁面。
l defaultUrl——默認頁的URL。通過FormsAuthentication.DefaultUrl屬性得到該配置值。
l protection——Cookie的保護模式,可取值包括All(同時進行加密和數據驗證)、Encryption(僅加密)、Validation(僅進行數據驗證)和None。為了安全,該屬性通常從不設置為None。
l timeout——Cookie的過期時間。
l path——Cookie的路徑。可以通過FormsAuthentication.FormsCookiePath屬性得到該配置值。
l requireSSL——在進行Forms Authentication時,與服務器交互是否要求使用SSL。可以通過FormsAuthentication.RequireSSL屬性得到該配置值。
l slidingExpiration——是否啟用“彈性過期時間”,如果該屬性設置為false,從首次驗證之後過timeout時間後Cookie即過期;如果該屬性為true,則從上次請求該開始過timeout時間才過期,這意味著,在首次驗證後,如果保證每timeout時間內至少發送一個請求,則Cookie將永遠不會過期。通過FormsAuthentication.SlidingExpiration屬性可以得到該配置值。
l enableCrossAppRedirects——是否可以將以進行了身份驗證的用戶重定向到其他應用程序中。通過FormsAuthentication.EnableCrossAppRedirects屬性可以得到該配置值。為了安全考慮,通常總是將該屬性設置為false。
l cookieless——定義是否使用Cookie以及Cookie的行為。Forms Authentication可以采用兩種方式在會話中保存用戶憑據信息,一種是使用Cookie,即將用戶憑據記錄到Cookie中,每次發送請求時瀏覽器都會將該Cookie提供給服務器。另一種方式是使用URI,即將用戶憑據當作URL中額外的查詢字符串傳遞給服務器。該屬性有四種取值——UseCookies(無論何時都使用Cookie)、UseUri(從不使用Cookie,僅使用URI)、AutoDetect(檢測設備和瀏覽器,只有當設備支持Cookie並且在瀏覽器中啟用了Cookie時才使用Cookie)和UseDeviceProfile(只檢測設備,只要設備支持Cookie不管瀏覽器是否支持,都是用Cookie)。通過FormsAuthentication.CookieMode屬性可以得到該配置值。通過FormsAuthentication.CookiesSupported屬性可以得到對於當前請求是否使用Cookie傳遞用戶憑證。
l domain——Cookie的域。通過FormsAuthentication.CookieDomain屬性可以得到該配置值。
以上針對<system.web>/<authentication>/<forms>節點的介紹非常簡略,基本上是Anders Liu個人對於文檔進行的額外說明。有關<forms>節點的更多說明,請參見MSDN文檔(http://msdn2.microsoft.com/zh-cn/library/1d3t3c61(VS.85).aspx)。
FormsAuthentication類
FormsAuthentication類用於輔助我們完成窗體驗證,並進一步完成用戶登錄等功能。該類位於system.web.dll程序集的System.Web.Security命名空間中。通常在Web站點項目中可以直接使用這個類,如果是在類庫項目中使用這個類,請確保引用了system.web.dll。
前一節已經介紹了FormsAuthentication類的所有屬性。這一節將介紹該類少數幾個常用的方法。
RedirectToLoginPage方法用於從任何頁面重定向到登錄頁,該方法有兩種重載方式:
public static void RedirectToLoginPage ()
public static void RedirectToLoginPage (string extraQueryString)
兩種方式均會使瀏覽器重定向到登錄頁(登錄頁的URL由<forms>節點的loginUrl屬性指出)。第二種重載方式還能夠提供額外的查詢字符串。
RedirectToLoginPage通常在任何非登錄頁的頁面中調用。該方法除了進行重定向之外,還會向URL中附加一個ReturnUrl參數,該參數即為調用該方法時所在的頁面的URL地址。這是為了方便登錄後能夠自動回到登錄前所在的頁面。
RedirectFromLoginPage方法用於從登錄頁跳轉回登錄前頁面。這個“登錄前”頁面即由訪問登錄頁時提供的ReturnUrl參數指定。如果沒有提供ReturnUrl參數(例如,不是使用RedirectToLoginPage方法而是用其他手段重定向到或直接訪問登錄頁時),則該方法會自動跳轉到由<forms>節點的defaultUrl屬性所指定的默認頁。
此外,如果<forms>節點的enableCrossAppRedirects屬性被設置為false,ReturnUrl參數所指定的路徑必須是當前Web應用程序中的路徑,否則(如提供其他站點下的路徑)也將返回到默認頁。
RedirectFromLoginPage方法有兩種重載形式:
public static void RedirectFromLoginPage (string userName, bool createPersistentCookie)
public static void RedirectFromLoginPage (string userName, bool createPersistentCookie, string strCookiePath)
userName參數表示用戶的標識(如用戶名、用戶ID等);createPersistentCookie參數表示是否“記住我”;strCookiePath參數表示Cookie路徑。
RedirectFromLoginPage方法除了完成重定向之外,還會將經過加密(是否加密取決於<forms>節點的protection屬性)的用戶憑據存放到Cookie或Uri中。在後續訪問中,只要Cookie沒有過期,則將可以通過HttpContext.User.Identity.Name屬性得到這裏傳入的userName屬性。
此外,FormsAuthentication還有一個SignOut方法,用於完成用戶註銷。其原理是從Cookie或Uri中移除用戶憑據。
好了,至此所需要掌握的基礎知識就齊備了,接下來我們將實現用戶註冊、登錄等功能:
ASP.NET MVC 建立 ASP.NET 基礎之上,很多 ASP.NET 的特性(如窗體身份驗證、成員資格)在 MVC 中可以直接使用。本文旨在提供可參考的代碼,不會涉及這方面太多理論的知識。
本文僅使用 ASP.NET 的窗體身份驗證,不會使用它的 成員資格(Membership) 和 角色管理 (RoleManager),原因有二:一是不靈活,二是和 MVC 關系不太。
一、示例項目
User.cs 是模型文件,其中包含了 User 類:
public class User
{
public int ID { get; set; }
public string Name { get; set; }
public string Password { get; set; }
public string[] Roles { get; set; }
}
UserRepository 為數據存取類,為了演示方便,並沒有連接數據庫,而是使用一個數組來作為數據源:
public class UserRepository
{
private static User[] usersForTest = new[]{
new User{ ID = 1, Name = "bob", Password = "bob", Roles = new []{"employee"}},
new User{ ID = 2, Name = "tom", Password = "tom", Roles = new []{"manager"}},
new User{ ID = 3, Name = "admin", Password = "admin", Roles = new[]{"admin"}},
};
public bool ValidateUser(string userName, string password)
{
return usersForTest
.Any(u => u.Name == userName && u.Password == password);
}
public string[] GetRoles(string userName)
{
return usersForTest
.Where(u => u.Name == userName)
.Select(u => u.Roles)
.FirstOrDefault();
}
public User GetByNameAndPassword(string name, string password)
{
return usersForTest
.FirstOrDefault(u => u.Name == name && u.Password == password);
}
}
二、用戶登錄及身份驗證
方式一
修改 AccountController:原有 AccountController 為了實現控制反轉,對窗體身份驗證進行了抽象。為了演示方便,我去除了這部分(以及註冊及修改密碼部分):
public class AccountController : Controller
{
private UserRepository repository = new UserRepository();
public ActionResult LogOn()
{
return View();
}
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (repository.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (!String.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl);
else return RedirectToAction("Index", "Home");
}
else
ModelState.AddModelError("", "用戶名或密碼不正確!");
}
return View(model);
}
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}
}
修改 Global.asax:
public class MvcApplication : System.Web.HttpApplication
{
public MvcApplication()
{
AuthorizeRequest += new EventHandler(MvcApplication_AuthorizeRequest);
}
void MvcApplication_AuthorizeRequest(object sender, EventArgs e)
{
IIdentity id = Context.User.Identity;
if (id.IsAuthenticated)
{
var roles = new UserRepository().GetRoles(id.Name);
Context.User = new GenericPrincipal(id, roles);
}
}
//...
}
給 MvcApplication 增加構造函數,在其中增加 AuthorizeRequest 事件的處理函數。
代碼下載:Mvc-FormsAuthentication-RolesAuthorization-1.rar (243KB)
方式二
此方式將用戶的角色保存至用戶 Cookie,使用到了 FormsAuthenticationTicket。
修改 AccountController:
public class AccountController : Controller
{
private UserRepository repository = new UserRepository();
public ActionResult LogOn()
{
return View();
}
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
User user = repository.GetByNameAndPassword(model.UserName, model.Password);
if (user != null)
{
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
user.Name,
DateTime.Now,
DateTime.Now.Add(FormsAuthentication.Timeout),
model.RememberMe,
user.Roles.Aggregate((i,j)=>i+","+j)
);
HttpCookie cookie = new HttpCookie(
FormsAuthentication.FormsCookieName,
FormsAuthentication.Encrypt(ticket));
Response.Cookies.Add(cookie);
if (!String.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl);
else return RedirectToAction("Index", "Home");
}
else
ModelState.AddModelError("", "用戶名或密碼不正確!");
}
return View(model);
}
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}
}
修改 Global.asax:
public class MvcApplication : System.Web.HttpApplication
{
public MvcApplication()
{
AuthorizeRequest += new EventHandler(MvcApplication_AuthorizeRequest);
}
void MvcApplication_AuthorizeRequest(object sender, EventArgs e)
{
var id = Context.User.Identity as FormsIdentity;
if (id != null && id.IsAuthenticated)
{
var roles = id.Ticket.UserData.Split(‘,‘);
Context.User = new GenericPrincipal(id, roles);
}
}
//...
}
代碼下載:Mvc-FormsAuthentication-RolesAuthorization-2.rar (244KB)
三、角色權限
使用任一種方式後,我們就可以在 Controller 中使用 AuthorizeAttribute 實現基於角色的權限管理了:
[Authorize(Roles = "employee,manager")]
public ActionResult Index1()
{
return View();
}
[Authorize(Roles = "manager")]
public ActionResult Index2()
{
return View();
}
[Authorize(Users="admin", Roles = "admin")]
public ActionResult Index3()
{
return View();
}
四、簡要說明
MVC 使用 HttpContext.User 屬性進行來進行實現身份驗證及角色管理,同樣 AuthorizeAttribute 也根據 HttpContext.User 進行角色權限驗證。
因些不要在用戶登錄後,將相關用戶信息保存在 Session 中(網上經常看到這種做法),將用戶保存在 Session 中是一種非常不好的做法。
也不要在 Action 中進行角色權限判斷,應該使用 AuthorizeAttribute 或它的子類,以下的方式都是錯誤的:
public ActionResult Action1()
{
if (Session["User"] == null) { /**/}
/**/
}
public ActionResult Action2()
{
if (User.Identity == null) { /**/}
if (User.Identity.IsAuthenticated == false) { /**/}
if (User.IsInRole("admin") == false) { /**/}
/**/
}
若本文中有錯誤或不妥之處,敬請指正,謝謝!
已下為項目中用到的代碼:
身份驗證
後臺:DateTime expiredTime = DateTime.Now.AddMonths(1);
string ssoData = userName + "," + saveLogin.ToString();
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, userName,
DateTime.Now,
expiredTime,
saveLogin,
ssoData,
FormsAuthentication.FormsCookiePath);
string authTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie lcookie = new HttpCookie(FormsAuthentication.FormsCookieName, authTicket);
lcookie.Expires = DateTime.Now.AddMonths(1);//勾選自動登錄時
Response.Cookies.Add(lcookie);
前臺if (!User.Identity.IsAuthenticated)
退出登錄:
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
return Redirect(FormsAuthentication.LoginUrl); //<system.web>後添加<authentication mode="Forms"><forms loginUrl="~/Portal" timeout="2880" name=".DASHBOARDNETAUTH"></forms></authentication>
}
ASP.NET MVC:窗體身份驗證及角色權限管理示例