Identity - 網站會員管理 (二)

  • 6051
  • 0

此篇紀錄Identity的配置流程。

上篇講到了如何設置 Identity 的 IdentityConfig初始化物件、DbContext連線物件、使用者的DataModel、Identity的使用者管理器類與其它擴充的類別。達到創建、修改、查詢使用者帳戶並存於資料庫,且可規範使用者的帳號與密碼的規則。

那這一次主要是要介紹 :

  • 使用者登入時用 認證(授權)管理器 賦予使用者相對應的 聲明(認證),以達到登入的效果
  • 刪除與修改與查詢 權限 的資料
  • 創建 權限模型的 DataModel
  • 創建 權限管理器類 達到管理權限 的功能
  • 對於 EF 資料庫初始化會有更深的體會。如在資料庫初始化的同時加入一些既定的資料

 

理解認證與授權過程

A. 再之前建立的 HomeController 中的 Index 方法加入驗證屬性。
     修改 HomeController 只顯示修改的程式碼 =>
public class HomeController : Controller 
{
    [Authorize] // 驗證屬性: 表示只有經過認證的才可使用該方法。 
    public ActionResult Index() { 
       .....
    } 
    .....
}
修改好之後執行起來,會看到錯誤的頁面,因為使用者若未驗證 MVC 會自動導向於設定中的頁面,這個設定在哪裡呢?? 就在 IdentityConfig 的自定義類別中。

 

public class IdentityConfig
{
   ....
   // 設定 Owin 中間層的 Cookie 認證機制
   app.UseCookieAuthentication(new CookieAuthenticationOptions {
       AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
       LoginPath = new PathString("/Account/Login"), // 若未驗證 導向於指定頁面 <---
   });
}
Identity 含有一個處理 AuthenticateRequest 生命週期事件的模塊,並使用瀏覽器發送過來的 Cookie 確認用戶是否已被認證。很快會演示如何創建這些 Cookie。如果用戶已被認證,此ASP.NET框架模塊便會將 IIdentity.IsAuthenticated 屬性的值設置為true,否則設置為false。( 此刻尚未實現讓使用者進行認證的特性,所以說IsAuthenticated屬性的值總是false )

 

 B. 創建一個 ViewModel 讓使用者可做登入的動作。再 \models\UserViewModels.cs 中創建一個類別 :
    修改 UserViewModels.cs 只顯示修改的程式碼 =>
namespace MvcIdentityTest2.Models
{
    ....
    /// <summary>
    /// 該類別是使用者登入帳號時所用的ViewModel
    /// </summary>
    public class LoginModel
    {
        [Required]
        public string Name { get; set; }
         [Required]
        public string Password { get; set; }
    }
}
 
C. 創建一個控制器名稱為 AccountController :
using System.Threading.Tasks;
using System.Web.Mvc;
using MvcIdentityTest2.Models;

namespace MvcIdentityTest2.Controllers
{
    [Authorize]
    public class AccountController : MyBaseController
    {
        [AllowAnonymous] //該方法可不需驗證即可使用
        public ActionResult Login(string returnUrl)
        {
            ViewBag.returnUrl = returnUrl;
            return View();
        }

        [HttpPost]
        [AllowAnonymous] // 該方法不需驗證
        [ValidateAntiForgeryToken] // 預防跨網頁攻擊
        public async Task<ActionResult> Login(LoginModel details, string returnUrl)
        {
            //前端的頁面是否已通過驗證??
            if (ModelState.IsValid)
            {
            }
            return View(details);
        }
    }
}
首先要注意Login動作方法有兩個版本,它們都有一個名稱為returnUrl的參數。當用戶請求一個受限的URL時,他們被重定向到/Account/Login URL上,並帶有查詢字符串,該字符串指定了一旦用戶得到認證後將用戶返回的URL,如下所示:
            /Account/Login ?ReturnUrl=%2FHome%2FIndex
ReturnUrl查詢字符串參數的值可讓我能夠對用戶進行重定向,使應用程序公開和保密部分之間的導航成為一個平滑無縫的過程。

 

D. 建立一個登入的頁面 請再 \Views\Account\ 底下建立 Login.cshtml : 
@model MvcIdentityTest2.Models.LoginModel

@{
    ViewBag.Title = "Login";
}

<h2>Log In</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @*已經在Login的方法中將 第一次導向時的 URL 中的 ?ReturnUrl 的值儲存起來*@
    @Html.Hidden("returnUrl", new { returnUrl = ViewBag.returnUrl })

    <div class="form-horizontal">
        <h4>LoginModel</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.PasswordFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Log In" class="btn btn-default" />
            </div>
        </div>
    </div>
}
跟著步驟做到這裡,對應的 ViewModel 與 Controller、.cshtml頁面也都建立完成,執行起來看看。
那接下來的步驟會繼續完成登入細節的部分。

 

E. 再 MyBaseController 中新增取得 當前的 認證(註冊認證)管理器類 :
    這裡只顯示修改程式碼與引入組件 => 
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.AspNet.Identity;
using System.Linq;
using Microsoft.Owin.Security;

namespace MvcIdentityTest2.Controllers
{
    public abstract class MyBaseController : Controller
    {
        ....
        /// <summary>
        /// 取得當前的 Identity 認證管理器類 <see cref="IAuthenticationManager"/>
        /// </summary>
        protected virtual IAuthenticationManager BaseAuthManager {
            get {
                //此方法是 Owin 對於 HttpContext 新增的擴充方法
                return HttpContext.GetOwinContext().Authentication;
            }
        }
    }
}

 

F. 為帳戶驗證通過的使用者添加聲明(認證),再回到 AccountController 中完成接續的動作 :
using System.Threading.Tasks;
using System.Web.Mvc;
using MvcIdentityTest2.Models;

using System.Security.Claims;
using Microsoft.AspNet.Identity;
using Microsoft.Owin.Security;

namespace MvcIdentityTest2.Controllers
{
    [Authorize]
    public class AccountController : MyBaseController
    {
        [AllowAnonymous] //該方法可不需驗證即可使用
        public ActionResult Login(string returnUrl)
        {
            ViewBag.returnUrl = returnUrl;
            return View();
        }

        [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);
                    // 先將當前使用者的 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);
        }
    }
}

 

最需要注意的地方有兩個物件 ClaimsIdentity、base.BaseAuthManager在 Identity 再 Owin 中新增的 Authentication 認證管理器類 : 
認證管理器類主要是為當前使用者 註冊一組認證過的 Cookie,該段Cookie 儲存認證的相關資訊。
ClaimsIdentity 這個類別很久以前就已經存在,Identity就是利用這個物件產生認證的相關資訊。後面會對這個物件做更多的說明。現階段只要知道是什麼做什麼用的即可。
以上步驟完成後,就可以測試一下驗證機制是否正常,順便看看那些類別是如何溝通的也是重點項目。
可以用瀏覽器的F12工具,看到用來標識已認證請求的Cookie。

 

G. 管理權限 與 賦予角色權限 :
  1. 建立 繼承自 IdentityRole 類的 DataModel : 再\Models\底下建立 AppRole.cs
    using Microsoft.AspNet.Identity.EntityFramework;
    
    namespace MvcIdentityTest2.Models
    {
        /// <summary>
        ///  該類別是為使用者註冊 角色(細項權限)時所需資訊 ViewModel。
        ///  繼承自 <see cref="IdentityRole"/>
        /// </summary>
        public class AppRole : IdentityRole
        {
            public AppRole() : base()
            {
            }
            // 建立權限時會用到多載建構式
            public AppRole(string roleName) : base(roleName)
            {
            }
        }
    }

     

  2. 建立 權限管理器類 繼承自 RoleManager<T> 請再 \Infrastructure\ 底下建立 AppRoleManager.cs
    using System;
    using Microsoft.AspNet.Identity;
    using Microsoft.AspNet.Identity.EntityFramework;
    using Microsoft.AspNet.Identity.Owin;
    using Microsoft.Owin;
    using MvcIdentityTest2.Models;
    
    namespace MvcIdentityTest2.Infrastructure
    {
        /// <summary>
        /// 該類別是 Identity 所提供的 <see cref="權限管理器類"/>。主要就是將 權限相關的設定 寫入SQL
        /// 並提供相對應的方法。
        /// </summary>
        public class AppRoleManager : RoleManager<AppRole>, IDisposable
        // RoleManager<T> : 條件約束是 where : class,IRole
        {
            public AppRoleManager(IRoleStore<AppRole, string> store) : base(store)
            {
            }
    
            /// <summary>
            /// 此方法為 回傳 <see cref="AppRoleManager"/>實例。( 使用 <see cref="AppIdentityDbContext"/> 的連線設定 )
            /// </summary>
            public static AppRoleManager Create(IdentityFactoryOptions<AppRoleManager> option,
                IOwinContext context)
            {
                // 創建一個帶參的 AppRoleManager 物件並回傳
                return new AppRoleManager(new RoleStore<AppRole>(context.Get<AppIdentityDbContext>()));
            }
        }
    }
    這個類定義了一個Create方法,它讓OWIN啟動類能夠為每一個訪問Identity數據的請求創建實例,這意味著在整個應用程序中,我不必散佈如何存儲角色數據的細節,卻能獲取AppRoleManager類的實例,並對其進行操作可以看到如何用OWIN啟動類(IdentityConfig)來註冊權限管理器類。這樣能夠確保,可以使用與AppUserManager類所用的同一個Entity Framework數據庫DbContext,來創建AppRoleManager類的實例。
  3. 再 IdentityConfig 中通知 Identity && Owin 我們有自定義的 權限管理器類,並且之後可以直接從 Owin 中取得 權限管理器類 的實例 : 修改 IdentityConfig 類別,在這只顯示修改程式碼 =>
    public class IdentityConfig
    {
        public void Configuration(IAppBuilder app)
        {
            ....
            // 設定 IdentityRoleManager 並提供返回對應型別實例的方法
            app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);
            ....
        }
    }

     

  4. 再 MyBaseController 中新增取得 當前的 權限(角色)管理器類 : 這裡只顯示修改程式碼 =>
    public abstract class MyBaseController : Controller
    {
        ....
        /// <summary>
        /// 取得當前的 Identity 權限管理器類 <see cref="Infrastructure.AppRoleManager"/>
        /// </summary>
        protected virtual Infrastructure.AppRoleManager BaseRoleManager {
            get {
                return HttpContext.GetOwinContext().GetUserManager<Infrastructure.AppRoleManager>();
            }
        }
        ....
    }

     

  5. 創建一個控制器名稱為 RoleAdminController 並建立 新增與刪除的方法 :
    using System.ComponentModel.DataAnnotations;
    using System.Threading.Tasks;
    using System.Web.Mvc;
    using Microsoft.AspNet.Identity;
    using MvcIdentityTest2.Models;
    
    namespace MvcIdentityTest2.Controllers
    {
        public class RoleAdminController : MyBaseController
        {
            public ActionResult Index()
            {
                return View(base.BaseRoleManager.Roles);
            }
    
            public ActionResult Create()
            {
                return View();
            }
    
            [HttpPost]
            public async Task<ActionResult> Create([Required]string name) // 這個參數算是小型的 ViewModel
            {
                if (ModelState.IsValid)
                {
                    // 新增權限名稱
                    IdentityResult result = await base.BaseRoleManager.CreateAsync(new AppRole(name));
                    if (result.Succeeded)
                    {
                        return RedirectToAction("Index");
                    }
                    else
                    {
                        base.BaseAddErrorsFromResult(result);
                    }
                }
                return View(name);
            }
    
            [HttpPost]
            public async Task<ActionResult> Delete(string id)
            {
                // 透過ID找尋對應的權限(腳色)
                AppRole role = await base.BaseRoleManager.FindByIdAsync(id);
                // 若找不到則跳轉到錯誤頁面
                if (role is null) return View("Error", new string[] { "Role Not Found" });
                // 刪除該權限(腳色)
                IdentityResult result = await base.BaseRoleManager.DeleteAsync(role);
                if (result.Succeeded)
                {
                    return RedirectToAction("Index");
                }
                else
                {
                    return View("Error", result.Errors);
                }
            }
        }
    }

     

  6. Entity Framework 的 IdentityRole類 中定義了一個Users屬性,能夠取得腳色成員的IdentityUserRole使用者集合。每一個IdentityUserRole對像都有一個UserId屬性,回傳一個使用者的ID。不過我希望得到的是每個ID所對應的使用者名稱。
    在Infrastructure資料夾中增加一個類別,名稱為IdentityHelpers.cs,透過該類別擴充方法以便之後能用 使用者ID 取得對應的 使用者名稱 :
    using System.Web;
    using System.Web.Mvc;
    using Microsoft.AspNet.Identity.Owin;
    
    namespace MvcIdentityTest2.Infrastructure
    {
        /// <summary>
        /// 新增自定義的 <see cref="HtmlHelper"/> 類的擴充方法
        /// </summary>
        public static class IdentityHelpers
        {
            /// <summary>
            /// 使用權限管理器類下的 <see cref="UserId"/>
            /// 該方法透過 使用者管理器類 找到對應使用者的 Name 並回傳 <see cref="MvcHtmlString"/> 
            /// </summary>
            public static MvcHtmlString GetUserName(this HtmlHelper html, string id)
            {
                //取得 使用者管理器類
                AppUserManager userMgr = HttpContext.Current.GetOwinContext().GetUserManager<AppUserManager>();
                //使用 使用者管理器類的方法 後等待回傳Result 再從回傳資料中取得 UserName
                return new MvcHtmlString(userMgr.FindByIdAsync(id).Result.UserName);
            }
        }
    }

     

  7. 建立 RoleAdminController 的 Index 方法對應的頁面,請於 \Views\RoleAdmin\ 建立 Index.cshtml :
    @model IEnumerable<MvcIdentityTest2.Models.AppRole>
    @using MvcIdentityTest2.Infrastructure
    
    @{
        ViewBag.Title = "Roles";
    }
    
    <h2>Roles</h2>
    
    <table class="table">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Id)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Users)
            </th>
            <th></th>
        </tr>
        @if (Model.Count() == 0)
        {
            <tr><td colspan="4" class="text-center">No Roles</td></tr>
        }
        else
        {
            foreach (var item in Model)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => item.Id)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Name)
                    </td>
                    <td>
                        @if (item.Users.Count == 0 || item.Users == null)
                        {
                            <P>該權限還未新增使用者</P>
                        }
                        else
                        {
                            <P>@string.Join(",", item.Users.Select(x => Html.GetUserName(x.UserId)))</P>
                        }
                    </td>
                    <td>
                        @using (Html.BeginForm("Delete", "RoleAdmin", new { id = item.Id }))
                        {
                            @Html.ActionLink("Edit", "Edit", new { id = item.Id }, new { @class = "btn btn-primary btn-xs" })
                            <input type="submit" value="Delete" class="btn btn-danger btn-xs" />
                        }
                    </td>
                </tr>
            }
        }
    </table>
    
    <p>
        @Html.ActionLink("Create New", "Create", null, new { @class = "btn btn-primary" })
    </p>
    這裡稍做說明,這裡的資料來源是來自於 權限管理器類 的資料,在RoleAdminController的Index方法中可以看到取得所有權限的資料。資料中有個屬性Users這個物件裡面只有兩個欄位一個是使用者ID另一個是權限ID,但我們想要顯示的是使用者名稱,上一點所提供的HtmlHelp的擴充方法可以達到此目的,透過使用者ID取得使用者姓名

    這裡剛好有一個小技巧就是在於同時使用Post&&GET這個傳遞方式,可以看到using並沒有框住整個TABLE代表著此次 Post回去的並不會將整個TABLE全部傳遞回去,只傳遞必要資訊。而可以看到這個using中還包著一個Html.ActionLink(...)這個HTML標籤,此法是使用GET的方式請求Server。這個技巧希望能達到減少傳輸量的目的。
  8. 建立 RoleAdminController 的 Create 方法對應的頁面,請於 \Views\RoleAdmin\ 建立 Create.cshtml :
    @model MvcIdentityTest2.Models.AppRole
    
    @{
        ViewBag.Title = "Create Role";
    }
    
    <h2>Create Role</h2>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
    
        <div class="form-horizontal">
            <h4>AppRole</h4>
            <hr />
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            <div class="form-group">
                @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
                </div>
            </div>
    
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Create" class="btn btn-default" />
                </div>
            </div>
        </div>
    }
    
    <div>
        @Html.ActionLink("Back to List", "Index")
    </div>
    這裡可以看到 View 使用 DataModel(@model MvcIdentityTest2.Models.AppRole)當作 View 的模型,那在對應的Create方法中參數並不是使用對應的DataModel去接收而是使用 Create([Required]string name)可以看到參數還有綁定一個 EF Model 才會用到的屬性,這個方法參數其實就是簡易型的DataModel。

    View 的 Model 不一定要使用 DataModel 可以改成 => @model string 這樣傳遞 !!
  9. 建立 RoleAdminController 的 Edit 頁面所對應的 ViewModel,修改 UserViewModels.cs :
    這裡只顯示修改程式碼與引入組件 => 
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace MvcIdentityTest2.Models
    {
        ...
        /// <summary>
        /// 該類別是修改 權限管理器資料表 時,方便歸類顯示於頁面上所建立的 ViewModel
        /// </summary>
        public class RoleEditModel
        {
            public AppRole Role { get; set; }
            /// <summary>
            /// 目前在權限中的使用者 的AppUser集合
            /// </summary>
            public IList<AppUser> Members { get; set; }
            /// <summary>
            /// 目前沒有在權限中的使用者 的AppUser集合
            /// </summary>
            public IList<AppUser> NonMnebers { get; set; }
        }
    
        /// <summary>
        /// 這是 真正修改 權限管理器資料表 時,所必要的資訊。
        /// </summary>
        public class RoleModificationModel
        {
            [Required]
            public string RoleName { get; set; }
            /// <summary>
            /// 權限欲新增的使用者
            /// </summary>
            public string[] IdsToAdd { get; set; }
            /// <summary>
            /// 權限欲刪除的使用者
            /// </summary>
            public string[] IdsToDelete { get; set; }
        }
    }
    以上的類別與 DataModel 並無任何關聯,這是純粹的 ViewModel 這就是為什麼大多數的人會選擇把 View 所需要用到的模型獨立出來(ViewModel),主要原因有以下 :
    • 降低與EF的耦合性
    • 客製化 MetaData 所需,比如欄位屬性限制、欄位名稱等。使 DataModel 更加單純
    • 不一定所有的 DataModel 都可套用到每個 View 的 model,像這個權限修改的範例因為要能夠達到複選這個機制,且回傳的是另一個 ViewModel 以達到需求,若直接用 DataModel 並無法達到 View 需求。
       
  10. 修改 RoleAdminController 新增修改方法 Edit : 
    這裡只顯示修改程式碼與命名空間 =>
    using System.ComponentModel.DataAnnotations;
    using System.Threading.Tasks;
    using System.Web.Mvc;
    using Microsoft.AspNet.Identity;
    using MvcIdentityTest2.Models;
    
    using System.Collections.Generic;
    using System.Linq;
    
    public class RoleAdminController : MyBaseController
    {
        public ActionResult Index()
        {
            return View(base.BaseRoleManager.Roles);
        }
    
        public ActionResult Create()
        {
            return View();
        }
    
        [HttpPost]
        public async Task<ActionResult> Create([Required]string name) // 這個參數算是小型的 ViewModel
        {
            if (ModelState.IsValid)
            {
                // 新增權限名稱
                IdentityResult result = await base.BaseRoleManager.CreateAsync(new AppRole(name));
                if (result.Succeeded)
                {
                    return RedirectToAction("Index");
                }
                else
                {
                    base.BaseAddErrorsFromResult(result);
                }
            }
            return View(name);
        }
    
        [HttpPost]
        public async Task<ActionResult> Delete(string id)
        {
            // 透過ID找尋對應的權限(腳色)
            AppRole role = await base.BaseRoleManager.FindByIdAsync(id);
            // 若找不到則跳轉到錯誤頁面
            if (role is null) return View("Error", new string[] { "Role Not Found" });
            // 刪除該權限(腳色)
            IdentityResult result = await base.BaseRoleManager.DeleteAsync(role);
            if (result.Succeeded)
            {
                return RedirectToAction("Index");
            }
            else
            {
                return View("Error", result.Errors);
            }
        }
    
        [HttpGet]
        public async Task<ActionResult> Edit(string id)
        {
            // 透過ID找尋對應的Role(權限) 並回傳 DataModel
            AppRole role = await base.BaseRoleManager.FindByIdAsync(id);
            if (role == null) return View("Error", new string[] { "Role Not Found" });
            // 針對目前權限中的使用者,撈取使用者Id並儲存成陣列
            string[] membersId = role.Users.Select(x => x.UserId).ToArray();
    
            // 透過 使用者管理器 取得全部的 使用者 後與 陣列的值比對,儲存成 AppUser 集合
            IList<AppUser> members = new List<AppUser>(); // 目前權限中的使用者,儲存成集合
            IList<AppUser> nonMambers = new List<AppUser>(); // 目前不再權限中的使用者,儲存成集合
            var allUser = base.BaseUserManager.Users.ToList(); // 取得所有使用者並明確轉型成 List
            allUser.ForEach(user => {
                if (membersId.Any(arr => arr == user.Id))
                {
                    members.Add(user);
                }
                else
                {
                    nonMambers.Add(user);
                }
            });
            // -------------------------------------------------------------------------
            return View(new RoleEditModel() { Role = role, Members = members, NonMnebers = nonMambers });
        }
    
        [HttpPost]
        public async Task<ActionResult> Edit(RoleModificationModel inModel)
        {
            if (ModelState.IsValid)
            {
                IdentityResult result;
                // note : inModel.IdsToAdd ?? new string[] { } 代表 若陣列為 null 回傳一組 匿名陣列 
                foreach (string userId in inModel.IdsToAdd ?? new string[] { })
                {
                    // 針對權限新增成員(使用者)
                    result = await base.BaseUserManager.AddToRoleAsync(userId, inModel.RoleName);
                    // 若新增失敗回傳錯誤頁面
                    if (!result.Succeeded) return View("Error", result.Errors);
                }
                foreach (string userId in inModel.IdsToDelete ?? new string[] { })
                {
                    // 針對權限移除成員(使用者)
                    result = await base.BaseUserManager.RemoveFromRoleAsync(userId, inModel.RoleName);
                    // 若新增失敗回傳錯誤頁面
                    if (!result.Succeeded) return View("Error", result.Errors);
                }
                return RedirectToAction("Index");
            }
            return View("Error", new string[] { "Role Not Found" });
        }
    }
    做到這裡不仿先將程式碼稍微理解一下,Edit 的程式碼並不難理解,且是常常會用到的方法。
  11. 建立 Edit 方法所對應的 View,在 \Views\RoleAdmin\ 底下建立 Edit.cshtml :
    odel MvcIdentityTest2.Models.RoleEditModel
    
    @{
        ViewBag.Title = "Edit Role";
    }
    
    <h2>Edit Role</h2>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
        <input name="RoleName" type="hidden" value="@Model.Role.Name" />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <h4>RoleEditModel</h4>
            <hr />
    
            <div class="panel panel-primary">
                <div class="panel-heading">Add To @Model.Role.Name</div>
                @*針對該VIEW稍作說明 : 若Controller 對應的不管是 DataModel 或是 其它參數,對於 網頁前端請求
                    網頁前端可用模型資料打出強行別的資料,但是網頁前端產生的Html只會是元素標籤,那根據傳輸法則那參數名稱要對應
                    到Html標籤的name值,若對應不到則會抱錯。*@
                <table class="table table-striped">
                    @*若該權限擁有全部的使用者*@
                    @if (Model.NonMnebers.Count() == 0)
                    {
                        <tr><td colspan="2">All Users Are Members</td></tr>
                    }
                    else
                    {
                        <tr>
                            <th>User ID</th>
                            <th>Add to Role</th>
                        </tr>
                        foreach (var user in Model.NonMnebers)
                        {
                            <tr>
                                <td>@user.UserName</td>
                                <td><input name="IdsToAdd" type="checkbox" value="@user.Id" /></td>
                            </tr>
                        }
                    }
                </table>
            </div>
            <div class="panel panel-primary">
                <div class="panel-heading">Remove from @Model.Role.Name</div>
                <table class="table table-striped">
                    @if (Model.Members.Count() == 0)
                    {
                        <tr><td colspan="2">No Users Are Members</td></tr>
                    }
                    else
                    {
                        <tr>
                            <th>User ID</th>
                            <th>Remove From Role</th>
                        </tr>
                        foreach (var user in Model.Members)
                        {
                            <tr>
                                <td>@user.UserName</td>
                                <td><input name="IdsToDelete" type="checkbox" value="@user.Id" /></td>
                            </tr>
                        }
                    }
                </table>
            </div>
            <div>
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
    }
    
    <div>
        @Html.ActionLink("Back to List", "Index")
    </div>
    此用兩個 Table 主要為了分別顯示所有使用者有無權限,第二點是創建了<input name="IdsToDelete" type="checkbox" value="@user.Id" /> 回傳到 Controller 後會賦予到相對應參數的物件屬性。

    在應用程序中添加AppRoleManager類會導致Entity Framework刪除數據庫的內容,並重建數據庫,這意味著在之前創建的使用者都會被刪除。因此,為了有使用者可以賦予權限,需啟動應用程序並導航到/Admin/Index 創建一些使用者。

    創建完之後測試權限的刪除與修改,並且修改使用者權限進行測試。
    測試完畢後代表現在可以管理權限並賦予使用者相關的權限了。
     
  12. 回到 AccountController 中新增方法 Logout :
    只顯示修改程式碼 =>
    [Authorize]
    public class AccountController : MyBaseController
    {
        .....
        public ActionResult Logout()
        {
           // 移除當前使用者的 Cookie 驗證,若有輸入參數 則是一次清除相同類型的驗證
           base.BaseAuthManager.SignOut();
           return RedirectToAction("Index", "Home");
        }
    }

     

  13. 修改 HomeController : 主要是為了讓使用者認證(聲明)的資訊顯示出來,更能清楚了解聲明是什麼
    只顯示修改程式碼 =>
    public class HomeController : Controller
    {
        [Authorize]
        public ActionResult Index()
        {
            return View(this.GetData("Index"));
        }
    
        [Authorize(Roles = "Users")] // 只讓聲明中有Users權限的使用者可使用該方法
        public ActionResult OtherAction()
        {
            return View("Index", GetData("OtherAction"));
        }
    
        private Dictionary<string, object> GetData(string actionName)
        {
            Dictionary<string, object> dict = new Dictionary<string, object>() {
                // 取得Action名稱
                {"Action", actionName}, 
                // 取得當前使用者的名稱
                {"User",HttpContext.User.Identity.Name},
                // 取得當前使用者是否以驗證
                {"Authenticated",HttpContext.User.Identity.IsAuthenticated },
                // 取得當前使用者驗證的形式
                {"Authentication Type",HttpContext.User.Identity.AuthenticationType },
                // 判斷當前使用者是否有在 User(權限) 中
                {"In Users Role",HttpContext.User.IsInRole("Users") }
            };
            return dict;
        }
    
        .......
    }

     

  14. 於 Home.cshtml 新增登出的Html標籤 :
    只顯示修改程式碼 =>
    .....
    @Html.ActionLink("Sign Out", "LogOut", "Account", null, new { @class = "btn btn-primary" })

     

  15. 為了不讓使用者重複登入,修改 AccountController 的 Login 方法 :
    只顯示修改程式碼 =>
    [Authorize]
    public class AccountController : MyBaseController
    {
        .....
        [AllowAnonymous] //該方法可不需驗證即可使用
        public ActionResult Login(string returnUrl)
        {
            if (HttpContext.User.Identity.IsAuthenticated)
            {
                return View("Error", new string[] { "已登入會員", "或是無權限造訪該頁面" });
            }
            ViewBag.returnUrl = returnUrl;
            return View();
        }
        ......
    }
    到了這裡可以試著登入與登出,並且了解在 AccountController 中使用 base.BaseAuthManager.SignIn(....) 產生了最基本的使用Cookie的認證(聲明)資訊。

    所以目前已可使用 Identity 新刪修查"使用者"&&"權限",並為使用者加入權限,但是這些功能正常來說是要由網站管理人員來執行。那網站初步執行的時候基本上都要為每個造訪頁面的方法綁上相對應的權限屬性,造訪頁面就需要擁有指定的權限才能訪問,那此時如何建立管理員?? 因為網站無任何開口可以建立,此時應要在初始化資料庫時就順便寫入管理員的資料了。所以下一步就是初始資料庫或更新資料庫結構時如何執行預設動作。

 

H. 設置 Identity 初始資料庫時,還會跑自定義的程式流程(設置管理員)。修改 AppIdentityDbContext類別 :
這裡只顯示修改程式碼與命名空間 =>
using System.Data.Entity;
using Microsoft.AspNet.Identity.EntityFramework;
using MvcIdentityTest2.Models;

using Microsoft.AspNet.Identity;

namespace MvcIdentityTest2.Infrastructure
{
    ......

    /// <summary>
    /// 資料庫初始化時的基本設定
    /// 若 DataModel 結構異動時資料庫的規則為以下設定。
    /// 且 EF 會洗掉資料庫資料,寫入新的資料庫
    /// 繼承自 DropCreateDatabaseIfModelChanges<> 會影響 Identity 更新資料庫結構時的變化
    /// </summary>
    public class IdentityDbInit : DropCreateDatabaseIfModelChanges<AppIdentityDbContext>
    {
        protected override void Seed(AppIdentityDbContext context)
        {
            PerformInitialSetup(context);
            base.Seed(context);
        }

        public void PerformInitialSetup(AppIdentityDbContext context)
        {
            // 在網站初始(第一次建立資料表時)時 需要配給一個Admin管理者 此時可以先外加工
            // 以搭配Controller的驗證機制與規則

            // 新增 使用者管理器類 並使用自定義連線
            AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));
            // 新增 權限管理器類 並使用自定義連線
            AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context));

            // 建立一個權限管理者的相關資訊
            var adminInfo = new {
                roleName = "Administrator",
                userName = "Admin",
                password = "~pWd~",
                email = "AdminAdmin@example.com"
            };

            AppUser user = userMgr.FindByName(adminInfo.userName);
            // 若找不到預設管理員帳號
            if (user == null)
            {
                // 創建網站管理員帳號
                userMgr.Create(new AppUser() { UserName = adminInfo.userName, Email = adminInfo.email }, adminInfo.password);

                user = userMgr.FindByName(adminInfo.userName);
            }

            // 若對應的權限名稱不存在 則創建指定權限
            if (!roleMgr.RoleExists(adminInfo.roleName))
            {
                roleMgr.Create(new AppRole(adminInfo.roleName));
            }

            // 若使用者中無對應的權限 則賦予指定使用者指定的權限
            if (!userMgr.IsInRole(user.Id, adminInfo.roleName))
            {
                userMgr.AddToRole(user.Id, adminInfo.roleName);
            }
        }
    }
}

 

I. 修改各個 Controller 以達到權限控管的目的 :
  1. 修改 AdminController 的權限屬性,只顯示修改程式碼 : =>
    //管理"使用者"的Controller
    [Authorize(Roles = "Administrator")] //每個方法都需是管理者權限才能訪問
    public class AdminController : MyBaseController
    {
       ....
    }

     

  2. 修改 RoleAdminController 的權限屬性,只顯示修改程式碼 : =>
    //控管所有的權限
    [Authorize(Roles = "Administrator")] //每個方法都需是管理者權限才能訪問
    public class RoleAdminController : MyBaseController
    {
       .....
    }
    以上步驟完成,若使用目前的資料數據是無法進入到這些Controller的,因為沒有任何的帳號有該權限。

    此時請刪除資料庫讓 Identity 執行一次資料庫的初始化,這一次初始資料庫裏頭就會有剛剛設置的 網頁管理員的帳戶了。此時一定會納悶該不會以後要重置數據庫不會都要使用這種刪除資料庫的方式吧!? 當然不是在EF中初始化數據庫分成兩種方式 :
    暴力式,只要DataModel資料結構異動即刪除Sql資料庫。初始所有Sql資料數據。
    溫和型,資料結構若異動,保留當前Sql資料數據並更新Sql資料庫結構。
    下一個部分就會來實作這兩種方式。

    跟著步驟做到了這裡應該先看看各個 Identity 類別與 DataModel 類別的關聯性,並觀察資料庫內資料的變化理解 Identity 最後輸入了哪些資料給資料庫。

 

※到了這裡應該要了解 : 

  • 透過了 UserManager(使用者管理器) 驗證了使用者帳密之後,我們使用了 Authentication(認證管理器) 授予使用者一個賦予認證的 Cookie認證(聲明)
  • 我們創建了 RoleManager(權限管理器) && IdentityRole(權限的DataModel),然後透過權限管理器管控(新刪修查)了該網站的權限,這時候再使用 UserManager(使用者管理器)修改了使用者的權限
  • 更了解 View && Controller 的規則。如網頁標籤如何對應到控制器中的參數??
  • 理解了 ViewModle 存在的意義
  • Identity 透過相關的配置達到初始數據庫,我們額外設定了初始數據庫同時要跑自定義的程序流程
  • 在 Controller 中使用了 [Authorize](權限屬性)管控已被賦予認證的使用者
做到這裏不訪先將以上六點先默想一次,腦中應該要浮現這兩大部分的 Identity 類別關聯圖了,如果還是不熟悉以上六個重點的話最好能再重新理解以上的物件與程式碼,不用到真的完全了解因為我們單純只是要使用Identity這個套件如何使用!!

在使用別人套件同時也能稍微理解別人的架構會獲得較好的學習曲線。

 

 


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

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