项目

本文档有多个版本。请选择最适合您的选项。

UI
Database

Web 应用程序开发教程 - 第 5 部分:授权

权限

ABP 提供了一个基于 ASP.NET Core 授权基础架构授权系统。在标准授权基础架构之上添加的一个主要功能是权限系统,它允许定义权限,并按角色、用户或客户端启用/禁用。

权限名称

一个权限必须有一个唯一的名称(一个 string)。最好的方式是将其定义为 const,以便我们可以重复使用该权限名称。

打开 Acme.BookStore.Application.Contracts 项目中的 BookStorePermissions 类(位于 Permissions 文件夹内),并添加新的权限名称:

namespace Acme.BookStore.Permissions;

public static class BookStorePermissions
{
    public const string GroupName = "BookStore";
    
    // 其他权限...
    // 其他权限...

    // *** 添加了一个新的嵌套类 ***
    public static class Books
    {
        public const string Default = GroupName + ".Books";
        public const string Create = Default + ".Create";
        public const string Edit = Default + ".Edit";
        public const string Delete = Default + ".Delete";
    }
}

这是定义权限名称的一种分层方式。例如,“创建图书”的权限名称被定义为 BookStore.Books.Create。ABP 并不强制要求你采用某种结构,但我们发现这种方式很有用。

权限定义

在使用权限之前,你应该先定义它们。

打开 Acme.BookStore.Application.Contracts 项目中的 BookStorePermissionDefinitionProvider 类(位于 Permissions 文件夹内),并按如下所示更改内容:

using Acme.BookStore.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;

namespace Acme.BookStore.Permissions;

public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
{
    public override void Define(IPermissionDefinitionContext context)
    {
        var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore"));
        
        // 仪表板权限
        bookStoreGroup.AddPermission(BookStorePermissions.Dashboard.Host, L("Permission:Dashboard"), MultiTenancySides.Host);
        bookStoreGroup.AddPermission(BookStorePermissions.Dashboard.Tenant, L("Permission:Dashboard"), MultiTenancySides.Tenant);

        // 图书权限
        var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books"));
        booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create"));
        booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit"));
        booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete"));
    }

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

这个类定义了一个权限组(用于在 UI 上分组权限,稍后会看到)和该组内的 4 个权限。此外,创建编辑删除BookStorePermissions.Books.Default 权限的子权限。一个子权限只有在父权限被选中时才能被选中。

最后,编辑本地化文件(Acme.BookStore.Domain.Shared 项目的 Localization/BookStore 文件夹下的 en.json),以定义上面使用的本地化键:

"Permission:BookStore": "书店",
"Permission:Books": "图书管理",
"Permission:Books.Create": "创建新图书",
"Permission:Books.Edit": "编辑图书",
"Permission:Books.Delete": "删除图书"

本地化键的名称是任意的,没有强制规则。但我们更喜欢上面使用的约定。

权限管理 UI

一旦定义了权限,你就可以在权限管理模态框中看到它们。

进入 管理 -> 身份标识 -> 角色 页面,选择 admin 角色的 权限 操作以打开权限管理模态框:

bookstore-permissions-ui

授予你想要的权限并保存模态框。

提示:如果你运行 Acme.BookStore.DbMigrator 应用程序,新权限会自动授予 admin 角色。

授权

现在,你可以使用这些权限来授权图书管理操作。

应用层 & HTTP API

打开 BookAppService 类,并将策略名称设置为上面定义的权限名称:

using System;
using Acme.BookStore.Permissions;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore.Books;

public class BookAppService :
    CrudAppService<
        Book, // Book 实体
        BookDto, // 用于显示图书
        Guid, // Book 实体的主键
        PagedAndSortedResultRequestDto, // 用于分页/排序
        CreateUpdateBookDto>, // 用于创建/更新图书
    IBookAppService // 实现 IBookAppService
{
    public BookAppService(IRepository<Book, Guid> repository)
        : base(repository)
    {
        GetPolicyName = BookStorePermissions.Books.Default;
        GetListPolicyName = BookStorePermissions.Books.Default;
        CreatePolicyName = BookStorePermissions.Books.Create;
        UpdatePolicyName = BookStorePermissions.Books.Edit;
        DeletePolicyName = BookStorePermissions.Books.Delete;
    }
}

已将代码添加到构造函数中。基类 CrudAppService 会在 CRUD 操作中自动使用这些权限。这使得应用程序服务是安全的,同时也使得 HTTP API 是安全的,因为如前所述,此服务会自动用作 HTTP API(参见自动 API 控制器)。

你将在开发作者管理功能时看到使用 [Authorize(...)] 属性的声明式授权。

授权 Razor 组件

打开 Acme.BookStore.Blazor.Client 项目中的 /Pages/Books.razor 文件,并在 @page 指令之后和以下命名空间导入(@using 行)之后添加 Authorize 属性,如下所示:

@page "/books"
@attribute [Authorize(BookStorePermissions.Books.Default)]
@using Acme.BookStore.Permissions
@using Microsoft.AspNetCore.Authorization
...

添加此属性可防止当前用户未登录或未被授予给定权限时进入此页面。如果尝试访问,用户将被重定向到登录页面。

显示/隐藏操作

图书管理页面有一个新建图书按钮以及每本书的编辑删除操作。如果当前用户未被授予相关权限,我们应该隐藏这些按钮/操作。

基类 AbpCrudPageBase 已经为这类操作提供了必要的功能。

设置策略(权限)名称

将以下代码块添加到 Books.razor 文件的末尾:

@code
{
    public Books() // 构造函数
    {
        LocalizationResource = typeof(BookStoreResource);
        
        CreatePolicyName = BookStorePermissions.Books.Create;
        UpdatePolicyName = BookStorePermissions.Books.Edit;
        DeletePolicyName = BookStorePermissions.Books.Delete;
    }
}

基类 AbpCrudPageBase 会自动在相关操作上检查这些权限。它还为我们定义了以下属性(如果我们需要手动检查它们):

  • HasCreatePermission:如果当前用户有权创建实体,则为 True。
  • HasUpdatePermission:如果当前用户有权编辑/更新实体,则为 True。
  • HasDeletePermission:如果当前用户有权删除实体,则为 True。

Blazor 提示:虽然将 C# 代码添加到 @code 块中对于小段代码是可以的,但当代码块变长时,建议使用代码隐藏(code-behind)方法来开发更易维护的代码库。我们将在作者部分使用这种方法。

隐藏新建图书按钮

用一个 if 块包裹新建图书按钮,如下所示:

@if (HasCreatePermission)
{
    <Button Color="Color.Primary"
            Clicked="OpenCreateModalAsync">@L["NewBook"]</Button>
}

隐藏编辑/删除操作

EntityAction 组件定义了 Visible 属性(参数)来有条件地显示操作。

更新 EntityActions 部分,如下所示:

<EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn">
    <EntityAction TItem="BookDto"
                  Text="@L["Edit"]"
                  Visible=HasUpdatePermission
                  Clicked="() => OpenEditModalAsync(context)" />
    <EntityAction TItem="BookDto"
                  Text="@L["Delete"]"
                  Visible=HasDeletePermission
                  Clicked="() => DeleteEntityAsync(context)"
                  ConfirmationMessage="()=>GetDeleteConfirmationMessage(context)" />
</EntityActions>

关于权限缓存

你可以运行并测试权限。从 admin 角色中移除一个与图书相关的权限,可以看到相关的按钮/操作从 UI 中消失。

ABP 在客户端缓存当前用户的权限。因此,当你为自己更改权限时,需要手动刷新页面才能生效。如果你不刷新并尝试使用被禁止的操作,则会从服务器收到 HTTP 403(禁止)响应。

为角色或用户更改权限在服务器端是立即可用的。因此,这个缓存系统不会引起任何安全问题。

菜单项

即使我们已经保护了图书管理页面的所有层,它仍然显示在应用程序的主菜单上。如果当前用户没有权限,我们应该隐藏该菜单项。

打开 Acme.BookStore.Blazor.Client 项目中的 BookStoreMenuContributor 类,找到下面的代码块:

context.Menu.AddItem(
    new ApplicationMenuItem(
        "BooksStore",
        l["Menu:BookStore"],
        icon: "fa fa-book"
    ).AddItem(
        new ApplicationMenuItem(
            "BooksStore.Books",
            l["Menu:Books"],
            url: "/books"
        )
    )
);

并用以下代码块替换它:

var bookStoreMenu = new ApplicationMenuItem(
    "BooksStore",
    l["Menu:BookStore"],
    icon: "fa fa-book"
);

context.Menu.AddItem(bookStoreMenu);

// 检查权限
bookStoreMenu.AddItem(new ApplicationMenuItem(
    "BooksStore.Books",
    l["Menu:Books"],
    url: "/books"
).RequirePermissions(BookStorePermissions.Books.Default));

你还需要在 ConfigureMenuAsync 方法中添加 async 关键字并重新调整返回值。最终的 ConfigureMainMenuAsync 方法应如下所示:

private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
    var l = context.GetLocalizer<BookStoreResource>();

    context.Menu.Items.Insert(
        0,
        new ApplicationMenuItem(
            "BookStore.Home",
            l["Menu:Home"],
            "/",
            icon: "fas fa-home"
        )
    );

    var bookStoreMenu = new ApplicationMenuItem(
        "BooksStore",
        l["Menu:BookStore"],
        icon: "fa fa-book"
    );

    context.Menu.AddItem(bookStoreMenu);

    // 检查权限
    bookStoreMenu.AddItem(new ApplicationMenuItem(
        "BooksStore.Books",
        l["Menu:Books"],
        url: "/books"
    ).RequirePermissions(BookStorePermissions.Books.Default));
}

在本文档中