2013年12月19日 星期四

ASP.NET Identity Example - ToDo

測試環境: VS2013、MV5、Windows7


上次的貼文在ASP.NET MVC5中建置以角色為基礎的授權機制 中,已經只道如何完成以"角色"為基礎的授權機制,這次要再深入些,試試看除了不同角色可以執行不同功能外,在同一功能中,不同使用者只能維護屬於自己的資料。本例中有二種角色,一是Admin(特殊身份者)、另一個是一般使用者。只有Admin 可以使用"管理工作清單"這個功能,在這個功能中可以看到所有人的所有工作任務資料。另一個功能"工作清單",在這功能中每一位使用者只能看到屬於自己的資料(同時可以維護它)。

貼文內容:
  • 建立MVC5新專案
  • 修改相關 Models
    • 擴展Identity Management Model
      • 加入新欄位
      • 建立Helper Class
    • 新增 ToDo Class
    • 擴展Account Management ViewModels (AccountViewModels.cs)
      • 在RegisterViewModel加入新欄位

  • 維護改相關 Controllers
    • 修改AccountController 中 Register Method 加入 Authorize attribute
    • 新增ToDoController.cs

  • 維護相關 Views
    • 修改Register.cshtml View
    • 新增All method的View

  • 在主頁面上新增功能按鈕
  • 啟動 Migration功能
  • 在Seed()方法中加入建立測試資料的程式碼
  • 更新資料庫
  • 執行結果



建立MVC5新專案




修改相關 Models
1. 擴展Identity Management Model (IdentityModels.cs)
  • 加入新欄位:為使用者資料多加二個屬性欄位,分別是HomeTown、ToDoes。



  • 建立Helper Class:利用Asp.Net Identity API建立一個 Identity Management Helper class: IdentityManager class,包含有建立角色、建立使用者...等功能。
IdentityModels.cs 完整程式
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System.Collections.Generic;
using System.Data.Entity;

namespace ToDoProject.Models
{
   // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
   public class ApplicationUser : IdentityUser
   {
       public string HomeTown { get; set; }
       public virtual ICollection<ToDo> ToDoes { get; set; }
   }

   public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
   {
       public ApplicationDbContext()
           : base("DefaultConnection")
       {
       }

       public DbSet<ToDo> ToDoes { get; set; }
   }

   public class IdentityManager
   {
       // 判斷角色是否已在存在
       public bool RoleExists(string name)
       {
           var rm = new RoleManager<IdentityRole>(
               new RoleStore<IdentityRole>(new ApplicationDbContext()));
           return rm.RoleExists(name);
       }
       // 新增角色
       public bool CreateRole(string name)
       {
           var rm = new RoleManager<IdentityRole>(
               new RoleStore<IdentityRole>(new ApplicationDbContext()));
           var idResult = rm.Create(new IdentityRole(name));
           return idResult.Succeeded;
       }
       // 新增角色
       public bool CreateUser(ApplicationUser user, string password)
       {
           var um = new UserManager<ApplicationUser>(
               new UserStore<ApplicationUser>(new ApplicationDbContext()));
           var idResult = um.Create(user, password);
           return idResult.Succeeded;
       }
       // 將使用者加入角色中
       public bool AddUserToRole(string userId, string roleName)
       {
           var um = new UserManager<ApplicationUser>(
               new UserStore<ApplicationUser>(new ApplicationDbContext()));
           var idResult = um.AddToRole(userId, roleName);
           return idResult.Succeeded;
       }
       // 清除使用者的角色設定
       public void ClearUserRoles(string userId)
       {
           var um = new UserManager<ApplicationUser>(
               new UserStore<ApplicationUser>(new ApplicationDbContext()));
           var user = um.FindById(userId);
           var currentRoles = new List<IdentityUserRole>();
           currentRoles.AddRange(user.Roles);
           foreach (var role in currentRoles)
           {
               um.RemoveFromRole(userId, role.Role.Name);
           }
       }
   }
}



  • 新增 ToDo Class





using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ToDoProject.Models
{
   public class ToDo
   {
       public int Id { get; set; }
       public string Description { get; set; }
       public bool IsDone { get; set; }
       public virtual ApplicationUser User { get; set; }
   }
}

2. 擴展Account Management ViewModels (AccountViewModels.cs)
  • 在RegisterViewModel加入新欄位

   public class RegisterViewModel
   {
       [Required]
       [Display(Name = "使用者名稱")]
       public string UserName { get; set; }

       [Required]
       [StringLength(100, ErrorMessage = "{0} 的長度至少必須為 {2} 個字元。", MinimumLength = 6)]
       [DataType(DataType.Password)]
       [Display(Name = "密碼")]
       public string Password { get; set; }

       [DataType(DataType.Password)]
       [Display(Name = "確認密碼")]
       [Compare("Password", ErrorMessage = "密碼和確認密碼不相符。")]
       public string ConfirmPassword { get; set; }

       [Display(Name = "居住城市")]
       public string HomeTown { get; set; }
   }





維護相關 Controllers
  • 修改AccountController中Register Method

       public async Task<ActionResult> Register(RegisterViewModel model)
       {
           if (ModelState.IsValid)
           {
               var user = new ApplicationUser() {
                   UserName = model.UserName,
                   HomeTown = model.HomeTown
               };
               var result = await UserManager.CreateAsync(user, model.Password);
               if (result.Succeeded)
               {
                   await SignInAsync(user, isPersistent: false);
                   return RedirectToAction("Index", "Home");
               }
               else
               {
                   AddErrors(result);
               }
           }

           // 如果執行到這裡,發生某項失敗,則重新顯示表單
           return View(model);
       }



  • 新增ToDoController





ToDoController.cs 內容調整如下:

All method 加入了  [Authorize(Roles="Admin")],只有具Admin角色者可以執行此action。同在 class 上加入[Authorize]表示僅有登入者可以使用class 中的 action。

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using ToDoProject.Models;

namespace ToDoProject.Controllers
{
   [Authorize]
   public class ToDoController : Controller
   {
       private ApplicationDbContext db = new ApplicationDbContext();
       private UserManager<ApplicationUser> manager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));

       // GET: /ToDo/
       // 只回傳屬於自己的資料
       public ActionResult Index()
       {
           var currentUser = manager.FindById(User.Identity.GetUserId());
           return View(db.ToDoes.ToList().Where(t => t.User.Id == currentUser.Id));
           //return View(db.ToDoes.ToList());
       }

       [Authorize(Roles="Admin")]
       // 管理者可以看所有人的資料
       public async Task<ActionResult> All()
       {
           return View(await db.ToDoes.ToListAsync());
       }

       // GET: /ToDo/Details/5
       // 只查看屬於自己的資料
       public async Task<ActionResult> Details(int? id)
       {
           var currentUser = await manager.FindByIdAsync(User.Identity.GetUserId());
           if (id == null)
           {
               return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
           }
           ToDo todo = await db.ToDoes.FindAsync(id);
           if (todo == null)
           {
               return HttpNotFound();
           }
           if (todo.User.Id != currentUser.Id)
           {
               return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
           }
           return View(todo);
       }

       // GET: /ToDo/Create
       public ActionResult Create()
       {
           return View();
       }

       // POST: /ToDo/Create
       // 若要免於過量張貼攻擊,請啟用想要繫結的特定屬性,如需
       // 詳細資訊,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。
       // 在新增的資料中寫入使用者資訊
       [HttpPost]
       [ValidateAntiForgeryToken]
       public async Task<ActionResult> Create([Bind(Include = "Id,Description,IsDone")] ToDo todo)
       {
           var currentUser = await manager.FindByIdAsync(User.Identity.GetUserId());
           if (ModelState.IsValid)
           {
               todo.User = currentUser;
               db.ToDoes.Add(todo);
               await db.SaveChangesAsync();
               return RedirectToAction("Index");
           }

           return View(todo);
       }

       // GET: /ToDo/Edit/5
       // 只編輯屬於自己的資料
       public async Task<ActionResult> Edit(int? id)
       {
           var currentUser = await manager.FindByIdAsync(User.Identity.GetUserId());
           if (id == null)
           {
               return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
           }
           ToDo todo = await db.ToDoes.FindAsync(id);
           if (todo == null)
           {
               return HttpNotFound();
           }
           if (todo.User.Id != currentUser.Id)
           {
               return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
           }
           return View(todo);
       }

       // POST: /ToDo/Edit/5
       // 若要免於過量張貼攻擊,請啟用想要繫結的特定屬性,如需
       // 詳細資訊,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。
       // 只編輯屬於自己的資料
       [HttpPost]
       [ValidateAntiForgeryToken]
       public async Task<ActionResult> Edit([Bind(Include = "Id,Description,IsDone")] ToDo todo)
       {
           if (ModelState.IsValid)
           {
               db.Entry(todo).State = EntityState.Modified;
               await db.SaveChangesAsync();
               return RedirectToAction("Index");
           }
           return View(todo);
       }

       // GET: /ToDo/Delete/5
       public async Task<ActionResult> Delete(int? id)
       {
           var currentUser = await manager.FindByIdAsync(User.Identity.GetUserId());
           if (id == null)
           {
               return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
           }
           ToDo todo = await db.ToDoes.FindAsync(id);
           if (todo == null)
           {
               return HttpNotFound();
           }
           if (todo.User.Id != currentUser.Id)
           {
               return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
           }
           return View(todo);
       }

       // POST: /ToDo/Delete/5
       [HttpPost, ActionName("Delete")]
       [ValidateAntiForgeryToken]
       public async Task<ActionResult> DeleteConfirmed(int id)
       {
           ToDo todo = db.ToDoes.Find(id);
           db.ToDoes.Remove(todo);
           await db.SaveChangesAsync();
           return RedirectToAction("Index");
       }

       protected override void Dispose(bool disposing)
       {
           if (disposing)
           {
               db.Dispose();
           }
           base.Dispose(disposing);
       }
   }
}



維護相關 Views
  • 修改Register.cshtml View

@model ToDoProject.Models.RegisterViewModel
@{
   ViewBag.Title = "註冊";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
   @Html.AntiForgeryToken()
   <h4>建立新的帳戶。</h4>
   <hr />
   @Html.ValidationSummary()
   <div class="form-group">
       @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
       <div class="col-md-10">
           @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
       </div>
   </div>
   <div class="form-group">
       @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
       <div class="col-md-10">
           @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
       </div>
   </div>
   <div class="form-group">
       @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
       <div class="col-md-10">
           @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
       </div>
   </div>
   <div class="form-group">
       @Html.LabelFor(m => m.HomeTown, new { @class = "col-md-2 control-label" })
       <div class="col-md-10">
           @Html.TextBoxFor(m => m.HomeTown, new { @class = "form-control" })
       </div>
   </div>
   <div class="form-group">
       <div class="col-md-offset-2 col-md-10">
           <input type="submit" class="btn btn-default" value="註冊" />
       </div>
   </div>
}

@section Scripts {
   @Scripts.Render("~/bundles/jqueryval")
}

  • 新增ToDoController Views - All (給管理者所使用的功能)




@model IEnumerable<ToDoProject.Models.ToDo>

@{
   ViewBag.Title = "Index";
}

<h2>顯示所有使用者資料</h2>

<table class="table">
   <tr>
       <th>
           @Html.DisplayNameFor(model => model.Description)
       </th>
       <th>
           @Html.DisplayNameFor(model => model.IsDone)
       </th>
       <th>
           @Html.DisplayNameFor(model => model.User.UserName)
       </th>
       <th>
           @Html.DisplayNameFor(model => model.User.HomeTown)
       </th>
       <th></th>
   </tr>

@foreach (var item in Model) {
   <tr>
       <td>
           @Html.DisplayFor(modelItem => item.Description)
       </td>
       <td>
           @Html.DisplayFor(modelItem => item.IsDone)
       </td>
       <td>
           @Html.DisplayFor(modelItem => item.User.UserName)
       </td>
       <td>
           @Html.DisplayFor(modelItem => item.User.HomeTown)
       </td>
   </tr>
}
</table>




在主頁面上新增功能按鈕

多加入了"工作清單"、及"管理工作清單"二個功能。



啟動 Migration功能



在Seed()方法中加入建立測試資料的程式碼


完整程式:
namespace RoleBaseProject.Migrations
{
   using RoleBaseProject.Models;
   using System;
   using System.Data.Entity;
   using System.Data.Entity.Migrations;
   using System.Linq;

   internal sealed class Configuration : DbMigrationsConfiguration<RoleBaseProject.Models.ApplicationDbContext>
   {
       public Configuration()
       {
           AutomaticMigrationsEnabled = false;
       }

       protected override void Seed(RoleBaseProject.Models.ApplicationDbContext context)
       {
           //  This method will be called after migrating to the latest version.

           //  You can use the DbSet<T>.AddOrUpdate() helper extension method
           //  to avoid creating duplicate seed data. E.g.
           //
           //    context.People.AddOrUpdate(
           //      p => p.FullName,
           //      new Person { FullName = "Andrew Peters" },
           //      new Person { FullName = "Brice Lambson" },
           //      new Person { FullName = "Rowan Miller" }
           //    );
           //
           this.AddUserAndRoles();
       }

       bool AddUserAndRoles()
       {
           bool success = false;

           var idManager = new IdentityManager();
           success = idManager.CreateRole("Admin");
           if (!success == true) return success;

           success = idManager.CreateRole("CanEdit");
           if (!success == true) return success;

           success = idManager.CreateRole("User");
           if (!success) return success;


           var newUser = new ApplicationUser()
           {
               UserName = "jatten",
               FirstName = "John",
               LastName = "Atten",
               Email = "jatten@typecastexception.com"
           };

           success = idManager.CreateUser(newUser, "Password1");
           if (!success) return success;

           success = idManager.AddUserToRole(newUser.Id, "Admin");
           if (!success) return success;

           success = idManager.AddUserToRole(newUser.Id, "CanEdit");
           if (!success) return success;

           success = idManager.AddUserToRole(newUser.Id, "User");
           if (!success) return success;

           return success;
       }
   }
}















更新資料庫




執行完 update-database指令後系統會自動建立好資料庫,除了ASP.NET Identity 所使用的相關Table 外,由我們自行定義的 ToDoes Table 也建立成功了




執行結果





以具有Admin角色的使用者登入後執行帳戶管理功能:

可以看到所有人的待辦任務



使用一般使用者登入時只能維護自己的資料

沒有留言:

張貼留言