閱讀目錄 走進聲明的世界 建立並使用聲明 基於聲明的授權 使用第三方來身分識別驗證 小節
在這篇文章中,我將繼續ASP.NET Identity 之旅,這也是ASP.NET Identity 三部曲的最後一篇。在本文中,將為大家介紹ASP.NET Identity 的進階功能,它支援聲明式並且還可以靈活的與ASP.NET MVC 授權結合使用,同時,它還支援使用第三方來實現身分識別驗證。
關於ASP.NET Identity 的基礎知識,請參考如下文章:
ASP.NET MVC 隨想錄——開始使用ASP.NET Identity,初級篇
ASP.NET MVC 隨想錄——探索ASP.NET Identity 身分識別驗證和基於角色的授權,中級篇
本文的樣本,你可以在此下載和預覽:
點此進行預覽
點此下載範例程式碼 回到頂部 走進聲明的世界
在舊的使用者管理系統,例如使用了ASP.NET Membership的應用程式,我們的應用程式被認為是擷取使用者所有資訊的權威來源,所以本質上可以將應用程式視為封閉的系統,它包含了所有的使用者資訊。在上一篇文章中,我使用ASP.NET Identity 驗證使用者儲存在資料庫的憑據,並根據與這些憑據相關聯的角色進行授權訪問,所以本質上身分識別驗證和授權所需要的使用者資訊來源於我們的應用程式。
ASP.NET Identity 還支援使用聲明來和使用者打交道,它效果很好,而且應用程式並不是使用者資訊的唯一來源,有可能來自外部,這比傳統角色授權來的更為靈活和方便。
接下來我將為大家介紹ASP.NET Identity 是如何支援基於聲明的授權(claims-based authorization)。
1.理解什麼是聲明
聲明(Claims)其實就是使用者相關的一條一條資訊的描述,這些資訊包括使用者的身份(如Name、Email、Country等)和角色成員,而且,它描述了這些資訊的類型、值以及發布聲明的認證方等。我們可以使用聲明來實現基於聲明的授權。聲明可以從外部系統獲得,當然也可以從本機使用者資料庫擷取。
對於ASP.NET MVC應用程式,通過自訂AuthorizeAttribute,聲明能夠被靈活的用來對指定的Action 方法授權訪問,不像傳統的使用角色授權那麼單一,基於聲明的授權更加豐富和靈活,它允許使用使用者資訊來驅動授權訪問。
既然聲明(Claim)是一條關於使用者資訊的描述,最簡單的方式來闡述什麼是聲明就是通過具體的例子來展示,這比抽象概念的講解來的更有用。所以,我在樣本項目中添加了一個名為Claims 的 Controller,它的定義如下所示: public class ClaimsController : Controller { [Authorize] public ActionResult Index() { ClaimsIdentity claimsIdentity = HttpContext.User.Identity as ClaimsIdentity; if (claimsIdentity == null) { return View("Error", new string[] {"未找到聲明"}); } else { return View(claimsIdentity.Claims); } } }
在這個例子中可以看出ASP.NET Identity 已經很好的整合到ASP.NET 平台中,而HttpContext.User.Identity 屬性返回一個 IIdentity 介面的實現,而當與ASP.NET Identity 結合使用時,返回的是ClaimsIdentity 對象。
ClaimsIdentity 類被定義在System.Security.Claims 名稱空間下,它包含如下重要的成員:
Claims |
返回使用者包含的聲明對象集合 |
AddClaim(claim) |
為使用者添加一個聲明 |
AddClaims(claims) |
為使用者添加一系列聲明 |
HasClaim(predicate) |
判斷是否包含聲明,如果是,返回True |
RemoveClaim(claim) |
為使用者移除聲明 |
當然ClaimsIdentity 類還有更多的成員,但上述表描述的是在Web應用程式中使用頻率很高的成員。在上述代碼中,將HttpContext.User.Identity 轉換為ClaimsIdentity 對象,並通過該對象的Claims 屬性擷取到使用者相關的所有聲明。
一個聲明對象代表了使用者的一條單獨的資訊資料,聲明對象包含如下屬性:
Issuer |
返回提供聲明的認證方名稱 |
Subject |
返回聲明指向的ClaimIdentity 對象 |
Type |
返回聲明代表的資訊類型 |
Value |
返回聲明代表的使用者資訊的值 |
有了對聲明的基本概念,對上述代碼的View進行修改,它呈現使用者所有聲明資訊,相應的視圖代碼如下所示: @using System.Security.Claims @using Users.Infrastructure @model IEnumerable<Claim> @{ ViewBag.Title = "Index"; } <div class="panel panel-primary"> <div class="panel-heading"> 聲明 </div> <table class="table table-striped"> <tr> <th>Subject</th> <th>Issuer</th> <th>Type</th> <th>Value</th> </tr> @foreach (Claim claim in Model.OrderBy(x=>x.Type)) { <tr> <td>@claim.Subject.Name</td> <td>@claim.Issuer</td> <td>@Html.ClaimType(claim.Type)</td> <td>@claim.Value</td> </tr> } </table> </div>
Claim對象的Type屬性返回URI Schema,這對於我們來說並不是特別有用,常見的被用來當作值的Schema定義在System.Security.Claims.ClaimType 類中,所以要使輸出的內容可讀性更強,我添加了一個HTML helper,它用來格式化Claim.Type 的值: public static MvcHtmlString ClaimType(this HtmlHelper html, string claimType) { FieldInfo[] fields = typeof(ClaimTypes).GetFields(); foreach (FieldInfo field in fields) { if (field.GetValue(null).ToString() == claimType) { return new MvcHtmlString(field.Name); } } return new MvcHtmlString(string.Format("{0}", claimType.Split('/', '.').Last())); }
有了上述的基礎設施代碼後,我請求ClaimsController 下的Index Action時,顯示使用者關聯的所有聲明,如下所示:
回到頂部 建立並使用聲明
有兩個原因讓我覺得聲明很有趣。第一個原因是,應用程式能從多個來源擷取聲明,而不是僅僅依靠本機資料庫來擷取。在稍後,我會向你展示如何使用外部第三方系統來驗證使用者身份和建立聲明,但此時我添加一個類,來類比一個內部提供聲明的系統,將它命名為LocationClaimsProvider,如下所示: public static class LocationClaimsProvider { public static IEnumerable<Claim> GetClaims(ClaimsIdentity user) { List<Claim> claims=new List<Claim>(); if (user.Name.ToLower()=="admin") { claims.Add(CreateClaim(ClaimTypes.PostalCode, "DC 20500")); claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "DC")); } else { claims.Add(CreateClaim(ClaimTypes.PostalCode, "NY 10036")); claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "NY")); } return claims; } private static Claim CreateClaim(string type,string value) { return new Claim(type, value, ClaimValueTypes.String, "RemoteClaims"); } }
上述代碼中,GetClaims 方法接受一個參數為ClaimsIdentity 對象並為使用者建立了PostalCode和StateOrProvince的聲明。在這個類中,假設我類比一個系統,如一個中央的人力資源資料庫,那麼這將是關於工作人員本地資訊的權威來源。
聲明是在身分識別驗證過程被添加到使用者中,故在Account/Login Action對代碼稍作修改: [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); claimsIdentity.AddClaims(LocationClaimsProvider.GetClaims(claimsIdentity)); AuthManager.SignOut(); AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity); return Redirect(returnUrl);