本文旨在簡單介紹mvc的權限驗證。
1、首先是建一個asp.net web應用程序
為了實現身份驗證,所以必須要添加一個登陸頁面,同時還需要有對應的用戶類型,所以添加了以下代碼
IUserDb接口。其實這個接口可以不用,但是在這里我用的是MEF組件來實現依賴注入。所以有了這個接口
namespace QuanXianYanZheng.Interface { interface IUserDb { bool ValidateUser(string userName, string password); string[] GetRoles(string userName); Models.User GetByNameAndPassword(string name, string password); } }
UserDb,這個類實際上是模擬數據庫操作的。export是MEF組件實現依賴注入的,可以看看前一篇隨筆了解。
namespace QuanXianYanZheng.DB { [Export("UserDb",typeof(IUserDb))] public class UserDb: IUserDb { //模擬數據庫,用戶表 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"}}, }; /// <summary> /// 驗證用戶密碼 /// </summary> /// <param name="userName"></param> /// <param name="password"></param> /// <returns></returns> public bool ValidateUser(string userName, string password) { return usersForTest .Any(u => u.Name == userName && u.Password == password); } /// <summary> /// 獲取用戶的角色 /// </summary> /// <param name="userName"></param> /// <returns></returns> public string[] GetRoles(string userName) { return usersForTest .Where(u => u.Name == userName) .Select(u => u.Roles) .FirstOrDefault(); } /// <summary> /// 獲取用戶 /// </summary> /// <param name="name"></param> /// <param name="password"></param> /// <returns></returns> public User GetByNameAndPassword(string name, string password) { return usersForTest .FirstOrDefault(u => u.Name == name && u.Password == password); } } }
User,用戶模型
namespace QuanXianYanZheng.Models { public class User { public int ID { get; set; } public string Name { get; set; } public string Password { get; set; } public string[] Roles { get; set; } } }
AccountController 登陸頁面的控制器
namespace QuanXianYanZheng.Controllers { public class AccountController : Controller { [Import("UserDb")] private IUserDb Repository { set; get; } public ActionResult LogOn() { return View(); } [HttpPost] public ActionResult LogOn(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { if(null==Repository) CustomTool.Compose(this); if (Repository.ValidateUser(model.UserName, model.Password)) { //將用戶信息保存到cookie,如果不能使用cookie則添加到URL FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe); if (!String.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl); else return RedirectToAction("Index", "Home"); } else ModelState.AddModelError("", "用戶名或密碼不正確!"); } return View(model); } } }
Compose,這個是為了實現依賴注入來實例化對象專門寫的一段公共代碼,如果不用這種方式來實例化對象,可以不要這段。
namespace QuanXianYanZheng { public class CustomTool { /// <summary> /// 這個是為了實現依賴注入的方式來實例化對象而必須要執行的一段代碼 /// </summary> /// <param name="o"></param> public static void Compose(object o) { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); CompositionContainer container = new CompositionContainer(catalog); container.ComposeParts(o); } } }
LogOnModel類太簡單了,就不貼代碼了。然后創建登陸頁面,就兩個文本框一個登陸按鈕,也沒什么難度了。然后就是Global.asax,在類MvcApplication中添加一個構造函數,代碼如下:
[Import("UserDb")] private IUserDb Repository { set; get; } /// <summary> /// 添加構造函數 /// </summary> public MvcApplication() { AuthorizeRequest += new EventHandler(MvcApplication_AuthorizeRequest); } void MvcApplication_AuthorizeRequest(object sender, EventArgs e) { IIdentity id = Context.User.Identity; if(null== Repository) CustomTool.Compose(this);//這個是為了實現依賴注入的方式而調用的 if (id.IsAuthenticated)//這里判斷訪問者是否成功進行了身份驗證 { var roles = Repository.GetRoles(id.Name); Context.User = new GenericPrincipal(id, roles); } }
至此,代碼基本寫好了,然后運行程序,將斷點打在此處:
會發現程序每次進入后台控制器之前,都會經過這里,不管你有沒有進行成功的登錄,這個if語句的判斷條件都是false。這很顯然不符合我的要求,我要的是在成功登錄后,這個判斷是true。之所以出現現在的情況,是因為配置文件還沒有相關的配置:
有了這段配置之后,再次運行程序。默認還是進入了home/index頁面,這與我的要求是有出入的,我要求在用戶沒登陸的情況下,默認進入登陸頁面。這個問題先放着,后面會解決。這里之所以進入的是home/index頁面,是因為路由的配置是這樣的,沒什么可說的。
運行程序后,在沒有登錄的情況下,上面的斷點一直是false,但如果登錄成功后,就會變成true了。有這樣的變化,是因為執行了下面這條語句
現在來處理剛才提到的在沒有登錄的時候默認進入了home/index頁面的問題,解決這個問題不需要去修改路由的配置,只需要在HomeController的index方法上添加特性 [Authorize]即可。
再次運行程序之前記得清空瀏覽器緩存,因為剛才已經成功登陸過了,使得上面斷點的地方判斷的是true,默認的是已經驗證過了,因此還是會進入home/index頁面,所以必須清空緩存。這樣就可以看到再次默認進入的是登陸頁面了。
與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" />。下面依次介紹一下各種屬性:
name——Cookie的名字。Forms Authentication可能會在驗證后將用戶憑證放在Cookie中,name屬性決定了該Cookie的名字。通過FormsAuthentication.FormsCookieName屬性可以得到該配置值(稍后介紹FromsAuthentication類)。
loginUrl——登錄頁的URL。通過FormsAuthentication.LoginUrl屬性可以得到該配置值。當調用FormsAuthentication.RedirectToLoginPage()方法時,客戶端請求將被重定向到該屬性所指定的頁面。loginUrl的默認值為“login.aspx”,這表明即便不提供該屬性值,ASP.NET也會嘗試到站點根目錄下尋找名為login.aspx的頁面。
defaultUrl——默認頁的URL。通過FormsAuthentication.DefaultUrl屬性得到該配置值。
protection——Cookie的保護模式,可取值包括All(同時進行加密和數據驗證)、Encryption(僅加密)、Validation(僅進行數據驗證)和None。為了安全,該屬性通常從不設置為None。
timeout——Cookie的過期時間。通過FormsAuthentication.Timeout獲取,默認是2天
path——Cookie的路徑。可以通過FormsAuthentication.FormsCookiePath屬性得到該配置值。 默認情況下得到的是:“/”
requireSSL——在進行Forms Authentication時,與服務器交互是否要求使用SSL。可以通過FormsAuthentication.RequireSSL屬性得到該配置值。
slidingExpiration——是否啟用“彈性過期時間”,如果該屬性設置為false,從首次驗證之后過timeout時間后Cookie即過期;如果該屬性為true,則從上次請求該開始過timeout時間才過期,這意味着,在首次驗證后,如果保證每timeout時間內至少發送一個請求,則Cookie將永遠不會過期。通過FormsAuthentication.SlidingExpiration屬性可以得到該配置值。
enableCrossAppRedirects——是否可以將已進行了身份驗證的用戶重定向到其他應用程序中。通過FormsAuthentication.EnableCrossAppRedirects屬性可以得到該配置值。為了安全考慮,通常總是將該屬性設置為false。
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傳遞用戶憑證。
domain——Cookie的域。通過FormsAuthentication.CookieDomain屬性可以得到該配置值。
FormsAuthentication類
FormsAuthentication類用於輔助我們完成窗體驗證,並進一步完成用戶登錄等功能。該類位於system.web.dll程序集的System.Web.Security命名空間中。通常在Web站點項目中可以直接使用這個類,如果是在類庫項目中使用這個類,請確保引用了system.web.dll。
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中移除用戶憑據。
現在我們再來看看登錄控制的代碼,這里看到有個returnUrl,這個參數是用於設置登錄成功后返回到登錄前的頁面的。按照這種方式,在從登錄前的頁面跳轉到登錄頁面的時候,是需要將登陸前的頁面URL作為參數傳遞到登錄頁面里面保存起來,然后在登錄的
時候再傳入下面的函數中。這種方式肯定可行,但是比較麻煩。
接下來要通過上面介紹的方式來實現在登錄成功后返回登錄前頁面。
為了簡單化,我們在Home/Index頁面上添加一個去登錄的按鈕,跳轉到登錄頁面,然后在登錄成功后又返回Home/Index頁面。
1、去掉之前在home控制器中添加的[Authorize],然后在頁面中添加一個按鈕,代碼如下:
運行代碼,點擊登錄按鈕,進入了登錄頁面了,可以看到URL中,在后面添加了一個ReturnUrl,但是,請注意這個ReturnUrl,看出問題了嗎。問題在於這個ReturnUrl,當我們在登陸成功后,調用RedirectFromLoginPage的時候,會執行Home/GoToLogin。
所以,對RedirectToLoginPage的調用方式需要重新處理,如下:
登錄按鈕的訪問地址多了個參數page。
在跳轉到登錄頁面的函數中,通過Request.IsAuthenticated來判斷是否已經登陸過了,然后下面根據page參數決定登錄成功后返回的頁面,當然代碼怎么寫根據自己的需要,如果登錄之前的頁面不是初始狀態,這就需要在點擊登錄按鈕的時候,將狀態傳遞進來,然后根據情況選擇是否用有參數的RedirectToLoginPage函數,這里要注意的問題是這個ReturnUrl,它不是登錄頁面之前的頁面的URL,而是調用函數RedirectToLoginPage所在的函數的URL。運行程序,點擊登錄后,可以看到,系統自動的在URL后面添加了一個returnURL,而且這個page=Index也出現在了里面。
點擊登錄后執行的是下面的代碼(代碼中的參數returnUrl其實已經沒用了,可以去掉,當然如果保留的話,可以通過這個參數獲取到RedirectToLoginPage在URL中附加的ReturnUrl參數):
[HttpPost] public ActionResult LogOn(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { if(null==Repository) CustomTool.Compose(this); if (Repository.ValidateUser(model.UserName, model.Password)) { //將用戶信息保存到cookie,如果不能使用cookie則添加到URL //FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe); //if (!String.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl); //else return RedirectToAction("Index", "Home"); FormsAuthentication.RedirectFromLoginPage(model.UserName, model.RememberMe); } else ModelState.AddModelError("", "用戶名或密碼不正確!"); } return View(model); }
現在,假如我們希望“關於”頁面必須在登錄后才能訪問,那么在上面的代碼寫好的前提下,再做如下處理即可:
當登錄成功后,程序會再次執行這個about函數,然后因為Request.IsAuthenticated=true,而跳過這個if語句,直接打開about頁面。除了這種方式,還可以這樣:
假如我希望角色為admin和manager的用戶可以訪問聯系方式這個頁面,那么按照如下方式處理:
這樣一來,就只有角色為admin和manager的用戶可以訪問聯系方式了。
特性Authorize不僅可以放在action上面,也可以放在control上面,這樣控制的是整個控制器里面所有的函數。
注意:
當特性里面用users來控制的時候,記住,這個users不是下圖中的name的值(但是用角色的時候,卻是對應的角色的值)
而是下圖中紅框里面的值。假如下面的代碼里面在model.UserName后面加上一個字符串“123”,那么在特性里面的users的值也必須是“123”結尾的字符串,如果有多個,那用逗號隔開。
現在有這樣一個問題,我已經登錄成功了,但是我沒有權限去訪問聯系方式頁面。按照上面的代碼,當我沒權限的時候,我去訪問聯系方式頁面,那么系統會跳轉到登陸頁面去,這顯然不對的,我希望的是,在我登錄后,沒有權限的情況下,訪問聯系方式頁面,
這時給我一個提示,然后停留在原來的頁面;如果我沒登陸,那么就跳轉到登陸頁面。實現這樣的功能,需要自定義權限特性。
代碼如下:
MyAuthorize是我自定義的一個權限驗證特性。
namespace QuanXianYanZheng.Attributes { public class MyAuthorizeAttribute: AuthorizeAttribute { /* 下面3個函數執行的順序是OnAuthorization--->AuthorizeCore--->HandleUnauthorizedRequest 在父類的OnAuthorization方法中會自動調用AuthorizeCore函數,因此如果重寫OnAuthorization 函數的時候,不執行父類的OnAuthorization,那么將不會執行AuthorizeCore和HandleUnauthorizedRequest。 當AuthorizeCore返回false時,才會執行HandleUnauthorizedRequest */ //public override void OnAuthorization(AuthorizationContext filterContext) //{ // base.OnAuthorization(filterContext); //} /// <summary> /// 這個方法主要是實現授權驗證邏輯的地方 /// </summary> /// <param name="httpContext"></param> /// <returns></returns> protected override bool AuthorizeCore(HttpContextBase httpContext) { bool result = false; if (httpContext == null) { throw new ArgumentNullException("httpContext"); } string[] roles = Roles.Split(','); if (!httpContext.User.Identity.IsAuthenticated)//判斷用戶是否成功登錄 return false; foreach(string r in roles) { if(httpContext.User.IsInRole(r)) { result = true; break; } } if (!result) { httpContext.Response.StatusCode = 403;//表示無權限訪問 } return result; } /// <summary> /// 這個函數處理在無權限的情況下的邏輯 /// </summary> /// <param name="filterContext"></param> protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { base.HandleUnauthorizedRequest(filterContext); if (filterContext == null) { throw new ArgumentNullException("filterContext"); } else if (filterContext.HttpContext.Response.StatusCode == 403) { filterContext.HttpContext.Response.Write("<script>alert('無訪問權限');history.go(-1);</script>"); filterContext.HttpContext.Response.End(); //下面這一句的作用是讓程序在無權訪問的時候,不要再繼續去執行被MyAuthorize修飾的函數。 //比如我們在訪問聯系方式頁面的時候,如果沒有權限,就不要繼續去執行Home/Contact了。 filterContext.Result = new EmptyResult(); } } } }
注意這個流程,當AuthorizeCore返回false且httpContext.Response.StatusCode=403的時候,表示無權限,那么在HandleUnauthorizedRequest中就會給出提示。
當AuthorizeCore返回false且httpContext.Response.StatusCode!=403的時候,表示用戶未登錄,那么在HandleUnauthorizedRequest中將沒有執行代碼,系統會自動跳轉到配置文件中authentication里面配置的loginUrl頁面。
當AuthorizeCore返回true,表示有權限了,程序不會執行HandleUnauthorizedRequest。會直接跳轉到目標頁面。