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; ////// 权限实体类 /// public class SysPermission : Entity { /// /// 服务名称 /// [MaxLength(64)] public string ServiceName { get; set; } /// /// 权限编码 /// [MaxLength(128)] public string Code { get; set; } /// /// 权限名称 /// [MaxLength(64)] public string Name { get; set; } /// /// 上级权限ID /// [MaxLength(128)] public string ParentCode { get; set; } /// /// 判断两个权限是否相同 /// /// /// public override bool Equals(object? obj) { return obj is SysPermission permission && permission.ServiceName == ServiceName && permission.Name == Name && permission.Code == Code && permission.ParentCode == ParentCode; } /// /// 设置ID的值 /// /// public void SetId(Guid id) { Id = id; } }
using System; using Volo.Abp.Domain.Entities; namespace Demo.Identity.Permissions.Entities; ////// 角色权限对应关系 /// public class RolePermissions : Entity { /// /// 角色编号 /// public Guid RoleId { get; set; } /// /// 权限编号 /// 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; ////// 权限DTO /// public class SysPermissionDto:EntityDto { /// /// 服务名称 /// [MaxLength(64)] public string ServiceName { get; set; } /// /// 权限编码 /// [MaxLength(128)] public string Code { get; set; } /// /// 权限名称 /// [MaxLength(64)] public string Name { get; set; } /// /// 上级权限ID /// [MaxLength(128)] public string ParentCode { get; set; } }
using System; using System.Collections.Generic; using Volo.Abp.Application.Dtos; namespace Demo.Identity.Permissions.Dto; ////// 权限树DTO /// public class PermissionTreeDto : EntityDto { /// /// 服务名称 /// public string ServiceName { get; set; } /// /// 权限编码 /// public string Code { get; set; } /// /// 权限名称 /// public string Name { get; set; } /// /// 上级权限ID /// public string ParentCode { get; set; } /// /// 子权限 /// public List Children { get; set; } }
using System; using System.Collections.Generic; namespace Demo.Identity.Permissions.Dto; ////// 设置角色权限DTO /// public class SetRolePermissionsDto { /// /// 角色编号 /// public Guid RoleId { get; set; } /// /// 权限ID列表 /// public List 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; ////// 角色管理应用服务接口 /// public interface IRolePermissionsAppService : IApplicationService { /// /// 获取角色所有权限 /// /// 角色ID /// Task > GetPermission(Guid roleId); ///
/// 设置角色权限 /// /// 角色权限信息 /// 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; ////// 权限管理应用服务接口 /// public interface ISysPermissionAppService:IApplicationService { /// /// 按服务注册权限 /// /// 服务名称 /// 权限列表 /// Task<bool> RegistPermission(string serviceName, List permissions); /// /// 按服务获取权限 /// /// 服务名称 /// 查询结果 Task > GetPermissions(string serviceName); ///
/// 获取完整权限树 /// /// /// 查询结果 Task > GetPermissionTree(); ///
/// 获取用户权限码 /// /// 用户编号 /// 查询结果 Task string>> GetUserPermissionCode(Guid userId); }
在公共类库文件夹common中创建.Net6类库项目项目Demo.Core,用于存放通用类。
这里我们在Demo.Core中添加文件夹CommonExtension用于存放通用扩展,添加EnumExtensions和ListExtensions类如下:
namespace Demo.Core.CommonExtension; ////// 枚举扩展类 /// public static class EnumExtensions { /// /// 获取描述特性 /// /// 枚举值 /// 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 { ////// 集合去重 /// /// 目标集合 /// 去重关键字 /// 集合元素类型 /// 去重关键字数据类型 /// 去重结果 public static List Distinct (this List lst,Func keySelector) { List result = new List (); HashSet set = new HashSet (); 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; ////// 集合比对 /// public class ListCompare { /* * 调用实例: * MutiCompare (lst1, lst2, x => x.Code, (obj, isnew) => * { * if (isnew) * { * Console.WriteLine($"新增项{obj.Id}"); * } * else * { * Console.WriteLine($"已存在{obj.Id}"); * } * }, out var lstNeedRemove); */ ////// 对比源集合和目标集合,处理已有项和新增项,并找出需要删除的项 /// /// 源集合 /// 目标集合 /// 集合比对关键字 /// 新增或已有项处理方法,参数:(数据项, 是否是新增) /// 需要删除的数据集 /// 集合对象数据类型 /// 对比关键字数据类型 public static void MutiCompare (List lstDestination,List lstSource, Func keySelector, Action bool> action, out Dictionary needRemove) { //目标集合去重 lstDestination.Distinct(keySelector); //将源集合存入字典,提高查询效率 needRemove = new Dictionary (); 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 { ////// 创建权限领域相关实体映射关系 /// /// public static void CreatePermissionsMap(this IdentityApplicationAutoMapperProfile profile) { profile.CreateMap (); profile.CreateMap (); profile.CreateMap (); } }
在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; ////// 权限建树帮助类 /// public static class PermissionTreeBuilder { /// /// 建立树形结构 /// /// /// public static List Build(List 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 (); 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 { ////// 权限管理应用服务 /// public class SysPermissionAppService : IdentityAppService, ISysPermissionAppService { #region 初始化 private readonly IRepository _rolePermissionsRepository; private readonly IRepository _sysPermissionsRepository; private readonly IRepository _userRolesRepository; public SysPermissionAppService( IRepository rolePermissionsRepository, IRepository sysPermissionsRepository, IRepository userRolesRepository ) { _rolePermissionsRepository = rolePermissionsRepository; _sysPermissionsRepository = sysPermissionsRepository; _userRolesRepository = userRolesRepository; } #endregion #region 按服务注册权限 /// /// 按服务注册权限 /// /// 服务名称 /// 权限列表 /// public async Task<bool> RegistPermission(string serviceName, List permissions) { //根据服务名称查询现有权限 var entities = await AsyncExecuter.ToListAsync( (await _sysPermissionsRepository.GetQueryableAsync()).Where(c => c.ServiceName == serviceName) ); var lst = ObjectMapper.Map , List
>(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 按服务获取权限 /// /// 按服务获取权限 /// /// 服务名称 /// 查询结果 public async Task > 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
>(lst); } #endregion #region 获取完整权限树 /// /// 获取完整权限树 /// /// 查询结果 public async Task > GetPermissionTree() { var per = await _sysPermissionsRepository.ToListAsync(); var lst = ObjectMapper.Map
, List
>(per); return PermissionTreeBuilder.Build(lst); } #endregion #region 获取用户权限码 /// /// 获取用户权限码 /// /// 用户编号 /// 查询结果 public async Task 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 { ////// 角色管理应用服务 /// public class RolePermissionsAppService : IdentityAppService, IRolePermissionsAppService { #region 初始化 private readonly IRepository _rolePermissionsRepository; private readonly IRepository _sysPermissionsRepository; public RolePermissionsAppService( IRepository rolePermissionsRepository, IRepository sysPermissionsRepository ) { _rolePermissionsRepository = rolePermissionsRepository; _sysPermissionsRepository = sysPermissionsRepository; } #endregion #region 获取角色所有权限 /// /// 获取角色所有权限 /// /// 角色ID /// public async Task > 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
>(permissions); return PermissionTreeBuilder.Build(lst); } #endregion #region 设置角色权限 /// /// 设置角色权限 /// /// 橘色编号 /// 权限编号 /// 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 DbSetSysPermissions { get; set; } public DbSet 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();
完成后运行身份管理服务,可正常运行和访问各接口,则基础服务层修改完成。
3. 公共组件
添加公共类库Demo.Permissions,编辑Demo.Permissions.csproj文件,将
"Microsoft.NET.Sdk.Web">
为Demo.Permissions项目添加Nuget引用Volo.Abp.Core和Microsoft.AspNetCore.Http,并应用Demo.Identity.HttpApi.Client项目。
在Demo.Permissions中添加权限关系枚举PermissionRelation如下:
namespace Demo.Permissions; ////// 权限关系枚举 /// public enum PermissionRelation { /// /// 需要同时满足 /// And, /// /// 只需要满足任意一项 /// Or, }
在Demo.Permissions中添加CusPermissionAttribute特性,用于标记接口所需要的权限,如下:
namespace Demo.Permissions; ////// 自定义权限特性 /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class CusPermissionAttribute : Attribute { /// /// 权限编码 /// public string[] PermissionCode { get; } /// /// 权限之间的关系 /// public PermissionRelation Relation { get; } = PermissionRelation.And; /// /// 构造函数 /// /// 权限关系 /// 权限编码 public CusPermissionAttribute(PermissionRelation relation,params string[] permissionCodes) { Relation = relation; PermissionCode = permissionCodes; } /// /// 构造函数 /// /// 权限编码 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; ////// 自定义权限中间件 /// 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特性则该接口不需要权限验证,直接跳过 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; ////// 权限注册 /// public static class PermissionRegistor { /// /// 在指定类型中获取权限集合 /// /// 服务名称 /// 类型 /// internal static List GetPermissions (string serviceName) { List result = new List (); 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 { ////// 注册自定义权限 /// public static void UseCusPermissions (this IApplicationBuilder app, string serviceName) { app.RegistPermissions (serviceName); app.UseMiddleware (); } /// /// 注册权限 /// /// /// 服务名称 /// private static async Task RegistPermissions (this IApplicationBuilder app, string serviceName) { var service = app.ApplicationServices.GetService (); var permissions = PermissionRegistor.GetPermissions (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; ////// 权限列表 /// 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("Store");
这样我们就可以在聚合服务层ApplicationService的方法上添加CusPermission用于声明接口所需要的权限,例如:
////// 分页查询订单列表 /// /// /// ///
[CusPermission(PermissionLab.ORDER_SELECT)] public async Task> GetListAsync(PagedAndSortedResultRequestDto input) { var ret = await _orderAppService.GetListAsync(input); return new PagedResultDto { TotalCount = ret.TotalCount, Items = ObjectMapper.Map , List >(ret.Items) }; }
5.补充说明
完成以上步骤后,我们可以在聚合服务层Admin项目中将身份管理服务中角色权限相关接口封装并暴露给客户端调用,其中注册权限接口仅为聚合服务层注册权限使用,不建议暴露给客户端。
这里我只简单使用了对权限码自身的校验,并未做父子关系的关联校验,在实际项目中,可以依据需要进行修改或扩展。