草叶睡蜢

导航

Web应用开发教程 - Part 5: 授权

Web应用开发教程 - Part 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";
        }
    }
}

This is a hierarchical way of defining permission names. For example, "create book" permission name was defined as BookStore.Books.Create. ABP doesn't force you to a structure, but we find this way useful.

这是一种定义权限名称的划分层级方式。例如,新建书籍的权限名称被定义为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"));

            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);
        }
    }
}

这个类定义了一个权限组(在用户界面上进行权限分组,下面会看到)和这个组内的4个权限。另外,创建编辑删除BookStorePermissions.Books.Default 权限的子权限。只有当父权限被选中时,子权限才能被选中。

最后,编辑本地化文件(en.jsonAcme.BookStore.Domain.Shared项目的Localization/BookStore文件夹下)定义上面使用的本地化键值对。

"Permission:BookStore": "Book Store",
"Permission:Books": "Book Management",
"Permission:Books.Create": "Creating new books",
"Permission:Books.Edit": "Editing the books",
"Permission:Books.Delete": "Deleting the books"

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

权限管理界面

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

进入管理->身份->角色页面,点击管理员角色的权限按钮,打开权限管理:

image

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

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

授权

现在,你可以使用权限来授权书籍管理。

应用层 & 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, //The Book entity
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto>, //Used to create/update a book
        IBookAppService //implement the 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,正如之前解释的那样(参考auto API controllers)。

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

{{if UI == "MVC"}}

Razor Page

虽然HTTP API和应用服务的安全防护可以防止未经授权的用户使用这些服务,但他们仍然可以导航到图书管理页面。当页面对服务端进行第一次AJAX调用时,他们会得到授权异常,我们也应该对页面进行授权,以获得更好的用户体验和安全。

打开BookStoreWebModule ,在ConfigureServices方法中添加以下代码块:

Configure<RazorPagesOptions>(options =>
{
    options.Conventions.AuthorizePage("/Books/Index", BookStorePermissions.Books.Default);
    options.Conventions.AuthorizePage("/Books/CreateModal", BookStorePermissions.Books.Create);
    options.Conventions.AuthorizePage("/Books/EditModal", BookStorePermissions.Books.Edit);
});

现在,未经授权的用户被重定向到登录页面

隐藏新建书籍按钮

书籍管理页面有一个新建书籍按钮,如果当前用户没有图书创建权限,该按钮应该是不可见的。

image

打开Pages/Books/Index.cshtml文件,修改内容如下:

@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Permissions
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Localization
@model IndexModel
@inject IStringLocalizer<BookStoreResource> L
@inject IAuthorizationService AuthorizationService
@section scripts
{
    <abp-script src="/Pages/Books/Index.js"/>
}

<abp-card>
    <abp-card-header>
        <abp-row>
            <abp-column size-md="_6">
                <abp-card-title>@L["Books"]</abp-card-title>
            </abp-column>
            <abp-column size-md="_6" class="text-right">
                @if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))
                {
                    <abp-button id="NewBookButton"
                                text="@L["NewBook"].Value"
                                icon="plus"
                                button-type="Primary"/>
                }
            </abp-column>
        </abp-row>
    </abp-card-header>
    <abp-card-body>
        <abp-table striped-rows="true" id="BooksTable"></abp-table>
    </abp-card-body>
</abp-card>
  • 添加 @inject IAuthorizationService AuthorizationService 来访问授权服务。
  • 使用@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))检查图书创建权限,条件判定的形式呈现新建书籍按钮。

JavaScript

图书管理页面中的图书列表,每一行都有一个操作按钮。该行为按钮包含编辑删除动作。

image

如果当前用户没有授予相关权限,我们应该隐藏这个动作。数据表行操作有一个visible选项,可以设置为false来隐藏动作项。

打开Acme.BookStore.Web项目中的Pages/Books/Index.js,为Edit动作添加一个visible选项,如下图所示:

{
    text: l('Edit'),
    visible: abp.auth.isGranted('BookStore.Books.Edit'), //CHECK for the PERMISSION
    action: function (data) {
        editModal.open({ id: data.record.id });
    }
}

对动作Delete做同样的处理:

visible: abp.auth.isGranted('BookStore.Books.Delete')
  • abp.auth.isGranted(...)是被用来检查之前定义的权限。
  • visible可以得到一个函数返回的bool值,该值将在以后根据某些条件进行计算。

菜单项

即使我们已经对图书管理页面做了全部方面防护,它仍然在应用程序的主菜单上可见。如果当前用户没有权限,我们应该隐藏该菜单项。

打开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);

//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
    bookStoreMenu.AddItem(new ApplicationMenuItem(
        "BooksStore.Books",
        l["Menu:Books"],
        url: "/Books"
    ));
}

你还需要为ConfigureMenuAsync方法添加async关键字并重组返回值。最终的BookStoreMenuContributor类应该是下面这样的:

using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Acme.BookStore.Localization;
using Acme.BookStore.MultiTenancy;
using Acme.BookStore.Permissions;
using Volo.Abp.TenantManagement.Web.Navigation;
using Volo.Abp.UI.Navigation;

namespace Acme.BookStore.Web.Menus
{
    public class BookStoreMenuContributor : IMenuContributor
    {
        public async Task ConfigureMenuAsync(MenuConfigurationContext context)
        {
            if (context.Menu.Name == StandardMenus.Main)
            {
                await ConfigureMainMenuAsync(context);
            }
        }

        private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
        {
            if (!MultiTenancyConsts.IsEnabled)
            {
                var administration = context.Menu.GetAdministration();
                administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName);
            }

            var l = context.GetLocalizer<BookStoreResource>();

            context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "~/"));

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

            context.Menu.AddItem(bookStoreMenu);

            //CHECK the PERMISSION
            if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
            {
                bookStoreMenu.AddItem(new ApplicationMenuItem(
                    "BooksStore.Books",
                    l["Menu:Books"],
                    url: "/Books"
                ));
            }
        }
    }
}

{{else if UI == "NG"}}

Angular Guard配置

UI的第一步是防止未经授权的用户看到 "书籍 "菜单项并进入书籍管理页面。

打开/src/app/book/book-routing.module.ts并替换为以下内容:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, PermissionGuard } from '@abp/ng.core';
import { BookComponent } from './book.component';

const routes: Routes = [
  { path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class BookRoutingModule {}
  • @abp/ng.core中导入了AuthGuardPermissionGuard
  • 添加canActivate: [AuthGuard, PermissionGuard]到路由定义中。

打开/src/app/route.provider.ts,在/books路由中添加requiredPolicy: 'BookStore.Books'/books路由块应该如下:

{
  path: '/books',
  name: '::Menu:Books',
  parentName: '::Menu:BookStore',
  layout: eLayoutType.application,
  requiredPolicy: 'BookStore.Books',
}

隐藏新建书籍按钮

图书管理页面有一个新建书籍按钮,如果当前用户没有书籍创建权限,该按钮应该是不可见的。

image

打开/src/app/book/book.component.html文件,替换新建按钮的HTML内容,如下所示:

<!-- Add the abpPermission directive -->
<button *abpPermission="'BookStore.Books.Create'" id="create" class="btn btn-primary" type="button" (click)="createBook()">
  <i class="fa fa-plus mr-1"></i>
  <span>{%{{{ '::NewBook' | abpLocalization }}}%}</span>
</button>
  • 仅仅添加*abpPermission="'BookStore.Books.Create'",如果当前用户没有权限,就会隐藏这个按钮。

隐藏编辑和删除操作

图书管理页面中的图书列表,每一行都有一个操作按钮。该操作按钮包括编辑删除操作:

image

如果当前用户没有授予相关权限,我们应该隐藏这个操作。

打开/src/app/book/book.component.html文件,替换编辑和删除按钮的内容,如下所示:

<!-- Add the abpPermission directive -->
<button *abpPermission="'BookStore.Books.Edit'" ngbDropdownItem (click)="editBook(row.id)">
  {%{{{ '::Edit' | abpLocalization }}}%}
</button>

<!-- Add the abpPermission directive -->
<button *abpPermission="'BookStore.Books.Delete'" ngbDropdownItem (click)="delete(row.id)">
  {%{{{ '::Delete' | abpLocalization }}}%}
</button>
  • 添加*abpPermission="'BookStore.Books.Edit'",如果当前用户没有编辑权限,会隐藏编辑操作。
  • 添加*abpPermission="'BookStore.Books.Delete'",如果当前用户没有删除权限,会隐藏删除操作。

授权Razor组件

打开Acme.BookStore.Blazor项目中的/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() // Constructor
    {
        CreatePolicyName = BookStorePermissions.Books.Create;
        UpdatePolicyName = BookStorePermissions.Books.Edit;
        DeletePolicyName = BookStorePermissions.Books.Delete;
    }
}

The base AbpCrudPageBase class automatically checks these permissions on the related operations. It also defines the given properties for us if we need to check them manually:

基类AbpCrudPageBase自动检查这些相关操作的权限。如果我们需要手动检查,它也为我们定义了指定的属性。

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

Blazor提示:虽然将C#代码添加到@code块中对于简单的代码部分是可以的,但是当代码块变长时,建议使用后台代码的方法来开发一个更可维护的代码库。我们将对作者的部分使用这种方法。

隐藏新建书籍按钮

用一个 "if "块包住新建书籍按钮,如下所示:

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

隐藏编辑/删除动作

EntityAction component defines Visible attribute (parameter) to conditionally show the action.

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>

关于权限缓存

你可以运行并测试这些权限。从管理员角色中删除一个与书有关的权限,可以看到相关的按钮/操作从用户界面中消失。

ABP框架在客户端缓存当前用户的权限。因此,当你为自己改变权限时,你需要手动刷新页面来使其生效。如果你不刷新并试图使用被禁止的操作,你会从服务端上得到一个HTTP 403(禁止)的响应。

改变一个角色或用户的权限在服务端会立即生效。所以,这个缓存系统不会造成任何安全问题。

菜单项

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

打开Acme.BookStore.Blazor项目中的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);

//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
    bookStoreMenu.AddItem(new ApplicationMenuItem(
        "BooksStore.Books",
        l["Menu:Books"],
        url: "/books"
    ));
}

你还需要在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);

    //CHECK the PERMISSION
    if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
    {
        bookStoreMenu.AddItem(new ApplicationMenuItem(
            "BooksStore.Books",
            l["Menu:Books"],
            url: "/books"
        ));
    }
}

{{end}}

下一篇

见本教程的下一篇

posted on 2021-12-20 13:16  草叶睡蜢  阅读(345)  评论(0编辑  收藏  举报