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

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项目中将身份管理服务中角色权限相关接口封装并暴露给客户端调用,其中注册权限接口仅为聚合服务层注册权限使用,不建议暴露给客户端。

这里我只简单使用了对权限码自身的校验,并未做父子关系的关联校验,在实际项目中,可以依据需要进行修改或扩展。