探索ASP.NET Identity 身份驗證和基於角色的授權,中級篇
回到頂部在前一篇文章中,我介紹了ASP.NET Identity 基本API的運用並建立了若干使用者賬號。那麼在本篇文章中,我將繼續ASP.NET Identity 之旅,向您展示如何運用ASP.NET Identity 進行身份驗證(Authentication)以及聯合ASP.NET MVC 基於角色的授權(Role-Based Authorization)。
本文的示例,你可以在此下載和預覽:
探索身份驗證與授權
在這一小節中,我將闡述和證明ASP.NET 身份驗證和授權的工作原理和執行機制,然後介紹怎樣使用Katana Middleware 和 ASP.NET Identity 進行身份驗證。
1. 理解ASP.NET 表單身份驗證與授權機制
談到身份驗證,我們接觸的最多的可能就是表單身份驗證(Form-based Authentication)。為了更好的去理解ASP.NET 表單身份驗證與授權機制,我搬出幾年前的一張舊圖,表示HttpApplication 19個事件,它們分別在HttpModule 中被註冊,這又被稱為ASP.NET 管道(Pipeline)事件。通俗的講,當請求到達伺服器時,ASP.NET 執行時會依次觸發這些事件:
身份驗證故名思義,驗證的是使用者提供的憑據(Credentials)。一旦驗證通過,將產生唯一的Cookie標識並輸出到瀏覽器。來自瀏覽器的下一次請求將包含此Cookie,對於ASP.NET 應用程式,我們熟知的FormsAuthenticationModule
如果將身份驗證看作是"開門"的話,主人邀請你進屋,但這並不意味著你可以進入到臥室或者書房,可能你的活動場所僅限書房——這就是授權。在PostAuthenticateRequest事件觸發過後,會觸發AuthorizeRequest 事件,它在UrlAuthorizationModule 中被註冊(題外插一句:UrlAuthorizationModule 以及上面提到的FormsAuthenticationModule你可以在IIS 級別的.config檔案中找到,這也是ASP.NET 和 IIS緊耦合關係的體現)。在該事件中,請求的URL會依據web.config中的authorization 配置節點進行授權,如下所示授予Kim以及所有Role為Administrator的成員具有訪問許可權,並且拒絕John以及匿名使用者訪問。
- <authorization>
- <allow users="Kim"/>
- <allow roles="Administrator"/>
- <deny users="John"/>
- <deny users="?"/>
- </authorization>
通過身份驗證和授權,我們可以對應用程式敏感的區域進行受限訪問,這確保了資料的安全性。
2.使用Katana進行身份驗證
到目前為止,你可能已經對OWIN、Katana 、 Middleware 有了基本的瞭解,如果不清楚的話,請移步到此瀏覽。
使用Katana,你可以選擇幾種不同型別的身份驗證方式,我們可以通過Nuget來安裝如下型別的身份驗證:
- 表單身份驗證
- 社交身份驗證(Twitter、Facebook、Google、Microsoft Account…)
- Windows Azure
- Active Directory
- OpenID
其中又以表單身份驗證用的最為廣泛,正如上面提到的那樣,傳統ASP.NET MVC 、Web Form 的表單身份驗證實際由FormsAuthenticationModule 處理,而Katana重寫了表單身份驗證,所以有必要比較一下傳統ASP.NET MVC & Web Form 下表單身份驗證與OWIN下表單身份驗證的區別:
Features | ASP.NET MVC & Web Form Form Authentication | OWIN Form Authentication |
Cookie Authentication | √ | √ |
Cookieless Authentication | √ | × |
Expiration | √ | √ |
Sliding Expiration | √ | √ |
Token Protection | √ | √ |
Claims Support | × | √ |
Unauthorized Redirection | √ | √ |
從上表對比可以看出,Katana幾乎實現了傳統表單身份驗證所有的功能,那我們怎麼去使用它呢?還是像傳統那樣在web.config中指定嗎?
非也非也,Katana 完全拋棄了FormsAuthenticationModule,實際上是通過Middleware來實現身份驗證。預設情況下,Middleware在HttpApplication的PreRequestHandlerExecute 事件觸發時鏈式執行,當然我們也可以將它指定在特定的階段執行,通過使用UseStageMarker方法,我們可以在AuthenticateRequest 階段執行Middleware 進行身份驗證。
那我們要怎樣去實現呢?幸運的是,Katana已經幫助我們封裝好了一個擴充套件方法,如下所示,
- app.UseCookieAuthentication(new CookieAuthenticationOptions
- {
- AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
- LoginPath = new PathString("/Account/Login")
- });
app.UseCookieAuthentication 是一個擴充套件方法,它的內部幫我們做了如下幾件事:
- 使用app.Use(typeof(CookieAuthenticationMiddleware), app, options) 方法,將CookieAuthenticationMiddleware 中介軟體註冊到OWIN Pipeline中
- 通過app.UseStageMarker(PipelineStage.Authenticate)方法,將前面新增的CookieAuthenticationMiddleware指定在 ASP.NET 整合管道(ASP.NET integrated pipeline)的AuthenticateRequest階段執行
當呼叫(Invoke)此Middleware時,將呼叫CreateHandler方法返回CookieAuthenticationHandler物件,它包含 AuthenticateCoreAsync方法,在這個方法中,讀取並且驗證Cookie,然後通過AddUserIdentity方法建立ClaimsPrincipal物件並新增到Owin環境字典中,可以通過OwinContext物件Request.User可以獲取當前使用者。
這是一個典型Middleware中介軟體使用場景,說白了就是去處理Http請求並將資料儲存到OWIN環境字典中進行傳遞。而CookieAuthenticationMiddleware所做的事其實和FormsAuthenticationModule做的事類似。
那我們怎麼產生Cookie呢?使用ASP.NET Identity 進行身份驗證,如果驗證通過,產生Cookie並輸出到客戶端瀏覽器, 這樣一個閉環就形成了,我將在下一小節實施這一步驟。
3.使用Authorize特性進行授權
ASP.NET Identity已經整合到了ASP.NET Framework中,在ASP.NET MVC 中,我們可以使用Authorize 特性進行授權,如下程式碼所示:
- [Authorize]
- public ActionResult Index()
- {
- return View();
- }
上述程式碼中,Index Action 已被設定了受限訪問,只有身份驗證通過才能訪問它,如果驗證不通過,返回401.0 – Unauthorized,然後請求在EndRequest 階段被 OWIN Authentication Middleware 處理,302 重定向到/Account/Login 登入。
回到頂部
使用ASP.NET Identity 身份驗證
有了對身份驗證和授權機制基本瞭解後,那麼現在就該使用ASP.NET Identity 進行身份驗證了。
1. 實現身份驗證所需的準備工作
當我們匿名訪問授權資源時,會被Redirect 到 /Account/Login 時,此時的URL結構如下:
http://localhost:60533/Account/Login?ReturnUrl=%2Fhome%2Findex
因為需要登陸,所以可以將Login 設定為允許匿名登陸,只需要在Action的上面新增 [AllowAnonymous] 特性標籤,如下所示:
- [AllowAnonymous]
- public ActionResult Login(string returnUrl)
- {
- //如果登入使用者已經Authenticated,提示請勿重複登錄
- if (HttpContext.User.Identity.IsAuthenticated)
- {
- return View("Error", new string[] {"您已經登入!"});
- }
- ViewBag.returnUrl = returnUrl;
- return View();
- }
注意,在這兒我將ReturnUrl 儲存了起來,ReturnUrl 顧名思義,當登入成功後,重定向到最初的地址,這樣提高了使用者體驗。
由於篇幅的限制,Login View 我不將程式碼貼出來了,事實上它也非常簡單,包含如下內容:
- 使用者名稱文字框
- 密碼框
- 儲存ReturnUrl的隱藏域
- @Html.AntiForgeryToken(),用來防止CSRF跨站請求偽造
2.新增使用者並實現身份驗證
當輸入了憑據之後,POST Form 表單到/Account/Login 下,具體程式碼如下:
- [HttpPost]
- [AllowAnonymous]
- [ValidateAntiForgeryToken]
- public async Task<ActionResult> Login(LoginModel model,string returnUrl)
- {
- if (ModelState.IsValid)
- {
- AppUser user = await UserManager.FindAsync(model.Name, model.Password);
- if (user==null)
- {
- ModelState.AddModelError("","無效的使用者名稱或密碼");
- }
- else
- {
- var claimsIdentity =
- await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
- AuthManager.SignOut();
- AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity);
- return Redirect(returnUrl);
- }
- }
- ViewBag.returnUrl = returnUrl;
- return View(model);
- }
上述程式碼中,首先使用 ASP.NET Identity 來驗證使用者憑據,這是通過 AppUserManager 物件的FindAsync 方法來實現,如果你不瞭解ASP.NET Identity 基本API ,請參考我這篇文章。
- AppUser user = await UserManager.FindAsync(model.Name, model.Password);
FindAsync 方法接受兩個引數,分別是使用者名稱和密碼,如果查詢到,則返回AppUser 物件,否則返回NULL。
如果FindAsync 方法返回AppUser 物件,那麼接下來就是建立Cookie 並輸出到客戶端瀏覽器,這樣瀏覽器的下一次請求就會帶著這個Cookie,當請求經過AuthenticateRequest 階段時,讀取並解析Cookie。也就是說Cookie 就是我們的令牌, Cookie如本人,我們不必再進行使用者名稱和密碼的驗證了。
使用ASP.NET Identity 產生Cookie 其實很簡單,就3行程式碼,如下所示:
- var claimsIdentity =
- await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
- AuthManager.SignOut();
- AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity);
對程式碼稍作分析,第一步建立了用來代表當前登入使用者的ClaimsIdentity 物件,ClaimsIndentity 是 ASP.NET Identity 中的類,它實現了IIdentity 介面。
ClaimsIdentity 物件實際上由AppUserManager 物件的CreateIdentityAsync 方法建立,它需要接受一個AppUser 物件和身份驗證型別,在這兒選擇ApplicationCookie。
接下來,就是讓已存在的Cookie 失效,併產生新Cookie。我預先定義了一個AuthManager 屬性,它是IAuthenticationManager 型別的物件,用來做一些通用的身份驗證操作。它 包含如下重要的操作:
- SignIn(options,identity) 故名思意登入,用來產生身份驗證過後的Cookie
- SignOut() 故名思意登出,讓已存在的Cookie 失效
SignIn 需要接受兩個引數,AuthenticationProperties 物件和ClaimsIdentity 物件,AuthticationProperties 有眾多屬性,我在這兒只設置IsPersistent=true ,意味著Authentication Session 被持久化儲存,當開啟新Session 時,該使用者不必重新驗證了。
最後,重定向到ReturnUrl:
- return Redirect(returnUrl);
使用角色進行授權
在前一小節中,使用了Authorize 特性對指定區域進行受限訪問,只有被身份驗證通過後才能繼續訪問。在這一小節將更細粒度進行授權操作,在ASP.NET MVC Framework 中,Authorize 往往結合User 或者 Role 屬性進行更小粒度的授權操作,正如如下程式碼所示:
- [Authorize(Roles = "Administrator")]
- public class RoleController : Controller
- {
- }
1.使用ASP.NET Identity 管理角色
對Authorize 有了基本的瞭解之後,將關注點轉移到角色Role的管理上來。ASP.NET Identity 提供了一個名為RoleManager<T> 強型別基類用來訪問和管理角色,其中T 實現了IRole 介面,IRole 介面包含了持久化Role 最基礎的欄位(Id和Name)。
Entity Framework 提供了名為IdentityRole 的類,它實現了IRole 介面,所以它不僅包含Id、Name屬性,還增加了一個集合屬性Users。IdentityRole重要的屬性如下所示:
Id | 定義了Role 唯一的Id |
Name | 定義了Role的名稱 |
Users | 返回隸屬於Role的所有成員 |
我不想在應用程式中直接使用IdentityRole,因為我們還可能要去擴充套件其他欄位,故定義一個名為AppRole的類,就像AppUser那樣,它繼承自IdentityRole:
- public class AppRole:IdentityRole
- {
- public AppRole() : base() { }
- public AppRole(string name) : base(name) { }
- // 在此新增額外屬性
- }
同時,再定義一個AppRoleManager 類,如同AppUserManager 一樣,它繼承RoleManager<T>,提供了檢索和持久化Role的基本方法:
- public class AppRoleManager:RoleManager<AppRole>
- {
- public AppRoleManager(RoleStore<AppRole> store):base(store)
- {
- }
- public static AppRoleManager Create(IdentityFactoryOptions<AppRoleManager> options, IOwinContext context)
- {
- return new AppRoleManager(new RoleStore<AppRole>(context.Get<AppIdentityDbContext>()));
- }
- }
最後,別忘了在OWIN Startup類中初始化該例項,它將儲存在OWIN上下文環境字典中,貫穿了每一次HTTP請求:
- app.CreatePerOwinContext(AppIdentityDbContext.Create);
- app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
- app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);
2.建立和刪除角色
使用ASP.NET Identity 建立和刪除角色很簡單,通過從OWIN 上下文中獲取到AppRoleManager,然後Create 或者 Delete,如下所示:
- [HttpPost]
- public async Task<ActionResult> Create(string name)
- {
- if (ModelState.IsValid)
- {
- IdentityResult result = await RoleManager.CreateAsync(new AppRole(name));
- if (result.Succeeded)
- {
- return RedirectToAction("Index");
- }
- else
- {
- AddErrorsFromResult(result);
- }
- }
- return View(name);
- }
- [HttpPost]
- public async Task<ActionResult> Delete(string id)
- {
- AppRole role = await RoleManager.FindByIdAsync(id);
- if (role != null)
- {
- IdentityResult result = await RoleManager.DeleteAsync(role);
- if (result.Succeeded)
- {
- return RedirectToAction("Index");
- }
- else
- {
- return View("Error", result.Errors);
- }
- }
- else
- {
- return View("Error", new string[] { "無法找到該Role" });
- }
- }
3.管理角色 MemberShip
要對使用者授權,除了建立和刪除角色之外,還需要對角色的MemberShip 進行管理,即通過Add /Remove 操作,可以向用戶新增/刪除角色。
為此,我添加了兩個ViewModel,RoleEditModel和RoleModificationModel,分別代表編輯時展示欄位和表單 Post時傳遞到後臺的欄位:
- public class RoleEditModel
- {
- public AppRole Role { get; set; }
- public IEnumerable<AppUser> Members { get; set; }
- public IEnumerable<AppUser> NonMembers { get; set; }
- }
- public class RoleModificationModel
- {
- public string RoleName { get; set; }
- public string[] IDsToAdd { get; set; }
- public string[] IDsToDelete { get; set; }
- }
在對角色進行編輯時,獲取所有隸屬於Role的成員和非隸屬於Role的成員:
- /// <summary>
- /// 編輯操作,獲取所有隸屬於此Role的成員和非隸屬於此Role的成員
- /// </summary>
- /// <param name="id"></param&