ASP.NET Identity 身份驗證和基於角色的授權


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會對HttpApplication 的管道(Pipeline)事件AuthenticateRequest 進行注冊,當請求經過ASP.NET Pipeline時,由ASP.NET Runtime 觸發它,在該事件中,它會驗證並解析該Cookie為對應的用戶對象,它是一個實現了 IPrincipal接口的對象。PostAuthenticateRequest 事件在AuthenticateRequest 事件之后觸發,表示用戶身份已經檢查完成 ,檢查后的用戶可以通過HttpContextUser屬性獲取並且HttpContext.User.Identity.IsAuthenticated屬性為True。

如果將身份驗證看作是"開門"的話,主人邀請你進屋,但這並不意味着你可以進入到卧室或者書房,可能你的活動場所僅限書房——這就是授權。在PostAuthenticateRequest事件觸發過后,會觸發AuthorizeRequest 事件,它在UrlAuthorizationModule 中被注冊(題外插一句:UrlAuthorizationModule 以及上面提到的FormsAuthenticationModule你可以在IIS 級別的.config文件中找到,這也是ASP.NET 和 IIS緊耦合關系的體現)。在該事件中,請求的URL會依據web.config中的authorization 配置節點進行授權,如下所示授予Kim以及所有Role為Administrator的成員具有訪問權限,並且拒絕John以及匿名用戶訪問。

  1. <authorization>
  2.    <allow users="Kim"/>
  3.    <allow roles="Administrator"/>
  4.    <deny users="John"/>
  5.    <deny users="?"/>
  6. </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已經幫助我們封裝好了一個擴展方法,如下所示,

  1. app.UseCookieAuthentication(new CookieAuthenticationOptions
  2.  {
  3.            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  4.            LoginPath = new PathString("/Account/Login")
  5.   });

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 特性進行授權,如下代碼所示:

  1. [Authorize]
  2. public ActionResult Index()
  3. {
  4.     return View();
  5. }

上述代碼中,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] 特性標簽,如下所示:

  1. [AllowAnonymous]
  2. public ActionResult Login(string returnUrl)
  3. {
  4.     //如果登錄用戶已經Authenticated,提示請勿重復登錄
  5.     if (HttpContext.User.Identity.IsAuthenticated)
  6.     {
  7.         return View("Error", new string[] {"您已經登錄!"});
  8.     }
  9.     ViewBag.returnUrl = returnUrl;
  10.     return View();
  11. }

注意,在這兒我將ReturnUrl 存儲了起來,ReturnUrl 顧名思義,當登錄成功后,重定向到最初的地址,這樣提高了用戶體驗。

由於篇幅的限制,Login View 我不將代碼貼出來了,事實上它也非常簡單,包含如下內容:

  • 用戶名文本框
  • 密碼框
  • 存儲ReturnUrl的隱藏域
  • @Html.AntiForgeryToken(),用來防止CSRF跨站請求偽造

2.添加用戶並實現身份驗證

當輸入了憑據之后,POST Form 表單到/Account/Login 下,具體代碼如下:

  1. [HttpPost]
  2. [AllowAnonymous]
  3. [ValidateAntiForgeryToken]
  4. public async Task<ActionResult> Login(LoginModel model,string returnUrl)
  5. {
  6.     if (ModelState.IsValid)
  7.     {
  8.         AppUser user = await UserManager.FindAsync(model.Name, model.Password);
  9.         if (user==null)
  10.         {
  11.             ModelState.AddModelError("","無效的用戶名或密碼");
  12.         }
  13.         else
  14.         {
  15.             var claimsIdentity =
  16.                 await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
  17.             AuthManager.SignOut();
  18.             AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity);
  19.             return Redirect(returnUrl);
  20.         }
  21.     }
  22.     ViewBag.returnUrl = returnUrl;
  23.  
  24.     return View(model);
  25. }

上述代碼中,首先使用 ASP.NET Identity 來驗證用戶憑據,這是通過 AppUserManager 對象的FindAsync 方法來實現,如果你不了解ASP.NET Identity 基本API ,請參考我這篇文章

  1. 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行代碼,如下所示:

  1. var claimsIdentity =
  2.     await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
  3. AuthManager.SignOut();
  4. 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:

  1. return Redirect(returnUrl);

使用角色進行授權

在前一小節中,使用了Authorize 特性對指定區域進行受限訪問,只有被身份驗證通過后才能繼續訪問。在這一小節將更細粒度進行授權操作,在ASP.NET MVC Framework 中,Authorize 往往結合User 或者 Role 屬性進行更小粒度的授權操作,正如如下代碼所示:

  1. [Authorize(Roles = "Administrator")]
  2. public class RoleController : Controller
  3. {
  4. }

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:

  1. public class AppRole:IdentityRole
  2. {
  3.     public AppRole() : base() { }
  4.     public AppRole(string name) : base(name) { }
  5.     // 在此添加額外屬性
  6. }

同時,再定義一個AppRoleManager 類,如同AppUserManager 一樣,它繼承RoleManager<T>,提供了檢索和持久化Role的基本方法:

  1. public class AppRoleManager:RoleManager<AppRole>
  2. {
  3.     public AppRoleManager(RoleStore<AppRole> store):base(store)
  4.     {
  5.     }
  6.  
  7.     public static AppRoleManager Create(IdentityFactoryOptions<AppRoleManager> options, IOwinContext context)
  8.     {
  9.         return new AppRoleManager(new RoleStore<AppRole>(context.Get<AppIdentityDbContext>()));
  10.     }
  11. }

最后,別忘了在OWIN Startup類中初始化該實例,它將存儲在OWIN上下文環境字典中,貫穿了每一次HTTP請求:

  1. app.CreatePerOwinContext(AppIdentityDbContext.Create);
  2. app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
  3. app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);

2.創建和刪除角色

使用ASP.NET Identity 創建和刪除角色很簡單,通過從OWIN 上下文中獲取到AppRoleManager,然后Create 或者 Delete,如下所示:

  1. [HttpPost]
  2. public async Task<ActionResult> Create(string name)
  3. {
  4.     if (ModelState.IsValid)
  5.     {
  6.         IdentityResult result = await RoleManager.CreateAsync(new AppRole(name));
  7.         if (result.Succeeded)
  8.         {
  9.             return RedirectToAction("Index");
  10.         }
  11.         else
  12.         {
  13.             AddErrorsFromResult(result);
  14.         }
  15.     }
  16.     return View(name);
  17. }
  18.  
  19. [HttpPost]
  20. public async Task<ActionResult> Delete(string id)
  21. {
  22.     AppRole role = await RoleManager.FindByIdAsync(id);
  23.     if (role != null)
  24.     {
  25.         IdentityResult result = await RoleManager.DeleteAsync(role);
  26.         if (result.Succeeded)
  27.         {
  28.             return RedirectToAction("Index");
  29.         }
  30.         else
  31.         {
  32.             return View("Error", result.Errors);
  33.         }
  34.     }
  35.     else
  36.     {
  37.         return View("Error", new string[] { "無法找到該Role" });
  38.     }
  39. }

3.管理角色 MemberShip

要對用戶授權,除了創建和刪除角色之外,還需要對角色的MemberShip 進行管理,即通過Add /Remove 操作,可以向用戶添加/刪除角色。

為此,我添加了兩個ViewModel,RoleEditModel和RoleModificationModel,分別代表編輯時展示字段和表單 Post時傳遞到后台的字段:

  1. public class RoleEditModel
  2. {
  3.     public AppRole Role { get; set; }
  4.     public IEnumerable<AppUser> Members { get; set; }
  5.     public IEnumerable<AppUser> NonMembers { get; set; }
  6. }
  7. public class RoleModificationModel
  8. {
  9.     public string RoleName { get; set; }
  10.     public string[] IDsToAdd { get; set; }
  11.     public string[] IDsToDelete { get; set; }
  12. }

在對角色進行編輯時,獲取所有隸屬於Role的成員和非隸屬於Role的成員:

  1. /// <summary>
  2. /// 編輯操作,獲取所有隸屬於此Role的成員和非隸屬於此Role的成員
  3. /// </summary>
  4. /// <param name="id"></param>
  5. /// <returns></returns>
  6. public async Task<ActionResult> Edit(string id)
  7. {
  8.     AppRole role = await RoleManager.FindByIdAsync(id);
  9.     string[] memberIDs = role.Users.Select(x => x.UserId).ToArray();
  10.     IEnumerable<AppUser> members = UserManager.Users.Where(x => memberIDs.Any(y => y == x.Id));
  11.     IEnumerable<AppUser> nonMembers = UserManager.Users.Except(members);
  12.     return View(new RoleEditModel()
  13.     {
  14.         Role = role,
  15.         Members = members,
  16.         NonMembers = nonMembers
  17.     });
  18. }

最終呈現的視圖如下所示:

當點擊保存,提交表單時,通過模型綁定,將數據Post 到Edit Action,實現了對角色的MemberShip 進行管理,即通過Add /Remove 操作,可以向用戶添加/刪除角色。

,如下所示:

  1. [HttpPost]
  2. public async Task<ActionResult> Edit(RoleModificationModel model)
  3. {
  4.     IdentityResult result;
  5.     if (ModelState.IsValid)
  6.     {
  7.         foreach (string userId in model.IDsToAdd??new string[] {})
  8.         {
  9.             result = await UserManager.AddToRoleAsync(userId, model.RoleName);
  10.             if (!result.Succeeded)
  11.             {
  12.                 return View("Error", result.Errors);
  13.             }
  14.         }
  15.         foreach (var userId in model.IDsToDelete??new string[] {})
  16.         {
  17.             result = await UserManager.RemoveFromRoleAsync(userId, model.RoleName);
  18.             if (!result.Succeeded)
  19.             {
  20.                 return View("Error", result.Errors);
  21.             }
  22.         }
  23.         return RedirectToAction("Index");
  24.     }
  25.     return View("Error",new string[] {"無法找到此角色"});
  26. }

在上述代碼中,你可能注意到了UserManager 類,它包含了若干與角色相關的操作方法:

AddToRoleAsync(string userId,string role)

添加用戶到指定的角色中

GetRolesAsync(string userId)

獲取User對應的角色列表

IsInRoleAsync(string userId,string role)

判斷用戶是否隸屬於指定的角色

RemoveFromRoleAsync(string userId,string role)

將用戶從指定角色中排除

 

初始化數據,Seeding 數據庫

在上一小節中,通過Authorize 標簽將Role 控制器受限訪問,只有Role=Administrator的用戶才能訪問和操作。

  1. [Authorize(Roles = "Administrator")]
  2. public class RoleController : Controller
  3. {
  4. }

但當我們的應用程序部署到新環境時,是沒有具體的用戶數據的,這就導致我們無法訪問Role Controller。這是一個典型的 "雞生蛋還是蛋生雞"問題。

要解決這個問題,我們一般是在數據庫中內置一個管理員角色,這也是我們熟知的超級管理員角色。通過Entity Framework Seed,我們可以輕松實現數據的初始化:

  1. public class IdentityDbInit
  2.     : DropCreateDatabaseIfModelChanges<AppIdentityDbContext>
  3. {
  4.     protected override void Seed(AppIdentityDbContext context)
  5.     {
  6.         PerformInitialSetup(context);
  7.         base.Seed(context);
  8.     }
  9.  
  10.     public void PerformInitialSetup(AppIdentityDbContext context)
  11.     {
  12.         // 初始化
  13.         AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));
  14.         AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context));
  15.  
  16.         string roleName = "Administrators";
  17.         string userName = "Admin";
  18.         string password = "Password2015";
  19.         string email = "admin@jkxy.com";
  20.  
  21.         if (!roleMgr.RoleExists(roleName))
  22.         {
  23.             roleMgr.Create(new AppRole(roleName));
  24.         }
  25.  
  26.         AppUser user = userMgr.FindByName(userName);
  27.         if (user == null)
  28.         {
  29.             userMgr.Create(new AppUser { UserName = userName, Email = email },
  30.                 password);
  31.             user = userMgr.FindByName(userName);
  32.         }
  33.  
  34.         if (!userMgr.IsInRole(user.Id, roleName))
  35.         {
  36.             userMgr.AddToRole(user.Id, roleName);
  37.         }
  38.     }
  39. }

在這兒實例化了AppUserManager和AppRoleManager實例,這是因為PerformInitialSetup 方法比OWIN 配置先執行。

小結

在這篇文章中,探索了使用ASP.NET Identity 進行身份驗證以及聯合ASP.NET MVC 基於角色的授權。最后實現了對角色的管理。在下一篇文章中,繼續ASP.NET Identity之旅,探索ASP.NET Identity 的高級應用——基於聲明的授權。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM