Identity - 網站會員管理 (四)

  • 5965
  • 0

此篇紀錄Identity的配置流程。

上一篇提到新增自 Identity 的 DataModel 的模型屬性與 Identity 如何更新資料庫結構。

這次主要介紹 : 

  • 更加了解認證(聲明),可達到更客製化的權限控管
  • 對於當前使用者增加認證(聲明)資訊

 

認證(聲明 Claims) :  

“Claim”在英文字典中不完全是“聲明”的意思,根據本文的描述,感覺把它說成“聲明”也不一定合適,所以在之後的譯文中基本都寫成中英文並用的形式,即“聲明(Claims)”。
聲明(Claims)的定義:聲明(Claims)是關於使用者的一些信息片段。一個使用者的信息片段當然有很多,每一個信息片段就是一項聲明(Claim),用戶的所有信息片段合起來就是該用戶的聲明(Claims)。請注意該單詞的單複數形式
你在應用程序中不一定要使用聲明(Claims),正如第之前的使用AuthManager給予Cookie的認證,ASP.NET Identity 能夠為應用程序提供充分的認證與授權服務,而根本不需要理解聲明(Claims)。

 


 

A. 理解聲明(認證) 建立 ClaimsController :

using System.Security.Claims; // 建立聲明的組件
using System.Web;
using System.Web.Mvc;

namespace MvcIdentityTest2.Controllers
{
    public class ClaimsController : Controller
    {
        [Authorize]
        public ActionResult Index()
        {
            //宣告聲明 並使用 當前使用者的 Identity 物件轉型
            ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;
            if (ident == null)
                return View("_Error", new string[] { "取得聲明失敗" });
            else
                return View(ident.Claims);
        }
    }
}

 

B. 建立 ClaimsController 中的 Index 所對應的頁面。於 \Views\Claims\ 建立 Index.cshtml :

@using System.Security.Claims @using MvcIdentityTest2.Infrastructure @model IEnumerable<Claim>
@{
    ViewBag.Title = "Claims";
}

<h2>Claims</h2>

<div class="panel panel-primary">
    <div class="panel-heading">
        Claims
    </div>
    <table class="table table-striped">
        <tr>
            <th>Subject</th>
            <th>Issuer</th>
            <th>Type</th>
            <th>Value</th>
        </tr>
        @foreach (var 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>
相關定義:  Subject(科目) Issuer(發行者) Type(類型)  Value(值)

 

C. 新增HtmlHelp擴充方法於 IdentityHelpers 類別 : 
只顯示修改程式碼與引入組件 =>

    using System.Reflection;
    using System.Security.Claims;
    using System.Linq; 

    public static class IdentityHelpers
    {
        ....
        /// <summary>
        /// 自定義的擴充方法 主要分析 <see cref="Claim"/> 聲明後拋出對應的字串
        /// </summary>
        public static MvcHtmlString ClaimType(this HtmlHelper html, string inClaimType)
        {
            //FieldInfo 裝載 物件欄位集合
            FieldInfo[] fields = typeof(ClaimTypes).GetFields();
            foreach(FieldInfo field in fields)
            {
                if(field.GetValue(null).ToString() == inClaimType)
                {
                    return new MvcHtmlString(field.Name);
                }
            }
            return new MvcHtmlString(string.Format("{0}",
                    inClaimType.Split(',', '.').Last()));
        }
    }
說明 : 在使用者每次發出請求都可以取得使用者的聲明

IIdentity idetn = HttpContext.User.Identity; 此時能使用的屬性 :
var str1 = ident.AuthenticationType; // 使用者此次的認證機制
var bool1 = ident.IsAuthenticated; // 使用者是否經過認證
var name = ident.Name; //使用者名稱

ClaimsIdentity idetn2 = HttpContext.User.Identity as ClaimsIdentity; 此時能使用的屬性 :
var str1 = ident.AuthenticationType; // 使用者此次的認證機制
var bool1 = ident.IsAuthenticated; // 使用者是否經過認證
var name = ident.Name; //使用者名稱


// 此時還能在取得更細節的聲明資訊,針對單一聲明(Claim)
Claim claim = ident.Claims.GetEnumerator().Current; 此時能取得當前第一個元素的聲明 : 
var issuer = claim.Issuer; // 取得聲明授權者
var subject = claim.Subject; // 取得聲明主體名稱
var type = claim.Type; // 取得該聲明的類型(並非值的類型,是MVC作判別該聲明是什麼種類的聲明)
var value = claim.Value; // 取得聲明的值

 

D. 創建自定義聲明(認證) \Infrastructure\ 在該資料夾下建立 LocationClaimsProvider 類別 :

using System.Collections.Generic;
using System.Security.Claims;

namespace MvcIdentityTest2.Infrastructure
{
    public class LocationClaimsProvider
    {
        /// <summary>
        /// 額外增加 聲明資訊
        /// </summary>
        public static IEnumerable<Claim> GetClaims(ClaimsIdentity userIdent)
        {
            List<Claim> claims = new List<Claim>();
            // 若該聲明使用者名稱為 alice
            if (userIdent.Name.ToLower() == "alice")
            {
                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");
        }
    }
}

 

E. 認證過程期間,聲明(Claims)是於 AccountController 賦予的,再來修改 Login 的方法 : 
只顯示修改程式碼 =>

    [Authorize]
    public class AccountController : MyBaseController
    {
        ....
        [HttpPost][AllowAnonymous][ValidateAntiForgeryToken] // 預防跨網頁攻擊
        public async Task<ActionResult> Login(LoginModel details, string returnUrl)
        {
            // 前端的頁面是否已通過驗證??
            if (ModelState.IsValid)
            {
                // 使用 Identity 中的方法,使用 使用者姓名 與 密碼 找尋,若有的話則傳回一個對應使用者的 DataModel
                AppUser user = await base.BaseUserManager.FindAsync(details.Name, details.Password);
                if (user != null)
                {                    
                    // 依據對應的 DataModle 建立起 認證機制的物件 , 
                    ClaimsIdentity ident = await base.BaseUserManager.CreateIdentityAsync(user,
                        DefaultAuthenticationTypes.ApplicationCookie);

                    // 使用 LocationClaimsProvider 添加自定義聲明 <---修改部分
                    ident.AddClaims(Infrastructure.LocationClaimsProvider.GetClaims(ident));
                    
                    // 先將當前使用者的 Cookie 認證清除
                    base.BaseAuthManager.SignOut();
                    // 將當前使用者使用 Cookie 賦予相關的認證資訊。 IsPersistent = false 是讓 Cookie 不為永久性的
                    // 此時 [Authorize] 最基本的驗證已可使用
                    base.BaseAuthManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, ident);
                    // 跳轉至登入前頁面
                    return Redirect(returnUrl);
                }
                ModelState.AddModelError("", "使用者名稱或密碼錯誤");
            }
            ViewBag.returnUrl = returnUrl; // 儲存使用者 登入前頁面的 URL
            return View(details);
        }
        ....
    }

此時運行網站時使用分別 Admin Alice 使用者登入,觀察兩位使用者的聲明變化!! 可以看到 兩位使用者 分別加入了我們剛剛自定義的聲明

在SERVER中可輕易的取得聲明(Claims)意味著程序不一定都要透過資料庫取得資料,並且能夠與外部的資訊集合。Claim.Issuer屬性,能夠告訴你一個聲明(Claim)的授權者,這有助於讓你判斷資訊的精確程度,也有助於讓你決定這類資訊在應用程序中的權重。

 

F. 根據自定義的聲明內容,額外再添加給Controller授權屬性作判別的聲明 !! 代表可以使用聲明達到更客製化的效果。

  1. 在 \Infrastructure\ 資料夾下建立 ClaimsRoles 類別 : 
    using System.Collections.Generic;
    using System.Security.Claims;
    
    namespace MvcIdentityTest2.Infrastructure
    {
        public class ClaimsRoles
        {
            /// <summary>
            /// 針對特殊條件下的聲明 增加額外的聲明資訊
            /// </summary>
            public static IEnumerable<Claim> CreateRolesFromCliams(ClaimsIdentity userIdent)
            {
                List<Claim> claims = new List<Claim>();
                // 對傳遞進來的認證物件 做條件的篩選 : 
                // 當授權者為 RemoteClaims && 聲明值為 DC && 權限類別的聲明值為 Employees 
                if (userIdent.HasClaim(x => x.Type == ClaimTypes.StateOrProvince &&
                     x.Issuer == "RemoteClaims" && x.Value == "DC") && userIdent.HasClaim(x =>
                        x.Type == ClaimTypes.Role && x.Value == "Employees"))
                {
                    // 建立聲明 : 聲明型態、聲明值
                    claims.Add(new Claim(ClaimTypes.Role, "DCStaff"));
                }
                return claims;
            }
        }
    }

     

  2. 再來修改 AccountController 中的 Login 方法 : 
    只顯示修改程式碼 =>
        [Authorize]
        public class AccountController : MyBaseController
        {
            ....
            [HttpPost][AllowAnonymous][ValidateAntiForgeryToken] // 預防跨網頁攻擊
            public async Task<ActionResult> Login(LoginModel details, string returnUrl)
            {
                // 前端的頁面是否已通過驗證??
                if (ModelState.IsValid)
                {
                    // 使用 Identity 中的方法,使用 使用者姓名 與 密碼 找尋,若有的話則傳回一個對應使用者的 DataModel
                    AppUser user = await base.BaseUserManager.FindAsync(details.Name, details.Password);
                    if (user != null)
                    {                    
                        // 依據對應的 DataModle 建立起 認證機制的物件 , 
                        ClaimsIdentity ident = await base.BaseUserManager.CreateIdentityAsync(user,
                            DefaultAuthenticationTypes.ApplicationCookie);
    
                        // 使用 LocationClaimsProvider 添加自定義聲明
                        ident.AddClaims(Infrastructure.LocationClaimsProvider.GetClaims(ident));
                        // 使用 CreateRolesFromCliams 針對特殊的聲明條件添加自定義聲明  <---修改部分
                        ident.AddClaims(Infrastructure.ClaimsRoles.CreateRolesFromCliams(ident));
                        
                        // 先將當前使用者的 Cookie 認證清除
                        base.BaseAuthManager.SignOut();
                        // 將當前使用者使用 Cookie 賦予相關的認證資訊。 IsPersistent = false 是讓 Cookie 不為永久性的
                        // 此時 [Authorize] 最基本的驗證已可使用
                        base.BaseAuthManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, ident);
                        // 跳轉至登入前頁面
                        return Redirect(returnUrl);
                    }
                    ModelState.AddModelError("", "使用者名稱或密碼錯誤");
                }
                ViewBag.returnUrl = returnUrl; // 儲存使用者 登入前頁面的 URL
                return View(details);
            }
            ....
        }

     

  3. 修改 ClaimsController :
    只顯示修改程式碼 =>
        public class ClaimsController : Controller
        {
            ....
            [Authorize(Roles = "DCStaff")]
            public string OtherAction()
            {
                return "This is the protected action";
            }
        }
    做到了這裡可以運行網站,並使用 Alice 與 Admin 登入後訪問 ClaimsController 中的 OtherAction 方法。可以看到 Alice 才有辦法訪問該方法。

 

G. 自定義 Controller 的 [Authorize]驗證屬性(Attribute)。

  1. 於 \Infrastructure\ 資料夾下建立 ClaimsAccessAttribute 類別 : 
    using System.Security.Claims;
    using System.Web;
    using System.Web.Mvc;
    
    namespace MvcIdentityTest2.Infrastructure
    {
        /// <summary>
        /// 增加 Controller 的屬性。類別名稱後面加 Attribute 且繼承自相關屬性的類別,即可在 Controller 使用
        /// </summary>
        public class ClaimsAccessAttribute : AuthorizeAttribute
        {
            /// <summary>
            /// 簽章者名稱
            /// </summary>
            public string Issuer { get; set; }
            /// <summary>
            /// 聲明型態
            /// </summary>
            public string ClaimsType { get; set; }
            /// <summary>
            /// 聲明內容(值)
            /// </summary>
            public string Value { get; set; }
    
            protected override bool AuthorizeCore(HttpContextBase httpContext)
            {
                //return base.AuthorizeCore(httpContext);
                return httpContext.User.Identity.IsAuthenticated && //當前使用者是否以使用聲明驗證
                    httpContext.User.Identity is ClaimsIdentity && //確認當前用戶的驗證型態
                    (httpContext.User.Identity as ClaimsIdentity).HasClaim(x =>
                        x.Issuer == this.Issuer && x.Type == this.ClaimsType && x.Value == this.Value);
            }
        }
    }

     

  2. 修改 ClaimsController,套用自定義屬性(Attribute) : 
    只顯示修改程式碼與引入組件 =>

        using System.Security.Claims; // 建立聲明的組件
        using System.Web.Mvc;
        using MvcIdentityTest2.Infrastructure; // 使用自定義屬性時需引入命名空間
        
        public class ClaimsController : Controller
        {
            ....
            // 使用自定義屬性
            [ClaimsAccess(Issuer = "RemoteClaims", ClaimsType = ClaimTypes.PostalCode, Value = "NY 10036")]
            public string OtherAction2()
            {
                return "This is the protected action";
            }
        }
    做到這裡運行網站使用 Alice && Admin 登入網站,並試著訪問 ClaimsController 中的方法觀其變化。

 

第三方認證 大致講解原理 : 
  • Identity 使用 第三方的Api服務 傳回的 Cookie認證,創建對應的使用者並且為該使用者加入聲明
  • 網站若要是第三方認證就不應該授予使用網站自身創建帳號,這在網站端管理起來會較為複雜尤其是在使用者帳密規範這一塊,因為就算使用第三方認證還是會跑基本原本設定的規範程序。 (邏輯上可以做到,但會使規範類別較為複雜)
  • 關於第三方認證可參考 : 推薦網站

 

※到了這裡應該要了解 :

  • 對於聲名(認證)更加了解,Identity 加入 ClaimsIdentity 到當前使用者的 Cookie認證資訊中 :
    a. 聲明的關聯性 =>  Claim -> Claims -> ClaimsIdentity -> IIdentity 
    b. 了解 IIdentity 聲明的相關屬性。EX: Name、IsAuthenticated、Iusser、Type、Value...等屬性
    c. 自定義 Claims 並使用 ClaimsIdentity 添加後再使用 AuthManage 註冊當前使用者
  • Controller 的認證機制是透過當前使用者的 IIdentity 這個物件資訊來做判斷
  • 自定義 Controller 的 Attribute屬性
總結 : 
使用 Identity 提供的機制,已能方便管理會員與權限機制,並且透過聲明讓Controller判別權限更加輕便。
若對 Identity && Owin 類別有所領悟那在會員機制的使用上應能達到很多需求與目的了,且有基本的安全機制。

 


多多指教!! 歡迎交流!!

你不知道自己不知道,那你會以為你知道