ABP vNext框架本身提供了一套权限框架,其功能非常丰富,具体可参考官方文档:https://docs.abp.io/en/abp/latest/Authorization
但是我们使用时会发现,对于正常的单体应用,ABP vNext框架提供的权限系统没有问题, 但是在微服务架构下,这种权限系统并不是非常的友好。
我希望我的权限系统可以满足以下要求:
在ABP vNext框架基础上,重新编写了一套分布式权限框架,大体规则如下:
权限系统具体实现见下文。
在之前的文章中我们已经搭建了身份认证服务的基础框架,这里我们直接在此基础上新增代码。
在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; }
/// <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;
///
public class RolePermissions : Entity
{
///
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;
///
public class SysPermissionDto:EntityDto
{
///
[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;
///
public class PermissionTreeDto : EntityDto
{
///
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;
///
public class SetRolePermissionsDto
{
///
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;
///
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;
///
public interface ISysPermissionAppService:IApplicationService
{
///
/// 服务名称
/// 权限列表
///
Task
/// <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;
///
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
{
List
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
* {
* if (isnew)
* {
* Console.WriteLine($"新增项{obj.Id}");
* }
* else
* {
* Console.WriteLine($"已存在{obj.Id}");
* }
* }, out var lstNeedRemove);
*/
///
/// 源集合
/// 目标集合
/// 集合比对关键字
/// 新增或已有项处理方法,参数:(数据项, 是否是新增)
/// 需要删除的数据集
///
///
public static void MutiCompare
Func
Action
out Dictionary
{
//目标集合去重
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
{
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
{
///
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
{
///
public class RolePermissionsAppService : IdentityAppService, IRolePermissionsAppService
{
#region 初始化
private readonly IRepository
private readonly IRepository
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
public DbSet
在Demo.Identity.EntityFrameworkCore项目目录下启动命令提示符,执行以下命令分别创建和执行数据迁移:
dotnet-ef migrations add AddPermissions
dotnet-ef database update
在Demo.Identity.EntityFrameworkCore项目IdentityEntityFrameworkCoreModule类ConfigureServices方法中找到 options.AddDefaultRepositories(includeAllEntities: true); ,在其后面加入以下代码:
options.AddDefaultRepository
完成后运行身份管理服务,可正常运行和访问各接口,则基础服务层修改完成。
添加公共类库Demo.Permissions,编辑Demo.Permissions.csproj文件,将
为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; }
/// <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;
///
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;
///
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
{
///
public static void UseCusPermissions
{
app.RegistPermissions
app.UseMiddleware
}
/// <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
{
}
在聚合服务层,我们就可以使用刚才创建的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
这样我们就可以在聚合服务层ApplicationService的方法上添加CusPermission用于声明接口所需要的权限,例如:
///
///
///
///
[CusPermission(PermissionLab.ORDER_SELECT)]
public async Task
{
var ret = await _orderAppService.GetListAsync(input);
return new PagedResultDto
{
TotalCount = ret.TotalCount,
Items = ObjectMapper.Map
};
}
完成以上步骤后,我们可以在聚合服务层Admin项目中将身份管理服务中角色权限相关接口封装并暴露给客户端调用,其中注册权限接口仅为聚合服务层注册权限使用,不建议暴露给客户端。
这里我只简单使用了对权限码自身的校验,并未做父子关系的关联校验,在实际项目中,可以依据需要进行修改或扩展。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章