駕一葉之扁舟 舉匏樽以相屬
寄蜉蝣于天地,渺滄海之一粟。哀吾生之須臾,羨長江之無窮。
挾飛仙以遨游,抱明月而長終。知不可乎驟得,托遺響于悲風。

從壹開始 [ Ids4實戰 ] 之六 ║ 統一角色管理(上)

前言

書接上文,咱們在上周,通過一篇《思考》 性質的文章,和很多小伙伴簡單的討論了下,如何統一同步處理角色的問題,眾說紛紜,這個我一會兒會在下文詳細說到,而且我最終也定稿方案了。所以今天咱們就大刀闊斧的開始遷移之路,這個 IdentityServer4 項目也是要盡快的完結,因為第六個系列《設計模式》已經開始了,然后還有直播,和錄制視頻,積壓太多會得不償失,而且好像還有人讓我講我的項目,所以,這兩周先把我在線的項目遷移了,WPF的項目就留在錄制 IdentityServer4 視頻里給大家詳細講解,文字教程到時候看看要不要補充一下。

那既然說到了角色管理,可能有一部分讀過我文章的小伙伴,腦海中稍微有點兒類似的印象,數據管理?好像之前說過,沒錯!在上上一篇文章中,我們說到了《用戶數據管理》,主要就是用戶數據的增刪改查,然后添加種子數據,從我的 Github 上自動生成,除了用戶,當時也生成了一點 Role 信息,只不過那里的 Role 信息,是固定的,不能修改,而且也僅僅是作為User 的 Claim 聲明來做處理的,并沒有涉及到真正的 Role 管理,比如基本的CURD ,但是今天我們就正式的開始對角色信息進行統一處理了 ,廢話不多說,直接開始。

 

在寫這篇文章的時候,是半夜,越寫越多,最后發現不得已,無奈的在文章標題里加了個()字,其實文章太長也不好,也不知道我為啥這么話癆??。

 

 

零、今天要實現橙色的部分

 

 

 

一、Role 的數據同步方案之回顧

 

在剛剛的前言中,我們說到了上一篇文章《五 ║ 多項目集成統一認證中心的思考》里,我們討論了幾種同步 Role 的方案,很是精彩,主要是文章下邊的評論很精彩,可能看的多了,一不小心會有一種神仙打架的意味,因此這里我簡單的做下總結吧,既是對上篇文章的回顧,也是今天這篇文章的引子,為了看著更清楚,我這里用編號來表示我們的思路:

 

01、我們的 IdentityServer4 項目是一個去中心化的認證服務中心,他提供一個 token 令牌,來實現我們對資源服務器的授權處理;

02、既然要授權,我們就需要對 Token 做一定的處理,這里一般是增加聲明 Claim,常見的就是 Role 信息;

03、然后我們從服務中心成功登錄后返回,并攜帶一個含有 Role Claim信息的 Token 令牌;

04、接著在返回到的資源服務器里,對 api 進行自定義授權,來對當前 Token 令牌,也等同于 Token 的持有者進行訪問限制;

05、那這個時候問題來了,我們的資源服務器看起來,本應該是不用關心我們的用戶信息和角色信息的,是要交給認證中心的;

06、但是 Blog.Core 項目,我們用到了數據庫的動態分配授權,是根據 Role 來分配特定的 api/url 的;

07、也就意味著我們要把 Role 放到資源服務器,但是上邊第 05 點,我們明確 Role 的管理是在 Identity 項目的;

08、這個時候方案就來了,

09、一:我們可以做一個定時器,定時將 Identity 認證項目的Role同步到資源服務器;

10、二:在 Identity 項目開發一個 api 接口,方便我們在 資源服務器 里調用;

11、三:直接把 Identity 和 core 項目共用一個 db 數據庫,使用一個 Role 表,就完美解決這個問題了;

12、四:單獨抽離出一個 Role 做分布式服務管理中心,可以使用 Redis,就是把 Role 單獨一個微服務,讓所有項目使用;

13、:最簡單的方法,Identity 項目單獨一個 db 庫,但是我們的資源服務器手動在 controller 上配置Authorize;

14、等等等等,還有其他的一些思路,不列舉。

 

從上邊的這一系列大家可以看得出來,我們平時開發還是需要很多的思考的,也是需要多多的討論,這樣才能進步。

最終思考了很久,我還是采用了方案三和方案五,這兩個簡單的方案,你可能好奇,為啥是兩個呢?而且感覺兩個背道而馳,一個是合并,一個是分庫,怎么能同時使用呢,其實很簡單的,因為我有多個資源服務器,這里目前就用兩個吧 —— Blog.Core 的前后端分離的 api 項目 和 ChristDDD 的 MVC 項目,當然以后還會有 WPF 項目。

我在 Blog.Core 項目采用方案三,合并到一個數據庫,可以很好的解決動態授權問題,

然后在 MVC 項目里,就采用手動在 controller 添加特性的形式吧,也就是方案五,這樣就完全滿足了需求,

也能夠同時給大家展示兩種方案,從而達到學習的目的。

 

好啦,到了現在,大家應該也明白了我以后的設計思路和開發方案了,現在就開始動手處理 Role 數據了。

 

 

 

二、兩種管理 ROLE 的方案

 

說明:以下內容可能有點兒繞,或者有點兒不容易懂,大家不要慌,我會這兩篇詳細講解,而且也會在視頻中,詳細給大家說明的,但是還是盡量能跟的上。

如果你使用 Ids4 項目的話(這里準確來講,是開發 Identity 的話,因為兩者是不一樣的喲),會有兩種開發方式.

 

1、簡述 Ids4 數據庫框架三模塊

在我們的 Ids4 項目中,我們在之前的文章中也說到了,一共有三個模塊,對應了三個上下文,分別是配置數據ConfigurationDbContext、操作數據PersistedGrantDbContext,然后最后才是我們應用數據(主要是用戶角色數據)ApplicationDbContext ,前兩個是 IdentityServer4 的相關類庫,第三個其實不是 Ids4 官方的,而且 NetCore 自帶的一個類庫,只是幫助我們更好的處理用戶數據的。

我們使用前兩個上下文來實現 Ids4 的去中心化認證,而第三個 ApplicationDbContext  只是來存儲我們的用戶和角色數據的。

因此!這個時候我們知道了,其實我們無論怎么處理用戶和角色數據,是不會影響 Ids4 整體性操作的,這個時候我們就恍然大悟了,這個時候兩種方案就出來了:

 

2、自定義封裝,實現用戶角色數據的持久化

這個第一種就是自己封裝一個 Repository 倉儲的方式,然后搭配 EFCore 持久化,還可以寫多個上下文等等。這種就是我們自定義的開發,這種好處很明顯,就是可以很好的進行擴展和自定義處理,而且匹配多個上下文,還可以支持事務等等,如果自己能力較高,或者說,身邊正好有這么一個項目案例,可以對比著學習學習,搭建搭建,今天我就不詳細的說這個了,下次給大家詳細說明,大家這個時候應該懂了,我們開發 Ids4 的思路,無非就是一個持久化的過程,之所以使用 Ids4 這個框架,僅僅是使用了 Ids4 封裝了很豐富的、去中心化的 Token 生成機制而已。

我這里簡單舉個例子,可以這么配置,看個思路就行了,代碼是不完整的,我以后會詳細說明,這里僅僅是展示一下:

// 配置上下文
public class MyDbContext : DbContext, IConfigurationDbContext
 {
     private readonly ConfigurationStoreOptions storeOptions;

     public MyDbContext(DbContextOptions<MyDbContext> options)
      : base(options)
     {
     }public DbSet<User> User { get; set; }
     public DbSet<Role> Roles { get; set; }
     public DbSet<ApiResource> ApiResources { get; set; }

     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         modelBuilder.ConfigureClientContext(storeOptions);
         modelBuilder.ConfigureResourcesContext(storeOptions);

         base.OnModelCreating(modelBuilder);
     }
 }


// 正常的注入服務
services.AddDbContext<MyDbContext>(builder => builder.UseOracle(connectionString, options => {
     options.MigrationsAssembly(migrationsAssembly);
     options.MigrationsHistoryTable("xxxx");
 }));

 

上邊的實體類可以自定義處理,然后我們再寫一個倉儲,或者是一個類,來處理數據:

    public class UserRepository
    {
        private readonly MyDbContext _dbContext;
        public UserRepository(MyDbContext dbContext)
        {
            _dbContext = dbContext;
        }
        /// <summary>
        /// 根據SubjectID查詢用戶信息
        /// </summary>
        /// <param name="subjectId">用戶id</param>
        /// <returns></returns>
        public IdentityUser FindBySubjectId(string subjectId) {
            return _dbContext.Set<User>().Where(r => r.SubjectId.Equals(subjectId)).Include(r => r.IdentityUserClaims).SingleOrDefault();
        }
        /// <summary>
        /// 根據用戶名查詢用戶
        /// </summary>
        /// <param name="username">用戶</param>
        /// <returns></returns>
        public IdentityUser FindByUsername(string username)
        {
            return _dbContext.Set<User>().Where(r => r.Username.Equals(username)).Include(r => r.IdentityUserClaims).SingleOrDefault();
        }
}

 

但是如果自己能力不是很高,或者說不想太麻煩的話,可以使用 IdentityServer4 中 Identity 自帶的,封裝好的一套邏輯來處理,就比如我之前來處理用戶數據的時候,用的就是 UserManager 類,我們這時候就使用一個 RoleManager.cs 類。

 

3、使用NetCore自帶 Identity 庫

 

這個其實是很簡單的,我們看一下 UserManager 類的命名空間就知道了,這個是微軟原生自帶的類庫,和 Ids4 其實沒有太大的關系:

 

 

這個類庫的名字和 Ids4 也很像,就是叫做 Identity ,一共七個表,來處理用戶和角色的關系:

 

 

很簡單,很方便,也很豐富,那今天我們就先說說這個第二種方案,第一種方案,我們下次再說。

 

 

 

三、利用 Identity 原生結構,處理角色信息

 

1、自定義 Role 擴展實體類

我們既然要對 Role 進行管理,那我們就需要做下封裝,Ids4 默認自帶的 IdentityRole 表,僅僅只要三個屬性:

public virtual TKey Id {get;set}
public virtual string Name{get;set}
public virtual string NormalizedName{get;set}

 

這是肯定不夠用的,不僅不夠用,我們還需要和資源服務器 Blog.Core 項目打通,所以兩個實體類要取并集,就是求最全的屬性,那我就自定義了一個應用角色表,用來滿足和 Blog.Core 項目的統一:

在項目的 Models 文件夾下,新建 ApplicationRole.cs 類:

// Add profile data for application roles by adding properties to the ApplicationRole class
public class ApplicationRole : IdentityRole<int>
{

    public bool IsDeleted { get; set; }
    public string Description { get; set; }
    /// <summary>
    ///排序
    /// </summary>
    public int OrderSort { get; set; }
    /// <summary>
    /// 是否激活
    /// </summary>
    public bool Enabled { get; set; }
    /// <summary>
    /// 創建ID
    /// </summary>
    public int? CreateId { get; set; }
    /// <summary>
    /// 創建者
    /// </summary>
    public string CreateBy { get; set; }
    /// <summary>
    /// 創建時間
    /// </summary>
    public DateTime? CreateTime { get; set; } = DateTime.Now;
    /// <summary>
    /// 修改ID
    /// </summary>
    public int? ModifyId { get; set; }
    /// <summary>
    /// 修改者
    /// </summary>
    public string ModifyBy { get; set; }
    /// <summary>
    /// 修改時間
    /// </summary>
    public DateTime? ModifyTime { get; set; } = DateTime.Now;

// 同理我們需要創建一個 ApplicationUserRole 關系表,具體的看我源碼吧
public ICollection<ApplicationUserRole> UserRoles { get; set; }

 

這里可以做任何的自定義,只不過這里有一個小的問題,那就是這個 Id 的問題,我們的 Blog.Core 項目使用的是 Int 整型自增,那 IdentityServer4 用的是 string 方式,所以說,這里要做下處理,一般有兩種辦法,一種是把 IdentityServer4 項目的string 全部切換成 int,然后還有一種,就是修改 Blog.Core 資源服務器的主鍵 Id 為 Guid string .

其實這兩種都可以,而且一般人都是采用的 Guid 和 string 的形式,但是很不巧的是,我的 Blog.Core 項目使用的是 Int 類型,所以,這里我就統一修改成 int,大家根據需要自己處理吧,具體如何處理 int 呢,大家多注意下文的類型就行,我會點明注意的點。

 

2、修改注入的 Identity 服務

我們需要把我們的 ApplicationRole 信息也注入到服務里去,這里不多說:

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

 

3、修改應用上下文

因為我們自定義了 ApplicationRole ,所以在數據庫上下文中,也需要對 Role 信息單獨做處理,而且還比較麻煩,這個具體的,可以通過 F12 查看源碼就能了解到相應的邏輯,咱們就直接這么修改:

 
// 注意下 紅色的 int類型,到時候創建的表的主鍵是 int 類型的。
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int, IdentityUserClaim<int>, ApplicationUserRole, IdentityUserLogin<int>, IdentityRoleClaim<int>, IdentityUserToken<int>> { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<ApplicationUserRole>(userRole => { userRole.HasKey(ur => new { ur.UserId, ur.RoleId }); userRole.HasOne(ur => ur.Role) .WithMany(r => r.UserRoles) .HasForeignKey(ur => ur.RoleId) .IsRequired(); userRole.HasOne(ur => ur.User) .WithMany(r => r.UserRoles) .HasForeignKey(ur => ur.UserId) .IsRequired(); }); builder.Entity<ApplicationRole>() .ToTable("Role"); }

 

 

4、數據庫遷移,生成 DB

打開我們的控制臺:工具 -》Nuget 包管理器 -》程序包管理器控制臺,

1、add-migration UpdateRole -Context ApplicationDbContext

2、update-database -Context ApplicationDbContext 

 

這里來一個動圖

 

 

然后我們可以看看生成的數據庫表結構,可以看到,和之前的表結構,幾乎是一樣的,可以看到我們右側的 Identity 生成的表結構,不僅主鍵變成了一樣的 Int 類型,相關的屬性字段也都有,如果你有強迫癥的話,也可以把字段的長度設為一致,還記得在哪里修改把,就是上下文里,這里不多說:

 

  

這里有一個要注意一下,如果我們什么都不操作,默認生成的數據庫表名是 AspNetRoles ,我們也可以自定義修改成自己的表名,直接修改實體類名是不行的,因為我們可以看一下生成的遷移記錄,無論修改成什么,只要我們的擴展實體類是繼承了類IdentityRole,那表名還是默認的 AspNetRoles:

 

 

那我們可以通過配置EFCore 的實體映射來做相應的處理,還記得我們剛剛的上下文么,就是這里:

 

 

然后我們做一下數據庫遷移,最后我們可以看到數據庫表名已經變了,具體的可以查看上邊的遷移對比圖。

完成!是不是這么寫已經完成了呢,不是的,現在只是完成了一半,剩下的一半,就是在控制器里,去進行業務邏輯設計了。 

 

5、設計角色的 CURD 頁面與業務邏輯

先構造函數注入下我們的 RoleManager 服務,這是 IdentityServer4 已經給我們封裝好的類:

 

 

然后設計接口,主要就是增刪改查,很簡單,當然,你也可以像用戶管理那樣,帶上權限信息:

 [HttpGet]
 [Route("account/Roleregister")]
 public IActionResult RoleRegister(string returnUrl = null)
 {
     ViewData["ReturnUrl"] = returnUrl;
     return View();
 }

 [HttpPost]
 [Route("account/Roleregister")]
 [ValidateAntiForgeryToken]
 public async Task<IActionResult> RoleRegister(RoleRegisterViewModel model, string returnUrl = null)
 {
     ViewData["ReturnUrl"] = returnUrl;
     IdentityResult result = new IdentityResult();

     if (ModelState.IsValid)
     {
         var roleItem = _roleManager.FindByNameAsync(model.RoleName).Result;

         if (roleItem == null)
         {
             var role = new ApplicationRole
             {
                 Name = model.RoleName
             };

             result = await _roleManager.CreateAsync(role);

             if (result.Succeeded)
             {
                 if (result.Succeeded)
                 {
                     return RedirectToLocal(returnUrl);
                 }
             }

         }
         else
         {
             ModelState.AddModelError(string.Empty, $"{roleItem?.Name} already exists");

         }

         AddErrors(result);
     }

     // If we got this far, something failed, redisplay form
     return View(model);
 }


 [HttpGet]
 [Route("account/Roles")]
 [Authorize]
 public IActionResult Roles(string returnUrl = null)
 {
     ViewData["ReturnUrl"] = returnUrl;
     var roles = _roleManager.Roles.Where(d => !d.IsDeleted).ToList();
     return View(roles);
 }



 [HttpGet("{id}")]
 [Route("account/Roleedit/{id}")]
 [Authorize(Roles = "SuperAdmin")]
 public async Task<IActionResult> RoleEdit(string id, string returnUrl = null)
 {
     ViewData["ReturnUrl"] = returnUrl;
     if (id == null)
     {
         return NotFound();
     }

     var user = await _roleManager.FindByIdAsync(id);

     if (user == null)
     {
         return NotFound();
     }

     return View(new RoleEditViewModel(user.Id, user.Name));
 }


 [HttpPost]
 [Route("account/Roleedit/{id}")]
 [ValidateAntiForgeryToken]
 [Authorize(Roles = "SuperAdmin")]
 public async Task<IActionResult> RoleEdit(RoleEditViewModel model, string id, string returnUrl = null)
 {
     ViewData["ReturnUrl"] = returnUrl;
     IdentityResult result = new IdentityResult();

     if (ModelState.IsValid)
     {
         var roleItem = _roleManager.FindByIdAsync(model.Id).Result;

         if (roleItem != null)
         {
             roleItem.Name = model.RoleName;

             result = await _roleManager.UpdateAsync(roleItem);

             if (result.Succeeded)
             {
                 return RedirectToLocal(returnUrl);
             }

         }
         else
         {
             ModelState.AddModelError(string.Empty, $"{roleItem?.Name} no exist!");
         }

         AddErrors(result);
     }

     // If we got this far, something failed, redisplay form
     return View(model);
 }

 [HttpPost]
 [Route("account/Roledelete/{id}")]
 [Authorize(Roles = "SuperAdmin")]
 public async Task<JsonResult> RoleDelete(string id)
 {
     IdentityResult result = new IdentityResult();

     if (ModelState.IsValid)
     {
         var roleItem = _roleManager.FindByIdAsync(id).Result;

         if (roleItem != null)
         {
             roleItem.IsDeleted = true;

             result = await _roleManager.UpdateAsync(roleItem);

             if (result.Succeeded)
             {
                 return Json(result);
             }
         }
         else
         {
             ModelState.AddModelError(string.Empty, $"{roleItem?.Name} no exist!");
         }

         AddErrors(result);
     }

     return Json(result.Errors);

 }

 

那剩下的就是我們修改用戶的角色信息了,畢竟我們要給用戶進行加權限,也就是賦角色操作的嘛。但是??????

時間很晚了,已經快凌晨了,篇幅太長了,今天就暫時先到這里了,總結來說,今天主要是把角色的相關操作給完整的走了一遍,還是很不錯的,很有收獲的,以后更精彩,下次再見。

 

 

四、Github && Gitee

 https://github.com/anjoy8/Blog.IdentityServer

 

優秀文章參考:

https://stackoverflow.com/questions/51004516/net-core-2-1-identity-get-all-users-with-their-associated-roles

 

posted @ 2019-11-27 10:01  老張的哲學  閱讀(...)  評論(...編輯  收藏
作者:老張的哲學
好好學習,天天向上
返回頂部小火箭
好友榜:
如果愿意,把你的博客地址放這里
jianshu.com/u/老張
SqlSugar codeisbug.com
鈺璽 studenty.cn
七乐彩2011年走势图南方双彩