Identity(四)

本文摘自:ASP.NET MVC 隨想錄——探索ASP.NET Identity 身份驗證和基於角色的受權,中級篇html

探索身份驗證與受權web


 

在這一小節中,我將闡述和證實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以及匿名用戶訪問。mvc

<authorization> 
   <allow users="Kim"/> 
   <allow roles="Administrator"/> 
   <deny users="John"/> 
   <deny users="?"/> 
</authorization>

經過身份驗證和受權,咱們能夠對應用程序敏感的區域進行受限訪問,這確保了數據的安全性。app

2.使用Katana進行身份驗證 less

到目前爲止,你可能已經對OWIN、Katana 、 Middleware 有了基本的瞭解,若是不清楚的話,請移步到瀏覽。async

使用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> 
/// <returns></returns> 
public async Task<ActionResult> Edit(string id) 
{ 
    AppRole role = await RoleManager.FindByIdAsync(id); 
    string[] memberIDs = role.Users.Select(x => x.UserId).ToArray(); 
    IEnumerable<AppUser> members = UserManager.Users.Where(x => memberIDs.Any(y => y == x.Id)); 
    IEnumerable<AppUser> nonMembers = UserManager.Users.Except(members); 
    return View(new RoleEditModel() 
    { 
        Role = role, 
        Members = members, 
        NonMembers = nonMembers 
    }); 
} 

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

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

,以下所示:

[HttpPost] 
public async Task<ActionResult> Edit(RoleModificationModel model) 
{ 
    IdentityResult result; 
    if (ModelState.IsValid) 
    { 
        foreach (string userId in model.IDsToAdd??new string[] {}) 
        { 
            result = await UserManager.AddToRoleAsync(userId, model.RoleName); 
            if (!result.Succeeded) 
            { 
                return View("Error", result.Errors); 
            } 
        } 
        foreach (var userId in model.IDsToDelete??new string[] {}) 
        { 
            result = await UserManager.RemoveFromRoleAsync(userId, model.RoleName); 
            if (!result.Succeeded) 
            { 
                return View("Error", result.Errors); 
            } 
        } 
        return RedirectToAction("Index"); 
    } 
    return View("Error",new string[] {"沒法找到此角色"}); 
} 

在上述代碼中,你可能注意到了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的用戶才能訪問和操做。

[Authorize(Roles = "Administrator")] 
public class RoleController : Controller 
{ 
} 

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

要解決這個問題,咱們通常是在數據庫中內置一個管理員角色,這也是咱們熟知的超級管理員角色。經過Entity Framework Seed,咱們能夠輕鬆實現數據的初始化:

public class IdentityDbInit 
    : DropCreateDatabaseIfModelChanges<AppIdentityDbContext> 
{ 
    protected override void Seed(AppIdentityDbContext context) 
    { 
        PerformInitialSetup(context); 
        base.Seed(context); 
    } 
 
    public void PerformInitialSetup(AppIdentityDbContext context) 
    { 
        // 初始化 
        AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context)); 
        AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context)); 
 
        string roleName = "Administrators"; 
        string userName = "Admin"; 
        string password = "Password2015"; 
        string email = "admin@jkxy.com"; 
 
        if (!roleMgr.RoleExists(roleName)) 
        { 
            roleMgr.Create(new AppRole(roleName)); 
        } 
 
        AppUser user = userMgr.FindByName(userName); 
        if (user == null) 
        { 
            userMgr.Create(new AppUser { UserName = userName, Email = email }, 
                password); 
            user = userMgr.FindByName(userName); 
        } 
 
        if (!userMgr.IsInRole(user.Id, roleName)) 
        { 
            userMgr.AddToRole(user.Id, roleName); 
        } 
    } 
} 

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


小結

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

相關文章
相關標籤/搜索