十、Abp vNext 基礎篇丨許可權

初久的私房菜 發表於 2021-09-23

介紹

本章節來把介面的許可權加一下

許可權配置和使用

官方地址:https://docs.abp.io/en/abp/latest/Authorization

下面這種程式碼可能我們日常開發都寫過,ASP.NET Core 提供的Authorize特性來幫我們做授權,但是BookStore_Author_Create策略,需要我們去手動宣告。

許可權配置

Abp定義了一個叫Permission System叫許可權系統啥的都可以,來幫助我們輕鬆的搞定授權,具體怎麼玩看下面程式碼就三部。

許可權配置

一、定義常量

    public static class CorePermissions
    {
        public const string GroupName = "Bvcp.Core";

        public static class Blogs
        {
            public const string Default = GroupName + ".Blog";
            public const string Management = Default + ".Management";
            public const string Delete = Default + ".Delete";
            public const string Update = Default + ".Update";
            public const string Create = Default + ".Create";
            public const string ClearCache = Default + ".ClearCache";
        }

        public static class Posts
        {
            public const string Default = GroupName + ".Post";
            public const string Delete = Default + ".Delete";
            public const string Update = Default + ".Update";
            public const string Create = Default + ".Create";
        }

        public static class Tags
        {
            public const string Default = GroupName + ".Tag";
            public const string Delete = Default + ".Delete";
            public const string Update = Default + ".Update";
            public const string Create = Default + ".Create";
        }

        public static class Comments
        {
            public const string Default = GroupName + ".Comment";
            public const string Delete = Default + ".Delete";
            public const string Update = Default + ".Update";
            public const string Create = Default + ".Create";
        }

        public static string[] GetAll()
        {
            return ReflectionHelper.GetPublicConstantsRecursively(typeof(CorePermissions));
        }
    }

二、新增許可權配置

    public class CorePermissionDefinitionProvider : PermissionDefinitionProvider
    {
        public override void Define(IPermissionDefinitionContext context)
        {
            var bloggingGroup = context.AddGroup(CorePermissions.GroupName, L("Permission:Core"));

            var blogs = bloggingGroup.AddPermission(CorePermissions.Blogs.Default, L("Permission:Blogs"));
            blogs.AddChild(CorePermissions.Blogs.Management, L("Permission:Management"));
            blogs.AddChild(CorePermissions.Blogs.Update, L("Permission:Edit"));
            blogs.AddChild(CorePermissions.Blogs.Delete, L("Permission:Delete"));
            blogs.AddChild(CorePermissions.Blogs.Create, L("Permission:Create"));
            blogs.AddChild(CorePermissions.Blogs.ClearCache, L("Permission:ClearCache"));

            var posts = bloggingGroup.AddPermission(CorePermissions.Posts.Default, L("Permission:Posts"));
            posts.AddChild(CorePermissions.Posts.Update, L("Permission:Edit"));
            posts.AddChild(CorePermissions.Posts.Delete, L("Permission:Delete"));
            posts.AddChild(CorePermissions.Posts.Create, L("Permission:Create"));

            var tags = bloggingGroup.AddPermission(CorePermissions.Tags.Default, L("Permission:Tags"));
            tags.AddChild(CorePermissions.Tags.Update, L("Permission:Edit"));
            tags.AddChild(CorePermissions.Tags.Delete, L("Permission:Delete"));
            tags.AddChild(CorePermissions.Tags.Create, L("Permission:Create"));

            var comments = bloggingGroup.AddPermission(CorePermissions.Comments.Default, L("Permission:Comments"));
            comments.AddChild(CorePermissions.Comments.Update, L("Permission:Edit"));
            comments.AddChild(CorePermissions.Comments.Delete, L("Permission:Delete"));
            comments.AddChild(CorePermissions.Comments.Create, L("Permission:Create"));
        }

        private static LocalizableString L(string name)
        {
            return LocalizableString.Create<CoreResource>(name);
        }
    }

三、使用

Authorize(CorePermissions.Posts.Delete)]

使用許可權

資源授權方案

資源授權可能用過的人不多,程式碼都會在整體的修改程式碼一節這裡先看就行,可以參考微軟官方文件

https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/resourcebased?view=aspnetcore-5.0

有些場景下,授權需要依賴於要訪問的資源,例如:每個資源通常會有一個建立者屬性,我們只允許該資源的建立者才可以對其進行編輯,刪除等操作,這就無法通過[Authorize]特性來指定授權了。因為授權過濾器會在我們的應用程式碼之前執行,無法確定所訪問的資源。此時,我們需要使用基於資源的授權,下面就來演示一下具體是如何操作的。

定義資源Requirement

在基於資源的授權中,我們要判斷的是使用者是否具有針對該資源的某項操作,因此,我們先定義一個代表操作的Requirement:

    public static class CommonOperations
    {
        public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement { Name = nameof(Update) };
        public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = nameof(Delete) };
    }

實現資源授權Handler

每一個 Requirement 都需要有一個對應的 Handler,來完成授權邏輯,可以直接讓 Requirement 實現IAuthorizationHandler介面。

我們是根據資源的建立者來判斷使用者是否具有操作許可權,實現我們的授權Handler:

  public class CommentAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Comment>
    {
        private readonly IPermissionChecker _permissionChecker;

        public CommentAuthorizationHandler(IPermissionChecker permissionChecker)
        {
            _permissionChecker = permissionChecker;
        }

        protected override async Task HandleRequirementAsync(
            AuthorizationHandlerContext context,
            OperationAuthorizationRequirement requirement,
            Comment resource)
        {
            if (requirement.Name == CommonOperations.Delete.Name && await HasDeletePermission(context, resource))
            {
                context.Succeed(requirement);
                return;
            }

            if (requirement.Name == CommonOperations.Update.Name && await HasUpdatePermission(context, resource))
            {
                context.Succeed(requirement);
                return;
            }
        }

        private async Task<bool> HasDeletePermission(AuthorizationHandlerContext context, Comment resource)
        {
            // 判斷建立人是否是登陸人
            if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId())
            {
                return true;
            }
            // 判斷當前使用者是否滿足資源操作策略
            if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Comments.Delete))
            {
                return true;
            }

            return false;
        }

        private async Task<bool> HasUpdatePermission(AuthorizationHandlerContext context, Comment resource)
        {
             // 判斷建立人是否是登陸人
            if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId())
            {
                return true;
            }
            // 判斷當前使用者是否滿足資源操作策略
            if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Comments.Update))
            {
                return true;
            }

            return false;
        }
    }

註冊Handler,這一點不要忘了

 public class CoreApplicationModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            Configure<AbpAutoMapperOptions>(options =>
            {
                options.AddMaps<CoreApplicationModule>();
            });

            Configure<AuthorizationOptions>(options =>
            {
                options.AddPolicy("BloggingUpdatePolicy", policy => policy.Requirements.Add(CommonOperations.Update));
                options.AddPolicy("BloggingDeletePolicy", policy => policy.Requirements.Add(CommonOperations.Delete));
            });

            context.Services.AddSingleton<IAuthorizationHandler, CommentAuthorizationHandler>();
            context.Services.AddSingleton<IAuthorizationHandler, PostAuthorizationHandler>();
         

        }
    }

使用策略授權

        [Authorize]
        public async Task<CommentWithDetailsDto> UpdateAsync(Guid id, UpdateCommentDto input)
        {

            var comment = await _commentRepository.GetAsync(id);
            // 檢測是否有許可權
            await AuthorizationService.CheckAsync(comment, CommonOperations.Update);

            comment.SetText(input.Text);

            comment = await _commentRepository.UpdateAsync(comment);

            return ObjectMapper.Map<Comment, CommentWithDetailsDto>(comment);
        }

整體的修改程式碼

整體結構

 public static class CommonOperations
    {
        public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement { Name = nameof(Update) };
        public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = nameof(Delete) };
    }
 public class CommentAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Comment>
    {
        private readonly IPermissionChecker _permissionChecker;

        public CommentAuthorizationHandler(IPermissionChecker permissionChecker)
        {
            _permissionChecker = permissionChecker;
        }

        protected override async Task HandleRequirementAsync(
            AuthorizationHandlerContext context,
            OperationAuthorizationRequirement requirement,
            Comment resource)
        {
            if (requirement.Name == CommonOperations.Delete.Name && await HasDeletePermission(context, resource))
            {
                context.Succeed(requirement);
                return;
            }

            if (requirement.Name == CommonOperations.Update.Name && await HasUpdatePermission(context, resource))
            {
                context.Succeed(requirement);
                return;
            }
        }

        private async Task<bool> HasDeletePermission(AuthorizationHandlerContext context, Comment resource)
        {
            if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId())
            {
                return true;
            }

            if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Comments.Delete))
            {
                return true;
            }

            return false;
        }

        private async Task<bool> HasUpdatePermission(AuthorizationHandlerContext context, Comment resource)
        {
            if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId())
            {
                return true;
            }

            if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Comments.Update))
            {
                return true;
            }

            return false;
        }
    }
  public class PostAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Post>
    {
        private readonly IPermissionChecker _permissionChecker;

        public PostAuthorizationHandler(IPermissionChecker permissionChecker)
        {
            _permissionChecker = permissionChecker;
        }

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement,
            Post resource)
        {

            if (requirement.Name == CommonOperations.Delete.Name && await HasDeletePermission(context, resource))
            {
                context.Succeed(requirement);
                return;
            }

            if (requirement.Name == CommonOperations.Update.Name && await HasUpdatePermission(context, resource))
            {
                context.Succeed(requirement);
                return;
            }

        }

        private async Task<bool> HasDeletePermission(AuthorizationHandlerContext context, Post resource)
        {
            if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId())
            {
                return true;
            }

            if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Posts.Delete))
            {
                return true;
            }

            return false;
        }

        private async Task<bool> HasUpdatePermission(AuthorizationHandlerContext context, Post resource)
        {
            if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId())
            {
                return true;
            }

            if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Posts.Update))
            {
                return true;
            }

            return false;
        }
    }

CoreApplicationModule.cs

 public class CoreApplicationModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            Configure<AbpAutoMapperOptions>(options =>
            {
                options.AddMaps<CoreApplicationModule>();
            });

            // 註冊
            Configure<AuthorizationOptions>(options =>
            {
                options.AddPolicy("BloggingUpdatePolicy", policy => policy.Requirements.Add(CommonOperations.Update));
                options.AddPolicy("BloggingDeletePolicy", policy => policy.Requirements.Add(CommonOperations.Delete));
            });

            context.Services.AddSingleton<IAuthorizationHandler, CommentAuthorizationHandler>();
            context.Services.AddSingleton<IAuthorizationHandler, PostAuthorizationHandler>();
         

        }
    }

CommentAppService.cs

        [Authorize]
        public async Task<CommentWithDetailsDto> UpdateAsync(Guid id, UpdateCommentDto input)
        {

            var comment = await _commentRepository.GetAsync(id);

            await AuthorizationService.CheckAsync(comment, CommonOperations.Update);

            comment.SetText(input.Text);

            comment = await _commentRepository.UpdateAsync(comment);

            return ObjectMapper.Map<Comment, CommentWithDetailsDto>(comment);
        }

        [Authorize]
        public async Task DeleteAsync(Guid id)
        {
            var comment = await _commentRepository.GetAsync(id);

            await AuthorizationService.CheckAsync(comment, CommonOperations.Delete);

            var replies = await _commentRepository.GetRepliesOfComment(id);

            foreach (var reply in replies)
            {
                await _commentRepository.DeleteAsync(reply.Id);
            }
        }

PostAppService.cs

 [Authorize(CorePermissions.Posts.Delete)]
        public async Task DeleteAsync(Guid id)
        {
            // 查詢文章
            var post = await _postRepository.GetAsync(id);
            // 判斷是否有資源操作權
            await AuthorizationService.CheckAsync(post, CommonOperations.Delete);
            // 根據文章獲取Tags
            var tags = await GetTagsOfPost(id);
            // 減少Tag引用數量
            await _tagRepository.DecreaseUsageCountOfTagsAsync(tags.Select(t => t.Id).ToList());
            // 刪除評論
            await _commentRepository.DeleteOfPost(id);
            // 刪除文章
            await _postRepository.DeleteAsync(id);
            await PublishPostChangedEventAsync(post.BlogId);
        }

        [Authorize(CorePermissions.Posts.Update)]
        public async Task<PostWithDetailsDto> UpdateAsync(Guid id, UpdatePostDto input)
        {
            var post = await _postRepository.GetAsync(id);

            input.Url = await RenameUrlIfItAlreadyExistAsync(input.BlogId, input.Url, post);

            await AuthorizationService.CheckAsync(post, CommonOperations.Update);

            post.SetTitle(input.Title);
            post.SetUrl(input.Url);
            post.Content = input.Content;
            post.Description = input.Description;
            post.CoverImage = input.CoverImage;

            post = await _postRepository.UpdateAsync(post);

            var tagList = SplitTags(input.Tags);
            await SaveTags(tagList, post);
            await PublishPostChangedEventAsync(post.BlogId);
            return ObjectMapper.Map<Post, PostWithDetailsDto>(post);
        }