ABP vNext微服务架构详细教程——分布式权限框架(上)

1 简介

ABP vNext框架本身提供了一套权限框架,其功能非常丰富,具体可参考官方文档:https://docs.abp.io/en/abp/latest/Authorization

但是我们使用时会发现,对于正常的单体应用,ABP vNext框架提供的权限系统没有问题, 但是在微服务架构下,这种权限系统并不是非常的友好。

我希望我的权限系统可以满足以下要求:

每个聚合服务持有独立的权限集合

每个聚合服务可以独立声明、使用其接口访问所需的权限。.

提供统一接口负责管理、存储所有服务权限并实现对角色的授权。

每个接口可以灵活组合使用一个或多个权限码。

权限框架使用尽量简单,减少额外编码量。

在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 AddPermissionsdotnet-ef database update
在Demo.Identity.EntityFrameworkCore项目IdentityEntityFrameworkCoreModule类ConfigureServices方法中找到 options.AddDefaultRepositories(includeAllEntities: true); ,在其后面加入以下代码:
options.AddDefaultRepository<IdentityUserRole>();
完成后运行身份管理服务,可正常运行和访问各接口,则基础服务层修改完成。后续操作请看下一篇