ABP vNext微服務架構詳細教程——分布式權限框架


1.簡介

ABP vNext框架本身提供了一套權限框架,其功能非常豐富,具體可參考官方文檔:https://docs.abp.io/en/abp/latest/Authorization

但是我們使用時會發現,對於正常的單體應用,ABP vNext框架提供的權限系統沒有問題, 但是在微服務架構下,這種權限系統並不是非常的友好。

我希望我的權限系統可以滿足以下要求:

  1. 每個聚合服務持有獨立的權限集合
  2. 每個聚合服務可以獨立聲明、使用其接口訪問所需的權限。
  3. 提供統一接口負責管理、存儲所有服務權限並實現對角色的授權。
  4. 每個接口可以靈活組合使用一個或多個權限碼。
  5. 權限框架使用盡量簡單,減少額外編碼量。

在ABP vNext框架基礎上,重新編寫了一套分布式權限框架,大體規則如下:

  • 使用ABP vNext框架中提供的用戶、角色模型不做改變,替代重新定義權限模型,重新定義權限的實體及相關服務接口。
  • 在身份管理服務中,實現權限的統一管理、角色授權和權限認證。
  • 在聚合服務中定義其具有的權限信息、權限關系並通過特性聲明各接口所需要的權限。
  • 在聚合服務啟動時,自動將其權限信息注冊到身份管理服務。
  • 客戶端訪問聚合服務層服務時在聚合服務層中間件中驗證當前用戶是否具有該接口權限,驗證過程需調用身份管理服務對應接口。 

權限系統具體實現見下文。

2. 身份認證服務

在之前的文章中我們已經搭建了身份認證服務的基礎框架,這里我們直接在此基礎上新增代碼。

在Demo.Identity.Domain項目中添加Permissions文件夾,並添加Entities子文件夾。在此文件夾下添加實體類SysPermission和RolePermissions如下:

using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Domain.Entities;

namespace Demo.Identity.Permissions.Entities;

/// <summary>
/// 權限實體類
/// </summary>
public class SysPermission : Entity<Guid>
{
    /// <summary>
    /// 服務名稱
    /// </summary>
    [MaxLength(64)]
    public string ServiceName { get; set; }

    /// <summary>
    /// 權限編碼
    /// </summary>
    [MaxLength(128)]
    public string Code { get; set; }

    /// <summary>
    /// 權限名稱
    /// </summary>
    [MaxLength(64)]
    public string Name { get; set; }

    /// <summary>
    /// 上級權限ID
    /// </summary>
    [MaxLength(128)]
    public string ParentCode { get; set; }


    /// <summary>
    /// 判斷兩個權限是否相同
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public override bool Equals(object? obj)
    {
        return obj is SysPermission permission
               && permission.ServiceName == ServiceName
               && permission.Name == Name
               && permission.Code == Code
               && permission.ParentCode == ParentCode;
    }

    /// <summary>
    /// 設置ID的值
    /// </summary>
    /// <param name="id"></param>
    public void SetId(Guid id)
    {
        Id = id;
    }
}
using System;
using Volo.Abp.Domain.Entities;

namespace Demo.Identity.Permissions.Entities;

/// <summary>
/// 角色權限對應關系
/// </summary>
public class RolePermissions : Entity<Guid>
{
    /// <summary>
    /// 角色編號
    /// </summary>
    public Guid RoleId { get; set; }

    /// <summary>
    /// 權限編號
    /// </summary>
    public Guid PermissionId { get; set; }
}

將Demo.Identity.Application.Contracts項目中原有Permissions文件夾中所有類刪除,並添加子文件夾Dto。在此文件夾下添加SysPermissionDto、PermissionTreeDto、SetRolePermissionsDto

類如下:

using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Application.Dtos;

namespace Demo.Identity.Permissions.Dto;

/// <summary>
/// 權限DTO
/// </summary>
public class SysPermissionDto:EntityDto<Guid>
{
    /// <summary>
    /// 服務名稱
    /// </summary>
    [MaxLength(64)]
    public string ServiceName { get; set; }
        
    /// <summary>
    /// 權限編碼
    /// </summary>
    [MaxLength(128)]
    public string Code { get; set; }

    /// <summary>
    /// 權限名稱
    /// </summary>
    [MaxLength(64)]
    public string Name { get; set; }

    /// <summary>
    ///     上級權限ID
    /// </summary>
    [MaxLength(128)]
    public string ParentCode { get; set; }
}
using System;
using System.Collections.Generic;
using Volo.Abp.Application.Dtos;

namespace Demo.Identity.Permissions.Dto;

/// <summary>
/// 權限樹DTO
/// </summary>
public class PermissionTreeDto : EntityDto<Guid>
{
    /// <summary>
    /// 服務名稱
    /// </summary>
    public string ServiceName { get; set; }

    /// <summary>
    /// 權限編碼
    /// </summary>
    public string Code { get; set; }

    /// <summary>
    /// 權限名稱
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 上級權限ID
    /// </summary>
    public string ParentCode { get; set; }

    /// <summary>
    /// 子權限
    /// </summary>
    public List<PermissionTreeDto> Children { get; set; }
        
}
using System;
using System.Collections.Generic;

namespace Demo.Identity.Permissions.Dto;

/// <summary>
/// 設置角色權限DTO
/// </summary>
public class SetRolePermissionsDto
{
    /// <summary>
    /// 角色編號
    /// </summary>
    public Guid RoleId { get; set; }

    /// <summary>
    /// 權限ID列表
    /// </summary>
    public List<Guid> Permissions { get; set; }
}

將Demo.Identity.Application.Contracts項目中Permissions文件夾下添加接口IRolePermissionsAppService如下:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Demo.Identity.Permissions.Dto;
using Volo.Abp.Application.Services;

namespace Demo.Identity.Permissions;

/// <summary>
///     角色管理應用服務接口
/// </summary>
public interface IRolePermissionsAppService
    : IApplicationService
{

    /// <summary>
    /// 獲取角色所有權限
    /// </summary>
    /// <param name="roleId">角色ID</param>
    /// <returns></returns>
    Task<List<PermissionTreeDto>> GetPermission(Guid roleId);


    /// <summary>
    /// 設置角色權限
    /// </summary>
    /// <param name="dto">角色權限信息</param>
    /// <returns></returns>
    Task SetPermission(SetRolePermissionsDto dto);
}

將Demo.Identity.Application.Contracts項目中Permissions文件夾下添加接口ISysPermissionAppService如下:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Demo.Identity.Permissions.Dto;
using Volo.Abp.Application.Services;

namespace Demo.Identity.Permissions;

/// <summary>
/// 權限管理應用服務接口 
/// </summary>
public interface ISysPermissionAppService:IApplicationService
{
    /// <summary>
    /// 按服務注冊權限
    /// </summary>
    /// <param name="serviceName">服務名稱</param>
    /// <param name="permissions">權限列表</param>
    /// <returns></returns>
    Task<bool> RegistPermission(string serviceName, List<SysPermissionDto> permissions);

    /// <summary>
    /// 按服務獲取權限
    /// </summary>
    /// <param name="serviceName">服務名稱</param>
    /// <returns>查詢結果</returns>
    Task<List<SysPermissionDto>> GetPermissions(string serviceName);

    /// <summary>
    /// 獲取完整權限樹
    /// </summary>
    /// <param name="Permission"></param>
    /// <returns>查詢結果</returns>
    Task<List<PermissionTreeDto>> GetPermissionTree();

    /// <summary>
    /// 獲取用戶權限碼
    /// </summary>
    /// <param name="userId">用戶編號</param>
    /// <returns>查詢結果</returns>
    Task<List<string>> GetUserPermissionCode(Guid userId);
}

在公共類庫文件夾common中創建.Net6類庫項目項目Demo.Core,用於存放通用類。

這里我們在Demo.Core中添加文件夾CommonExtension用於存放通用擴展,添加EnumExtensions和ListExtensions類如下:

namespace Demo.Core.CommonExtension;

/// <summary>
/// 枚舉擴展類
/// </summary>
public static class EnumExtensions
{
    /// <summary>
    /// 獲取描述特性
    /// </summary>
    /// <param name="enumValue">枚舉值</param>
    /// <returns></returns>
    public static string GetDescription(this Enum enumValue)
    {
        string value = enumValue.ToString();
        FieldInfo field = enumValue.GetType().GetField(value);
        object[] objs = field.GetCustomAttributes(typeof(DescriptionAttribute), false);  //獲取描述屬性
        if (objs == null || objs.Length == 0)  //當描述屬性沒有時,直接返回名稱
            return value;
        DescriptionAttribute descriptionAttribute = (DescriptionAttribute)objs[0];
        return descriptionAttribute.Description;
    }
}
namespace Demo.Core.CommonExtension;

public static class ListExtensions
{
    /// <summary>
    /// 集合去重
    /// </summary>
    /// <param name="lst">目標集合</param>
    /// <param name="keySelector">去重關鍵字</param>
    /// <typeparam name="T">集合元素類型</typeparam>
    /// <typeparam name="TKey">去重關鍵字數據類型</typeparam>
    /// <returns>去重結果</returns>
    public static List<T> Distinct<T,TKey>(this List<T> lst,Func<T, TKey> keySelector)
    {
        List<T> result = new List<T>();
        HashSet<TKey> set = new HashSet<TKey>();
        foreach (var item in lst)
        {
            var key = keySelector(item);
            if (!set.Contains(key))
            {
                set.Add(key);
                result.Add(item);
            }
        }
        return result;
    }
}

在Demo.Core項目中添加文件夾CommonFunction用於存放通用方法,這里我們添加用於集合比對的ListCompare類如下:

using VI.Core.CommonExtension;

namespace VI.Core.CommonFunction;

/// <summary>
/// 集合比對
/// </summary>
public class ListCompare
{
    /*
     * 調用實例:
     *  MutiCompare<Permission, string>(lst1, lst2, x => x.Code, (obj, isnew) =>
     *  {
     *      if (isnew)
     *      {
     *          Console.WriteLine($"新增項{obj.Id}");
     *      }
     *      else
     *      {
     *          Console.WriteLine($"已存在{obj.Id}");
     *      }
     *  }, out var lstNeedRemove);
     */
    /// <summary>
    /// 對比源集合和目標集合,處理已有項和新增項,並找出需要刪除的項
    /// </summary>
    /// <param name="lstSource">源集合</param>
    /// <param name="lstDestination">目標集合</param>
    /// <param name="keySelector">集合比對關鍵字</param>
    /// <param name="action">新增或已有項處理方法,參數:(數據項, 是否是新增)</param>
    /// <param name="needRemove">需要刪除的數據集</param>
    /// <typeparam name="TObject">集合對象數據類型</typeparam>
    /// <typeparam name="TKey">對比關鍵字數據類型</typeparam>
    public static void MutiCompare<TObject,TKey>(List<TObject> lstDestination,List<TObject> lstSource,
        Func<TObject, TKey> keySelector,
        Action<TObject, bool> action, 
        out Dictionary<TKey, TObject> needRemove)
    {
        //目標集合去重
        lstDestination.Distinct(keySelector);
        //將源集合存入字典,提高查詢效率
        needRemove = new Dictionary<TKey, TObject>();
        foreach (var item in lstSource)
        {
            needRemove.Add(keySelector(item),item);
        }
        //遍歷目標集合,區分新增項及已有項
        //在字典中排除目標集合中的項,剩余的即為源集合中需刪除的項
        foreach (var item in lstDestination)
        {
            if (needRemove.ContainsKey(keySelector(item)))
            {
                action(item, false);
                needRemove.Remove(keySelector(item));
            }
            else
            {
                action(item, true);
            }
        }
    }
}

在Demo.Identity.Application項目中添加Permissions文件夾。

在Demo.Identity.Application項目Permissions文件夾中添加PermissionProfileExtensions類用於定義對象映射關系如下:

using Demo.Identity.Permissions.Dto;
using Demo.Identity.Permissions.Entities;

namespace Demo.Identity.Permissions;

public static class PermissionProfileExtensions
{
    /// <summary>
    /// 創建權限領域相關實體映射關系
    /// </summary>
    /// <param name="profile"></param>
    public static void CreatePermissionsMap(this IdentityApplicationAutoMapperProfile profile)
    {
        profile.CreateMap<SysPermission, PermissionTreeDto>();
        profile.CreateMap<SysPermission,SysPermissionDto>();
        profile.CreateMap<SysPermissionDto,SysPermission>();
    }
}

在Demo.Identity.Application項目IdentityApplicationAutoMapperProfile類的IdentityApplicationAutoMapperProfile方法中添加如下代碼:

this.CreatePermissionsMap();

在Demo.Identity.Application項目Permissions文件夾中添加PermissionTreeBuilder類,定義構造權限樹形結構的通用方法如下:

using System.Collections.Generic;
using System.Linq;
using Demo.Identity.Permissions.Dto;

namespace Demo.Identity.Permissions;

/// <summary>
/// 權限建樹幫助類
/// </summary>
public static class PermissionTreeBuilder
{
    /// <summary>
    /// 建立樹形結構
    /// </summary>
    /// <param name="lst"></param>
    /// <returns></returns>
    public static List<PermissionTreeDto> Build(List<PermissionTreeDto> lst)
    {
        var result = lst.ToList();

        for (var i = 0; i < result.Count; i++)
        {
            if (result[i].ParentCode == null)
            {
                continue;
            }
            foreach (var item in lst)
            {
                item.Children ??= new List<PermissionTreeDto>();
                if (item.Code != result[i].ParentCode)
                {
                    continue;
                }

                item.Children.Add(result[i]);
                result.RemoveAt(i);
                i--;
                break;
            }
        }
        return result;
    }
}

之后我們在Demo.Identity.Application項目Permissions文件夾中添加權限管理實現類SysPermissionAppService如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Demo.Core.CommonFunction;
using Demo.Identity.Permissions.Dto;
using Demo.Identity.Permissions.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;
using Demo.Core.CommonExtension;

namespace Demo.Identity.Permissions
{
    /// <summary>
    /// 權限管理應用服務
    /// </summary>
    public class SysPermissionAppService : IdentityAppService, ISysPermissionAppService
    {
        #region 初始化

        private readonly IRepository<RolePermissions> _rolePermissionsRepository;
        private readonly IRepository<SysPermission> _sysPermissionsRepository;
        private readonly IRepository<IdentityUserRole> _userRolesRepository;

        public SysPermissionAppService(
            IRepository<RolePermissions> rolePermissionsRepository,
            IRepository<SysPermission> sysPermissionsRepository,
            IRepository<IdentityUserRole> userRolesRepository
        )
        {
            _rolePermissionsRepository = rolePermissionsRepository;
            _sysPermissionsRepository = sysPermissionsRepository;
            _userRolesRepository = userRolesRepository;
        }

        #endregion
        
        #region 按服務注冊權限

        /// <summary>
        /// 按服務注冊權限
        /// </summary>
        /// <param name="serviceName">服務名稱</param>
        /// <param name="permissions">權限列表</param>
        /// <returns></returns>
        public async Task<bool> RegistPermission(string serviceName, List<SysPermissionDto> permissions)
        {
            //根據服務名稱查詢現有權限
            var entities = await AsyncExecuter.ToListAsync( 
                (await _sysPermissionsRepository.GetQueryableAsync()).Where(c => c.ServiceName == serviceName)
            );
            var lst = ObjectMapper.Map<List<SysPermissionDto>, List<SysPermission>>(permissions);
            ListCompare.MutiCompare(lst, entities, x => x.Code, async (entity, isNew) =>
            {
                if (isNew)
                {
                    //新增
                    await _sysPermissionsRepository.InsertAsync(entity);
                }
                else
                {
                    //修改
                    var tmp = lst.FirstOrDefault(x => x.Code == entity.Code);
                    //調用權限判斷方法,如果code和name相同就不進行添加
                    if (!entity.Equals(tmp)&&tmp!=null)
                    {
                        entity.SetId(tmp.Id);
                        await _sysPermissionsRepository.UpdateAsync(entity);
                    }
                }
            }, out var needRemove);
            foreach (var item in needRemove)
            {
                //刪除多余項
                await _sysPermissionsRepository.DeleteAsync(item.Value);
            }
            return true;
        }

        #endregion

        #region 按服務獲取權限

        /// <summary>
        ///     按服務獲取權限
        /// </summary>
        /// <param name="serviceName">服務名稱</param>
        /// <returns>查詢結果</returns>
        public async Task<List<SysPermissionDto>> GetPermissions(string serviceName)
        {
            var query = (await _sysPermissionsRepository.GetQueryableAsync()).Where(x => x.ServiceName == serviceName);
            //使用AsyncExecuter進行異步查詢
            var lst = await AsyncExecuter.ToListAsync(query);
            //映射實體類到dto
            return ObjectMapper.Map<List<SysPermission>, List<SysPermissionDto>>(lst);
        }

        #endregion

        #region 獲取完整權限樹

        /// <summary>
        /// 獲取完整權限樹
        /// </summary>
        /// <returns>查詢結果</returns>
        public async Task<List<PermissionTreeDto>> GetPermissionTree()
        {
            var per = await _sysPermissionsRepository.ToListAsync();
            var lst = ObjectMapper.Map<List<SysPermission>, List<PermissionTreeDto>>(per);
            return PermissionTreeBuilder.Build(lst);
        }

        #endregion

        #region 獲取用戶權限碼

        /// <summary>
        /// 獲取用戶權限碼
        /// </summary>
        /// <param name="userId">用戶編號</param>
        /// <returns>查詢結果</returns>
        public async Task<List<string>> GetUserPermissionCode(Guid userId)
        {
            var query = from user in (await _userRolesRepository.GetQueryableAsync()).Where(c => c.UserId == userId)
                join rp in (await _rolePermissionsRepository.GetQueryableAsync()) on user.RoleId equals rp.RoleId
                join pe in (await _sysPermissionsRepository.GetQueryableAsync()) on rp.PermissionId equals pe.Id
                select pe.Code;
            var permission = await AsyncExecuter.ToListAsync(query);
            return permission.Distinct(x=>x);
        }

        #endregion
    }
}

添加角色權限關系管理實現類RolePermissionsAppService如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Demo.Identity.Permissions.Dto;
using Demo.Identity.Permissions.Entities;
using Volo.Abp.Domain.Repositories;

namespace Demo.Identity.Permissions
{
    /// <summary>
    /// 角色管理應用服務
    /// </summary>
    public class RolePermissionsAppService : IdentityAppService, IRolePermissionsAppService
    {
        #region 初始化
        private readonly IRepository<RolePermissions> _rolePermissionsRepository;
        private readonly IRepository<SysPermission> _sysPermissionsRepository;
        
        public RolePermissionsAppService(
            IRepository<RolePermissions> rolePermissionsRepository,
            IRepository<SysPermission> sysPermissionsRepository
            )
        {
            _rolePermissionsRepository = rolePermissionsRepository;
            _sysPermissionsRepository = sysPermissionsRepository;
        }
        #endregion


        #region 獲取角色所有權限
        /// <summary>
        /// 獲取角色所有權限
        /// </summary>
        /// <param name="roleId">角色ID</param>
        /// <returns></returns>
        public async Task<List<PermissionTreeDto>> GetPermission(Guid roleId)
        {
            var query = from rp in (await _rolePermissionsRepository.GetQueryableAsync())
                    .Where(x => x.RoleId == roleId)
                join permission in (await _sysPermissionsRepository.GetQueryableAsync())
                    on rp.PermissionId equals permission.Id
                select permission;
            var permissions = await AsyncExecuter.ToListAsync(query);
            var lst = ObjectMapper.Map<List<SysPermission>, List<PermissionTreeDto>>(permissions);
            return PermissionTreeBuilder.Build(lst);
        }
        #endregion

        #region 設置角色權限
        /// <summary>
        /// 設置角色權限
        /// </summary>
        /// <param name="roleId">橘色編號</param>
        /// <param name="permissions">權限編號</param>
        /// <returns></returns>
        public async Task SetPermission(SetRolePermissionsDto dto)
        {
            await _rolePermissionsRepository.DeleteAsync(x => x.RoleId == dto.RoleId);
            foreach (var permissionId in dto.Permissions)
            {
                RolePermissions entity = new RolePermissions()
                {
                    PermissionId = permissionId,
                    RoleId = dto.RoleId,
                };
                await _rolePermissionsRepository.InsertAsync(entity);
            }
        }
        #endregion

    }
}

 在Demo.Identity.EntityFrameworkCore項目IdentityDbContext類中加入以下屬性:

public DbSet<SysPermission> SysPermissions { get; set; }
public DbSet<RolePermissions> RolePermissions { get; set; }

在Demo.Identity.EntityFrameworkCore項目目錄下啟動命令提示符,執行以下命令分別創建和執行數據遷移:

dotnet-ef migrations add AddPermissions
dotnet-ef database update

在Demo.Identity.EntityFrameworkCore項目IdentityEntityFrameworkCoreModule類ConfigureServices方法中找到 options.AddDefaultRepositories(includeAllEntities: true); ,在其后面加入以下代碼:

options.AddDefaultRepository<IdentityUserRole>();

完成后運行身份管理服務,可正常運行和訪問各接口,則基礎服務層修改完成。

3. 公共組件

 添加公共類庫Demo.Permissions,編輯Demo.Permissions.csproj文件,將 <Project Sdk="Microsoft.NET.Sdk"> 改為:

<Project Sdk="Microsoft.NET.Sdk.Web">

為Demo.Permissions項目添加Nuget引用Volo.Abp.Core和Microsoft.AspNetCore.Http,並應用Demo.Identity.HttpApi.Client項目。

在Demo.Permissions中添加權限關系枚舉PermissionRelation如下:

namespace Demo.Permissions;

/// <summary>
/// 權限關系枚舉
/// </summary>
public enum PermissionRelation
{
    /// <summary>
    /// 需要同時滿足
    /// </summary>
    And,
    /// <summary>
    /// 只需要滿足任意一項
    /// </summary>
    Or,
        
}

在Demo.Permissions中添加CusPermissionAttribute特性,用於標記接口所需要的權限,如下:

namespace Demo.Permissions;

/// <summary>
/// 自定義權限特性
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class CusPermissionAttribute : Attribute
{
    /// <summary>
    /// 權限編碼
    /// </summary>
    public string[] PermissionCode { get; }

    /// <summary>
    /// 權限之間的關系
    /// </summary>
    public PermissionRelation Relation { get; } = PermissionRelation.And;
        

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="relation">權限關系</param>
    /// <param name="permissionCodes">權限編碼</param>
    public CusPermissionAttribute(PermissionRelation relation,params string[] permissionCodes)
    {
        Relation = relation;
        PermissionCode = permissionCodes;
    }
        
    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="permissionCodes">權限編碼</param>
    public CusPermissionAttribute(params string[] permissionCodes)
    {
        PermissionCode = permissionCodes;
    }
}

其中一個特性可以聲明多個權限碼,Relation表示該特性中所有權限碼間的關系,如果為And,需要用戶具有該特性聲明的所有權限碼才可通過驗證,若為Or,則表示用戶只要具有任意一個或多個該特性中聲明的權限就可通過驗證。一個接口可以聲明多個特性,特性與特性之間是And關系。

在Demo.Permissions中添加權限驗證中間件CusPermissionMiddleware如下:

using Demo.Identity.Permissions;
using Microsoft.AspNetCore.Http.Features;
using Volo.Abp.Users;

namespace Demo.Permissions;

/// <summary>
/// 自定義權限中間件
/// </summary>
public class CusPermissionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ICurrentUser _currentUser;
    private readonly ISysPermissionAppService _service;

    public CusPermissionMiddleware(RequestDelegate next, ICurrentUser currentUser, ISysPermissionAppService service)
    {
        _next = next;
        _currentUser = currentUser;
        _service = service;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var attributes = 
            context.GetEndpoint()?.Metadata.GetOrderedMetadata<CusPermissionAttribute>();
        
        //如果不存在CusPermissionAttribute特性則該接口不需要權限驗證,直接跳過
        if (attributes==null||attributes.Count==0)
        {
            await _next(context);
            return;
        }
        //如果需要權限驗證則必須是已登錄用戶,否則返回401
        if (_currentUser.Id == null)
        {
            context.Response.StatusCode = 401;
            return;
        }
        //獲取用戶權限
        var userPermisions = (await _service.GetUserPermissionCode((Guid) _currentUser.Id)).ToHashSet();
        //比對權限 如果無權限則返回403
        foreach (var cusPermissionAttribute in attributes)
        {
            var flag = cusPermissionAttribute.Relation == PermissionRelation.And
                ? cusPermissionAttribute.PermissionCode.All(code => userPermisions.Contains(code))
                : cusPermissionAttribute.PermissionCode.Any(code => userPermisions.Contains(code));
            if (!flag)
            {
                context.Response.StatusCode = 403;
                return;
            }
        }

        await _next(context);
    }
}

在接口調用時,該中間件會獲取接口所聲明的權限特性,並調用身份管理服務接口獲取當前用戶所持有的權限碼,按特性順序依次驗證。

在Demo.Permissions中添加PermissionRegistor類,用於在聚合服務啟動時讀取代碼中聲明的所有權限碼,並注冊到身份管理服務。代碼如下:

using System.ComponentModel;
using Demo.Identity.Permissions.Dto;

namespace Demo.Permissions;

/// <summary>
/// 權限注冊
/// </summary>
public static class PermissionRegistor
{

    /// <summary>
    /// 在指定類型中獲取權限集合
    /// </summary>
    /// <param name="serviceName">服務名稱</param>
    /// <typeparam name="T">類型</typeparam>
    /// <returns></returns>
    internal static List<SysPermissionDto> GetPermissions<T>(string serviceName)
    {
        List<SysPermissionDto> result = new List<SysPermissionDto>();
        Type type = typeof(T);
        var fields = type.GetFields().Where(x=>x.IsPublic&&x.IsStatic);
        foreach (var field in fields)
        {
            string code = field.GetValue(null).ToString();
            string name = "";
            object[] objs = field.GetCustomAttributes(typeof(DescriptionAttribute), false);  //獲取描述屬性
            if (objs != null && objs.Length > 0)
            {
                DescriptionAttribute descriptionAttribute = (DescriptionAttribute) objs[0];
                name = descriptionAttribute.Description;
            }

            string parentCode = null;
            if (code.Contains("."))
            {
                parentCode = code.Substring(0, code.LastIndexOf('.'));
            }

            result.Add(new SysPermissionDto()
            {
                Name = name,
                Code = code,
                ParentCode = parentCode,
                ServiceName = serviceName,
            });
        }

        return result;
    }
}

在Demo.Permissions中添加CusPermissionExtensions類,提供IApplicationBuilder的擴展方法,用於注冊中間件和注冊權限,代碼如下:

using Demo.Identity.Permissions;

namespace Demo.Permissions;

public static class CusPermissionExtensions
{
    /// <summary>
    /// 注冊自定義權限
    /// </summary>
    public static void UseCusPermissions<T>(this IApplicationBuilder app, string serviceName)
    {
        app.RegistPermissions<T>(serviceName);
        app.UseMiddleware<CusPermissionMiddleware>();
    }

    /// <summary>
    /// 注冊權限
    /// </summary>
    /// <param name="app"></param>
    /// <param name="serviceName">服務名稱</param>
    /// <typeparam name="T"></typeparam>
    private static async Task RegistPermissions<T>(this IApplicationBuilder app, string serviceName)
    {
        var service = app.ApplicationServices.GetService<ISysPermissionAppService>();
        var permissions = PermissionRegistor.GetPermissions<T>(serviceName);
        await service.RegistPermission(serviceName, permissions);
    }
}

在Demo.Permissions中添加DemoPermissionsModule類如下:

using Demo.Identity;
using Volo.Abp.Modularity;

namespace Demo.Permissions;

[DependsOn(
typeof(IdentityHttpApiClientModule))] public class DemoPermissionsModule:AbpModule { }

 4. 聚合服務層

 在聚合服務層,我們就可以使用剛才創建的Demo.Permissions類庫,這里以商城服務為例。

在Demo.Store.Application項目中添加Demo.Permissions的項目引用,並為DemoStoreApplicationModule類添加以下特性:

[DependsOn(typeof(DemoPermissionsModule))]

在Demo.Store.Application項目中添加在PermissionLab類用於聲明該服務中用到的所有權限,代碼如下

using System.ComponentModel;

namespace Demo.Store.Application;

/// <summary>
/// 權限列表
/// </summary>
public class PermissionLab
{
    [Description("訂單")]
    public const string ORDER = "Order";

    [Description("創建訂單")]
    public const string ORDER_CREATE = $"{ORDER}.Create";

    [Description("查詢訂單")]
    public const string ORDER_SELECT = $"{ORDER}.Select";

    //添加其他權限 ……
}

這里使用常量定義權限,其中常量的值為權限碼,常量名稱使用Description特性標記。

在Demo.Store.HttpApi.Host項目配置文件appsettings.json中的RemoteServices中添加身份管理服務地址如下:

"Default": {
    "BaseUrl": "http://localhost:5000/"
},

在Demo.Store.HttpApi.Host項目DemoStoreHttpApiHostModule類OnApplicationInitialization方法中找到 app.UseRouting(); ,在其后面添加如下內容:

app.UseCusPermissions<PermissionLab>("Store");

這樣我們就可以在聚合服務層ApplicationService的方法上添加CusPermission用於聲明接口所需要的權限,例如:

/// <summary>
/// 分頁查詢訂單列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
[CusPermission(PermissionLab.ORDER_SELECT)] public async Task<PagedResultDto<StoreOrderDto>> GetListAsync(PagedAndSortedResultRequestDto input) {   var ret = await _orderAppService.GetListAsync(input);   return new PagedResultDto<StoreOrderDto>   {     TotalCount = ret.TotalCount,     Items = ObjectMapper.Map<IReadOnlyList<OrderDto>, List<StoreOrderDto>>(ret.Items)   }; }

5.補充說明

完成以上步驟后,我們可以在聚合服務層Admin項目中將身份管理服務中角色權限相關接口封裝並暴露給客戶端調用,其中注冊權限接口僅為聚合服務層注冊權限使用,不建議暴露給客戶端。

這里我只簡單使用了對權限碼自身的校驗,並未做父子關系的關聯校驗,在實際項目中,可以依據需要進行修改或擴展。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM