ABP演示 - 授权

ABP v8.1 授权系统完整指南 - Publishers模块实战示例

本指南详细演示如何为ABP BookStore项目的Publishers模块添加完整的权限控制,涵盖从权限定义到实际应用的全流程。
exported_image


📚 目录


概述

ABP授权系统简介

ABP的授权系统基于ASP.NET Core的授权机制,并提供了权限系统来简化权限管理。它支持:

  • 声明式授权:使用 [Authorize] 特性
  • 隐式授权:在 CrudAppService 中配置策略名称
  • 代码级授权:使用 AuthorizationService 进行灵活检查
  • 基于资源的授权:根据资源本身进行权限判断

本示例目标

为Publishers模块实现以下功能:

功能 权限名称 说明
查看出版商 BookStore.Publishers 查看列表和详情
创建出版商 BookStore.Publishers.Create 创建新出版商
编辑出版商 BookStore.Publishers.Edit 更新出版商信息
删除出版商 BookStore.Publishers.Delete 删除出版商

权限定义与注册

步骤 1: 定义权限常量

文件: src/Acme.BookStore.Application.Contracts/Permissions/BookStorePermissions.cs
image

public static class Publishers
{
    public const string Default = GroupName + ".Publishers";
    public const string Create = Default + ".Create";
    public const string Edit = Default + ".Edit";
    public const string Delete = Default + ".Delete";
}

命名约定

  • 格式:{GroupName}.{Entity}.{Action}
  • 示例:BookStore.Publishers.Create
  • 好处:清晰、有层次、避免冲突

步骤 2: 注册权限

文件: src/Acme.BookStore.Application.Contracts/Permissions/BookStorePermissionDefinitionProvider.cs
image

public override void Define(IPermissionDefinitionContext context)
{
    var bookStoreGroup = context.AddGroup(
        BookStorePermissions.GroupName, 
        L("Permission:BookStore"));

    // 添加Publishers父权限
    var publishersPermission = bookStoreGroup.AddPermission(
        BookStorePermissions.Publishers.Default, 
        L("Permission:Publishers"));

    // 添加子权限
    publishersPermission.AddChild(
        BookStorePermissions.Publishers.Create, 
        L("Permission:Publishers.Create"));

    publishersPermission.AddChild(
        BookStorePermissions.Publishers.Edit, 
        L("Permission:Publishers.Edit"));

    publishersPermission.AddChild(
        BookStorePermissions.Publishers.Delete, 
        L("Permission:Publishers.Delete"));
}

权限层次结构

BookStore (权限组)
└── Publishers (父权限)
    ├── Create (子权限)
    ├── Edit (子权限)
    └── Delete (子权限)

步骤 3: 添加本地化文本

文件: src/Acme.BookStore.Domain.Shared/Localization/BookStore/zh-Hans.json

{
  "texts": {
    "Permission:BookStore": "书店管理",
    "Permission:Publishers": "出版商管理",
    "Permission:Publishers.Create": "创建新出版商",
    "Permission:Publishers.Edit": "编辑出版商",
    "Permission:Publishers.Delete": "删除出版商"
  }
}

权限的三种使用方式

方式 1: 隐式使用(CrudAppService 自动处理)

适用场景:标准的 CRUD 操作

工作原理
在构造函数中设置权限策略名称后,CrudAppService 基类会自动应用授权检查。

代码示例

public class PublisherAppService : CrudAppService<
    Publisher, PublisherDto, Guid, GetPublisherDto, 
    CreateUpdatePublisherDto, CreateUpdatePublisherDto>, 
    IPublisherAppService
{
    public PublisherAppService(
        IRepository<Publisher, Guid> repository, 
        IPublisherRepository publisherRepository,
        IOptions<PublisherOptions> publisherOptions,
        ILocalEventBus localEventBus)
        : base(repository)
    {
        _publisherRepository = publisherRepository;
        _publisherOptions = publisherOptions.Value;
        _localEventBus = localEventBus;

        // 🎯 配置权限策略 - 基类会自动使用这些配置!
        GetPolicyName = BookStorePermissions.Publishers.Default;
        GetListPolicyName = BookStorePermissions.Publishers.Default;
        CreatePolicyName = BookStorePermissions.Publishers.Create;
        UpdatePolicyName = BookStorePermissions.Publishers.Edit;
        DeletePolicyName = BookStorePermissions.Publishers.Delete;
    }

    // 以下方法会自动检查权限(不需要手动添加 [Authorize])
    public override async Task<PublisherDto> GetAsync(Guid id)
    {
        // 自动检查: BookStore.Publishers.Default 权限
        // 如果用户没有权限,会自动返回 403 Forbidden
    }

    public override async Task<PagedResultDto<PublisherDto>> GetListAsync(GetPublisherDto input)
    {
        // 自动检查: BookStore.Publishers.Default 权限
    }

    public override async Task<PublisherDto> CreateAsync(CreateUpdatePublisherDto input)
    {
        // 自动检查: BookStore.Publishers.Create 权限
    }

    public override async Task<PublisherDto> UpdateAsync(Guid id, CreateUpdatePublisherDto input)
    {
        // 自动检查: BookStore.Publishers.Edit 权限
    }

    public override async Task DeleteAsync(Guid id)
    {
        // 自动检查: BookStore.Publishers.Delete 权限
    }
}

image

内部机制(ABP源码简化版):

// CrudAppService 基类内部逻辑
[Authorize] 
public virtual async Task<TEntityDto> CreateAsync(TCreateInput input)
{
    await CheckCreatePolicyAsync(); // 检查权限
    // ... 执行创建逻辑
}

protected virtual async Task CheckCreatePolicyAsync()
{
    if (CreatePolicyName != null)
    {
        await AuthorizationService.CheckAsync(CreatePolicyName);
    }
}

优点

  • ✅ 代码简洁,不需要手动添加特性
  • ✅ 符合 DRY 原则
  • ✅ 统一的权限检查逻辑

缺点

  • ❌ 只能在 CrudAppService 中使用
  • ❌ 灵活性较低

方式 2: 显式使用 [Authorize] 特性

适用场景:自定义的非 CRUD 方法

代码示例

using Microsoft.AspNetCore.Authorization;

public class PublisherAppService : ApplicationService
{
    /// <summary>
    /// 批量删除出版商(需要删除权限)
    /// </summary>
    [Authorize(BookStorePermissions.Publishers.Delete)]
    public async Task BatchDeleteAsync(List<Guid> ids)
    {
        foreach (var id in ids)
        {
            await Repository.DeleteAsync(id);
        }
    }

    /// <summary>
    /// 允许匿名访问(通常用于公开API)
    /// </summary>
    [AllowAnonymous]
    public async Task<PublisherDto> GetPublicPublisherAsync(Guid id)
    {
        // 任何人都可以访问
    }

    /// <summary>
    /// 整个服务都需要登录
    /// </summary>
    [Authorize]
    public class PublisherAppService : ApplicationService
    {
        // 所有方法都需要登录
    }
}

优点

  • ✅ 声明式,代码清晰
  • ✅ 易于理解和维护
  • ✅ 符合 ASP.NET Core 的授权模式

缺点

  • ❌ 无法在方法内部添加权限相关的业务逻辑
  • ❌ 无法根据权限返回不同的数据

方式 3: 使用 AuthorizationService

适用场景:需要在代码中进行灵活的权限检查

3.1 使用 CheckAsync(强制检查)

using Volo.Abp.Authorization;

public class PublisherAppService : ApplicationService
{
    /// <summary>
    /// 获取出版商统计信息(需要查看权限)
    /// </summary>
    public async Task<string> GetPublisherStatisticsAsync()
    {
        // 检查权限,如果没有权限会抛出 AbpAuthorizationException
        await AuthorizationService.CheckAsync(BookStorePermissions.Publishers.Default);

        var totalCount = await Repository.CountAsync();
        var recentCount = await Repository.CountAsync(
            p => p.CreationTime > DateTime.Now.AddDays(-30));

        return $"总出版商数: {totalCount}, 最近30天新增: {recentCount}";
    }
}

特点

  • 无权限时抛出 AbpAuthorizationException 异常
  • 异常会被ABP框架捕获并转换为 403 Forbidden 响应
  • 适用于需要强制权限检查的场景

3.2 使用 IsGrantedAsync(条件检查)

using Volo.Abp.Authorization;

public class PublisherAppService : ApplicationService
{
    /// <summary>
    /// 获取带权限信息的出版商详情
    /// </summary>
    public async Task<PublisherDto> GetPublisherWithDetailsAsync(Guid id)
    {
        var publisher = await Repository.GetAsync(id);
        var dto = ObjectMapper.Map<Publisher, PublisherDto>(publisher);

        // 检查用户是否有编辑权限(不会抛出异常)
        bool canEdit = await AuthorizationService.IsGrantedAsync(
            BookStorePermissions.Publishers.Edit);
        dto.CanEdit = canEdit;

        // 检查用户是否有删除权限
        bool canDelete = await AuthorizationService.IsGrantedAsync(
            BookStorePermissions.Publishers.Delete);
        dto.CanDelete = canDelete;

        return dto;
    }
}

特点

  • 不抛出异常,返回 truefalse
  • 可以根据权限返回不同的数据
  • 这是前端UI控制按钮显示的关键!

前端使用示例

// 在 Angular/React/Vue 前端
const publisher = await publisherService.getPublisherWithDetailsAsync(id);

if (publisher.canEdit) {
    // 显示编辑按钮
    showEditButton = true;
}

if (publisher.canDelete) {
    // 显示删除按钮
    showDeleteButton = true;
}

3.3 在循环中检查权限

public async Task<List<string>> BatchExportAsync(List<Guid> ids)
{
    var results = new List<string>();
    
    foreach (var id in ids)
    {
        var publisher = await Repository.GetAsync(id);
        
        // 检查用户是否有权限查看这个特定的出版商
        if (await AuthorizationService.IsGrantedAsync(
            BookStorePermissions.Publishers.Default))
        {
            results.Add(publisher.Name);
        }
        else
        {
            // 跳过无权限的项,而不是直接失败
            results.Add($"[无权限查看] {publisher.Name}");
        }
    }
    
    return results;
}

优点

  • ✅ 最灵活,可以完全控制权限检查逻辑
  • ✅ 可以根据权限返回不同的响应
  • ✅ 适合复杂的业务场景
  • ✅ 可以在循环中进行条件性检查

缺点

  • ❌ 需要手动编写检查代码
  • ❌ 代码量相对较多

使用方式对比

使用方式 适用场景 示例方法 优点 缺点
隐式使用 CRUD 操作 CreateAsync, UpdateAsync, DeleteAsync, GetListAsync 简洁,无需手动添加 只能在 CrudAppService 中使用,灵活性低
[Authorize] 特性 自定义 API 方法 BatchDeleteAsync, ExportDataAsync 声明式,代码清晰 无法在方法内部添加逻辑
AuthorizationService.CheckAsync 需要强制权限检查 GetStatisticsAsync, GetReportAsync 灵活,可添加业务逻辑 需要手动编写检查代码
AuthorizationService.IsGrantedAsync 需要条件性逻辑 GetWithPermissionsAsync, BatchExportAsync 最灵活,可返回不同数据 代码量相对较多

完整代码示例

1. 权限定义

BookStorePermissions.cs

namespace Acme.BookStore.Permissions;

public static class BookStorePermissions
{
    public const string GroupName = "BookStore";

    public static class Publishers
    {
        public const string Default = GroupName + ".Publishers";
        public const string Create = Default + ".Create";
        public const string Edit = Default + ".Edit";
        public const string Delete = Default + ".Delete";
    }
}

2. 权限注册

BookStorePermissionDefinitionProvider.cs

public override void Define(IPermissionDefinitionContext context)
{
    var bookStoreGroup = context.AddGroup(
        BookStorePermissions.GroupName, 
        L("Permission:BookStore"));

    var publishersPermission = bookStoreGroup.AddPermission(
        BookStorePermissions.Publishers.Default, 
        L("Permission:Publishers"));

    publishersPermission.AddChild(
        BookStorePermissions.Publishers.Create, 
        L("Permission:Publishers.Create"));

    publishersPermission.AddChild(
        BookStorePermissions.Publishers.Edit, 
        L("Permission:Publishers.Edit"));

    publishersPermission.AddChild(
        BookStorePermissions.Publishers.Delete, 
        L("Permission:Publishers.Delete"));
}

3. 应用服务

PublisherAppService.cs

using Acme.BookStore.Authors;
using Acme.BookStore.Options;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Authorization;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Local;

namespace Acme.BookStore.Publishers
{
    public class PublisherAppService : CrudAppService<
        Publisher, 
        PublisherDto, 
        Guid, 
        GetPublisherDto, 
        CreateUpdatePublisherDto, 
        CreateUpdatePublisherDto>, 
        IPublisherAppService
    {
        private readonly IPublisherRepository _publisherRepository;
        private readonly PublisherOptions _publisherOptions;
        private readonly ILocalEventBus _localEventBus;

        public PublisherAppService(
            IRepository<Publisher, Guid> repository, 
            IPublisherRepository publisherRepository,
            IOptions<PublisherOptions> publisherOptions,
            ILocalEventBus localEventBus)
            : base(repository)
        {
            _publisherRepository = publisherRepository;
            _publisherOptions = publisherOptions.Value;
            _localEventBus = localEventBus;

            // 🎯 隐式使用:配置权限策略名称
            GetPolicyName = BookStorePermissions.Publishers.Default;
            GetListPolicyName = BookStorePermissions.Publishers.Default;
            CreatePolicyName = BookStorePermissions.Publishers.Create;
            UpdatePolicyName = BookStorePermissions.Publishers.Edit;
            DeletePolicyName = BookStorePermissions.Publishers.Delete;
        }

        // CRUD方法会自动应用权限检查(隐式方式)
        public override async Task<PublisherDto> CreateAsync(CreateUpdatePublisherDto input)
        {
            var publisher = new Publisher(
                GuidGenerator.Create(),
                input.Name,
                input.EstablishmentTime,
                new Address(
                    input.Address.Street,
                    input.Address.City,
                    input.Address.StateOrProvince,
                    input.Address.PostalCode,
                    input.Address.Country
                ),
                input.Description,
                input.Url
            );
            
            await Repository.InsertAsync(publisher);
            return ObjectMapper.Map<Publisher, PublisherDto>(publisher);
        }

        public override async Task<PagedResultDto<PublisherDto>> GetListAsync(GetPublisherDto input)
        {
            if (input.Sorting.IsNullOrWhiteSpace())
            {
                input.Sorting = _publisherOptions.DefaultSorting;
            }

            var maxResultCount = input.MaxResultCount > 0 
                ? Math.Min(input.MaxResultCount, _publisherOptions.MaxPageSize)
                : _publisherOptions.DefaultPageSize;

            var publishers = await _publisherRepository.GetListAsync(
                input.SkipCount,
                maxResultCount,
                input.Sorting,
                input.Filter
            );

            var totalCount = input.Filter == null
                ? await _publisherRepository.CountAsync()
                : await _publisherRepository.CountAsync(
                    publisher => publisher.Name.Contains(input.Filter));

            return new PagedResultDto<PublisherDto>(
                totalCount,
                ObjectMapper.Map<List<Publisher>, List<PublisherDto>>(publishers)
            );
        }

        // 🎯 显式使用1:使用 [Authorize] 特性
        [Authorize(BookStorePermissions.Publishers.Delete)]
        public async Task BatchDeleteAsync(List<Guid> ids)
        {
            foreach (var id in ids)
            {
                await Repository.DeleteAsync(id);
            }
        }

        // 🎯 显式使用2:使用 AuthorizationService.CheckAsync
        public async Task<string> GetPublisherStatisticsAsync()
        {
            await AuthorizationService.CheckAsync(BookStorePermissions.Publishers.Default);

            var totalCount = await Repository.CountAsync();
            var recentCount = await Repository.CountAsync(
                p => p.CreationTime > DateTime.Now.AddDays(-30));

            return $"总出版商数: {totalCount}, 最近30天新增: {recentCount}";
        }

        // 🎯 显式使用3:使用 AuthorizationService.IsGrantedAsync
        public async Task<PublisherDto> GetPublisherWithDetailsAsync(Guid id)
        {
            var publisher = await Repository.GetAsync(id);
            var dto = ObjectMapper.Map<Publisher, PublisherDto>(publisher);

            dto.CanEdit = await AuthorizationService.IsGrantedAsync(
                BookStorePermissions.Publishers.Edit);
            dto.CanDelete = await AuthorizationService.IsGrantedAsync(
                BookStorePermissions.Publishers.Delete);

            return dto;
        }
    }
}

4. DTO定义

PublisherDto.cs

public class PublisherDto : FullAuditedEntityDto<Guid>
{
    public string Name { get; set; }
    public string Description { get; set; }
    public DateTime EstablishmentTime { get; set; }
    public string Address { get; set; }
    public string Url { get; set; }

    /// <summary>
    /// 当前用户是否可以编辑此出版商
    /// </summary>
    public bool CanEdit { get; set; }

    /// <summary>
    /// 当前用户是否可以删除此出版商
    /// </summary>
    public bool CanDelete { get; set; }
}

5. 接口定义

IPublisherAppService.cs

public interface IPublisherAppService : IApplicationService
{
    Task<PagedResultDto<PublisherDto>> GetListAsync(GetPublisherDto input);

    /// <summary>
    /// 批量删除出版商(需要删除权限)
    /// </summary>
    Task BatchDeleteAsync(List<Guid> ids);

    /// <summary>
    /// 获取出版商统计信息(需要查看权限)
    /// </summary>
    Task<string> GetPublisherStatisticsAsync();

    /// <summary>
    /// 获取带权限信息的出版商详情
    /// </summary>
    Task<PublisherDto> GetPublisherWithDetailsAsync(Guid id);
}

权限测试

1. 在管理后台设置权限

  1. 以管理员身份登录
  2. 导航到 用户管理角色管理
  3. 选择要设置的用户或角色
  4. 在权限选项卡中,找到 BookStore > 出版商管理
  5. 勾选相应的权限复选框
  6. 保存设置

界面效果

权限管理
├─ BookStore (权限组)
│  ├─ 图书管理
│  ├─ 作者管理
│  └─ 出版商管理  ← 点击展开
│     ├─ ☑ 创建新出版商
│     ├─ ☑ 编辑出版商
│     └─ ☑ 删除出版商

2. 测试API端点

测试获取列表(有权限)

curl -X GET "https://localhost:44300/api/app/publisher" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# 响应:200 OK
{
  "totalCount": 10,
  "items": [...]
}

测试创建出版商(无权限)

curl -X POST "https://localhost:44300/api/app/publisher" \
  -H "Authorization: Bearer TOKEN_WITHOUT_CREATE_PERMISSION" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "示例出版社",
    "establishmentTime": "2024-01-01T00:00:00Z",
    "address": {
      "street": "人民路123号",
      "city": "北京市",
      "stateOrProvince": "北京市",
      "postalCode": "100000",
      "country": "中国"
    }
  }'

# 响应:403 Forbidden
{
  "error": {
    "code": "Volo.Abp.Authorization.AbpAuthorizationException",
    "message": "授权失败!您的权限不足:需要权限 'BookStore.Publishers.Create'",
    "details": "..."
  }
}

3. 查看日志

在日志中搜索权限检查相关的日志:

[INF] Authorization: Checking permission: BookStore.Publishers.Delete
[WRN] Authorization: Permission 'BookStore.Publishers.Delete' was not granted for user: john.doe

权限系统高级配置与扩展

一、权限系统的高级配置

1.1 权限多租户细分配置

ABP 官方支持通过 multiTenancySide 参数指定权限适用范围,这对于多租户应用非常重要。

MultiTenancySides 枚举值

  • Host - 仅适用于主机(租户管理员)
  • Tenant - 仅适用于租户
  • Both - 适用于主机和租户(默认值)

代码示例

public override void Define(IPermissionDefinitionContext context)
{
    var bookStoreGroup = context.AddGroup(
        BookStorePermissions.GroupName, 
        L("Permission:BookStore"));

    // 仅主机管理员可以访问的权限
    var hostOnlyPermission = bookStoreGroup.AddPermission(
        "BookStore.HostOnly", 
        L("Permission:HostOnly"));
    hostOnlyPermission.MultiTenancySide = MultiTenancySides.Host;

    // 仅租户用户可以访问的权限
    var tenantOnlyPermission = bookStoreGroup.AddPermission(
        "BookStore.TenantOnly", 
        L("Permission:TenantOnly"));
    tenantOnlyPermission.MultiTenancySide = MultiTenancySides.Tenant;

    // 主机和租户都可以访问的权限(默认)
    var bothPermission = bookStoreGroup.AddPermission(
        "BookStore.Publishers.Default", 
        L("Permission:Publishers"));
    bothPermission.MultiTenancySide = MultiTenancySides.Both;
}

多租户场景适配

  • 主机管理员权限:系统级配置、租户管理、模块安装等
  • 租户权限:业务数据操作、用户管理等
  • 共享权限:通用功能,如查看权限等

1.2 权限启用 / 禁用控制

ABP 允许通过 isEnabled: false 禁用权限,禁用后所有用户无法使用该权限,权限检查会返回 false

禁用权限

public override void Define(IPermissionDefinitionContext context)
{
    var bookStoreGroup = context.AddGroup(
        BookStorePermissions.GroupName, 
        L("Permission:BookStore"));

    // 禁用某个权限(例如:功能未完成时)
    var disabledPermission = bookStoreGroup.AddPermission(
        "BookStore.FeatureNotReady", 
        L("Permission:FeatureNotReady"));
    disabledPermission.IsEnabled = false;
}

修改依赖模块的权限

public override void Define(IPermissionDefinitionContext context)
{
    // 获取并修改 ABP 框架内置权限
    var deleteRolePermission = context.GetPermissionOrNull(IdentityPermissions.Roles.Delete);
    if (deleteRolePermission != null)
    {
        // 禁用删除角色的权限(例如:不允许删除系统角色)
        deleteRolePermission.IsEnabled = false;
    }
}

适用场景

  • 功能未完成时临时禁用
  • 特定环境下禁用某些功能
  • 限制对框架内置权限的使用

二、权限扩展与自定义核心能力

2.1 Permission Value Providers 扩展

ABP 官方提供 3 种预定义 Provider,并支持自定义 Provider 来扩展权限授予逻辑。

预定义 Provider

  • UserPermissionValueProvider - 基于用户授予权限
  • RolePermissionValueProvider - 基于角色授予权限
  • ClientPermissionValueProvider - 基于客户端(应用)授予权限

自定义 Provider 示例

using Volo.Abp.Authorization;
using Volo.Abp.Security.Claims;
using Volo.Abp.Users;

namespace Acme.BookStore.Authorization
{
    public class DepartmentPermissionValueProvider : PermissionValueProvider
    {
        public override string Name => "Department";

        protected override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
        {
            var userId = context.Principal?.FindUserId();
            if (userId == null)
            {
                return PermissionGrantResult.Undefined;
            }

            // 从用户声明中获取部门信息
            var departmentClaim = context.Principal.FindFirst("Department");
            if (departmentClaim == null)
            {
                return PermissionGrantResult.Undefined;
            }

            // 根据部门授予特定权限
            if (departmentClaim.Value == "IT" && 
                context.Permission.Name == "BookStore.TechnicalReports")
            {
                return PermissionGrantResult.Granted;
            }

            return PermissionGrantResult.Undefined;
        }
    }
}

注册自定义 Provider

public override void ConfigureServices(ServiceConfigurationContext context)
{
    Configure<AbpPermissionOptions>(options =>
    {
        options.ValueProviders.Add<DepartmentPermissionValueProvider>();
    });
}

PermissionGrantResult 枚举值

  • Granted - 权限已授予
  • Denied - 权限被拒绝
  • Undefined - 未定义(继续检查其他 Provider)

2.2 自定义声明(IAbpClaimsPrincipalContributor)

ABP 支持通过实现 IAbpClaimsPrincipalContributor 接口添加自定义声明,这些声明可以用于权限检查或其他业务逻辑。

添加自定义声明示例

using System.Security.Claims;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Security.Claims;

namespace Acme.BookStore.Security
{
    public class CustomClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
    {
        public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
        {
            var userId = context.ClaimsPrincipal?.FindUserId();
            if (userId == null)
            {
                return;
            }

            // 从数据库获取用户额外信息
            var user = await UserRepository.GetAsync(userId.Value);
            
            // 添加自定义声明
            context.Claims.Add(new Claim("Department", user.Department));
            context.Claims.Add(new Claim("EmployeeId", user.EmployeeId));
            context.Claims.Add(new Claim("UserType", user.Type.ToString()));
        }
    }
}

配置声明服务

public override void ConfigureServices(ServiceConfigurationContext context)
{
    Configure<AbpClaimsServiceOptions>(options =>
    {
        options.AddContributor<CustomClaimsPrincipalContributor>();
    });
}

使用自定义声明

public async Task<bool> CanAccessDepartmentReportAsync()
{
    var department = CurrentUser.FindClaim("Department")?.Value;
    return department == "Finance";
}

2.3 自定义策略覆盖权限

ABP 允许注册与权限名相同的 ASP.NET Core 策略,覆盖已有权限逻辑。

自定义策略示例

using Microsoft.AspNetCore.Authorization;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Authorization
{
    public class TimeBasedPermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
        protected override Task HandleRequirementAsync(
            AuthorizationHandlerContext context,
            PermissionRequirement requirement)
        {
            // 仅在工作时间(9:00-18:00)授予权限
            var now = DateTime.Now;
            var isWorkingHours = now.Hour >= 9 && now.Hour < 18;

            if (!isWorkingHours)
            {
                context.Fail("非工作时间,无法执行此操作");
                return Task.CompletedTask;
            }

            context.Succeed(requirement);
            return Task.CompletedTask;
        }
    }
}

注册自定义策略

public override void ConfigureServices(ServiceConfigurationContext context)
{
    var services = context.Services;
    
    services.AddAuthorization(options =>
    {
        options.AddPolicy("BookStore.Publishers.Create", policy =>
        {
            policy.Requirements.Add(new PermissionRequirement());
            policy.AddRequirements(new TimeBasedRequirement());
        });
    });

    services.AddSingleton<IAuthorizationHandler, TimeBasedPermissionHandler>();
}

三、高级组件与特殊场景

3.1 Permission Store 接口

IPermissionStore 是权限数据持久化的核心接口,ABP 默认提供数据库实现(由权限管理模块提供)。

接口定义

public interface IPermissionStore
{
    Task<bool> IsGrantedAsync(string name, string providerName, string providerKey);
    Task<MultiplePermissionValueProviderResult> IsGrantedAsync(string[] names);
    Task<PermissionGrantResult> IsGrantedAsync(string name, string providerName, string providerKey);
}

自定义 Permission Store 示例(例如:使用 Redis 缓存):

using System.Threading.Tasks;
using Volo.Abp.Authorization;
using Volo.Abp.Caching;
using StackExchange.Redis;

namespace Acme.BookStore.Authorization
{
    public class RedisPermissionStore : IPermissionStore, ITransientDependency
    {
        private readonly IDistributedCache<PermissionCacheItem> _cache;
        private readonly IConnectionMultiplexer _redis;

        public RedisPermissionStore(
            IDistributedCache<PermissionCacheItem> cache,
            IConnectionMultiplexer redis)
        {
            _cache = cache;
            _redis = redis;
        }

        public async Task<bool> IsGrantedAsync(
            string name, 
            string providerName, 
            string providerKey)
        {
            var cacheKey = $"Permission:{providerName}:{providerKey}:{name}";
            var cached = await _cache.GetAsync(cacheKey);
            return cached?.IsGranted ?? false;
        }

        public async Task<MultiplePermissionValueProviderResult> IsGrantedAsync(string[] names)
        {
            var result = new MultiplePermissionValueProviderResult();
            
            foreach (var name in names)
            {
                var isGranted = await IsGrantedAsync(name, null, null);
                result.Result[name] = isGranted ? PermissionGrantResult.Granted : PermissionGrantResult.Undefined;
            }
            
            return result;
        }

        public async Task<PermissionGrantResult> IsGrantedAsync(
            string name, 
            string providerName, 
            string providerKey)
        {
            var isGranted = await IsGrantedAsync(name, providerName, providerKey);
            return isGranted ? PermissionGrantResult.Granted : PermissionGrantResult.Undefined;
        }
    }
}

注册自定义 Store

public override void ConfigureServices(ServiceConfigurationContext context)
{
    context.Services.Replace<IPermissionStore, RedisPermissionStore>();
}

3.2 AlwaysAllowAuthorizationService(禁用授权)

ABP 支持通过 AddAlwaysAllowAuthorization() 注册服务,用于集成测试时绕过授权。

注册 AlwaysAllowAuthorizationService

using Volo.Abp.Authorization;

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // 仅在测试环境中启用
    if (context.Services.IsTesting())
    {
        context.Services.AddAlwaysAllowAuthorization();
    }
}

使用场景

  • 集成测试:避免权限检查干扰测试逻辑
  • 开发调试:临时禁用授权以快速测试功能
  • 数据迁移:某些迁移操作需要绕过权限

注意事项

  • ⚠️ 仅在测试或开发环境使用
  • ⚠️ 生产环境必须禁用
  • ⚠️ 不会影响 [AllowAnonymous] 特性

3.3 基于资源的授权

ABP 支持针对具体资源(如单个出版商记录)的授权检查,通过实现 AuthorizationHandler<OperationAuthorizationRequirement, TResource>

定义操作需求

using Microsoft.AspNetCore.Authorization;

namespace Acme.BookStore.Authorization
{
    public static class PublisherOperations
    {
        public static OperationAuthorizationRequirement Edit = 
            new OperationAuthorizationRequirement { Name = nameof(Edit) };
        public static OperationAuthorizationRequirement Delete = 
            new OperationAuthorizationRequirement { Name = nameof(Delete) };
    }
}

实现授权处理器

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Authorization;
using Volo.Abp.Users;

namespace Acme.BookStore.Authorization
{
    public class PublisherAuthorizationHandler : 
        AuthorizationHandler<OperationAuthorizationRequirement, Publisher>
    {
        protected override Task HandleRequirementAsync(
            AuthorizationHandlerContext context,
            OperationAuthorizationRequirement requirement,
            Publisher resource)
        {
            var userId = context.User.FindUserId();
            if (userId == null)
            {
                context.Fail();
                return Task.CompletedTask;
            }

            // 检查是否是创建者
            if (resource.CreatorId == userId)
            {
                context.Succeed(requirement);
                return Task.CompletedTask;
            }

            // 检查是否有全局编辑权限
            if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Publishers.Edit))
            {
                context.Succeed(requirement);
                return Task.CompletedTask;
            }

            context.Fail();
            return Task.CompletedTask;
        }
    }
}

注册授权处理器

public override void ConfigureServices(ServiceConfigurationContext context)
{
    var services = context.Services;
    
    services.AddSingleton<IAuthorizationHandler<PublisherOperations.Edit, Publisher>, PublisherAuthorizationHandler>();
    services.AddSingleton<IAuthorizationHandler<PublisherOperations.Delete, Publisher>, PublisherAuthorizationHandler>();
}

使用基于资源的授权

public async Task<PublisherDto> UpdateAsync(Guid id, CreateUpdatePublisherDto input)
{
    var publisher = await Repository.GetAsync(id);
    
    // 使用基于资源的授权检查
    await AuthorizationService.CheckAsync(PublisherOperations.Edit, publisher);
    
    // 继续更新...
}

四、客户端权限检查细节

4.1 前端 JavaScript 权限检查

ABP 明确前端可通过 abp.auth.isGranted("权限名") 在 JS 中检查权限。

JavaScript API 使用

// 检查单个权限
const canCreate = abp.auth.isGranted('BookStore.Publishers.Create');
if (canCreate) {
    // 显示创建按钮
    $('#createButton').show();
}

// 检查多个权限
const permissions = [
    'BookStore.Publishers.Create',
    'BookStore.Publishers.Edit',
    'BookStore.Publishers.Delete'
];
const hasAnyPermission = permissions.some(p => abp.auth.isGranted(p));
if (hasAnyPermission) {
    // 显示管理面板
    $('#managementPanel').show();
}

// 检查所有权限
const hasAllPermissions = permissions.every(p => abp.auth.isGranted(p));
if (hasAllPermissions) {
    // 显示完整功能
    $('#fullFeatures').show();
}

React 前端示例

import { usePermissions } from '@abp/ng.core';

function PublisherList() {
    const { isGranted } = usePermissions();
    
    return (
        <div>
            {isGranted('BookStore.Publishers.Create') && (
                <Button>Create Publisher</Button>
            )}
            {isGranted('BookStore.Publishers.Edit') && (
                <Button>Edit</Button>
            )}
            {isGranted('BookStore.Publishers.Delete') && (
                <Button>Delete</Button>
            )}
        </div>
    );
}

Angular 前端示例

import { PermissionService } from '@abp/ng.core';

@Component({
    selector: 'app-publisher-list',
    template: `
        <button *ngIf="permissionService.isGranted('BookStore.Publishers.Create')">
            Create
        </button>
        <button *ngIf="permissionService.isGranted('BookStore.Publishers.Edit')">
            Edit
        </button>
    `
})
export class PublisherListComponent {
    constructor(public permissionService: PermissionService) {}
}

Vue 前端示例

import { usePermission } from '@abp/vue';

export default {
    setup() {
        const { isGranted } = usePermission();
        
        return {
            canCreate: computed(() => isGranted('BookStore.Publishers.Create')),
            canEdit: computed(() => isGranted('BookStore.Publishers.Edit')),
            canDelete: computed(() => isGranted('BookStore.Publishers.Delete'))
        };
    }
};

权限变化监听

// 监听权限变化
abp.event.on('abp.permissionsChanged', () => {
    console.log('Permissions changed!');
    // 重新检查权限并更新 UI
    updateUI();
});

常见问题

Q1: 为什么构造函数中的权限配置"看起来没有被使用"?

A: 这些配置会被 CrudAppService 基类在内部使用。当您设置 CreatePolicyName = BookStorePermissions.Publishers.Create 后,基类会自动为 CreateAsync 方法添加授权检查。这是隐式使用,您看不到直接的调用。

验证方式

// 您可以检查 ABP 源码中的 CrudAppService
// 它内部会调用类似这样的代码:
protected virtual async Task CheckCreatePolicyAsync()
{
    if (CreatePolicyName != null)
    {
        await AuthorizationService.CheckAsync(CreatePolicyName);
    }
}

Q2: [Authorize] 特性应该在接口定义还是实现类中添加?

A: 应该在实现类中添加。

❌ 不推荐

[Authorize(BookStorePermissions.Publishers.Create)]  // 不要在接口中添加
public interface IPublisherAppService
{
    Task<PublisherDto> CreateAsync(CreateUpdatePublisherDto input);
}

✅ 推荐

public interface IPublisherAppService
{
    Task<PublisherDto> CreateAsync(CreateUpdatePublisherDto input);
}

[Authorize(BookStorePermissions.Publishers.Create)]  // 在实现类中添加
public class PublisherAppService : IPublisherAppService
{
    // ...
}

Q3: 如何实现"用户只能编辑自己创建的记录"?

A: 使用基于资源的授权或 AuthorizationService 检查:

public async Task<PublisherDto> UpdateAsync(Guid id, CreateUpdatePublisherDto input)
{
    var publisher = await Repository.GetAsync(id);

    // 方式1: 检查是否是创建者
    if (publisher.CreatorId != CurrentUser.Id)
    {
        // 检查是否有编辑所有记录的权限
        await AuthorizationService.CheckAsync(
            BookStorePermissions.Publishers.Edit);
    }

    // 继续更新...
}

或者使用基于资源的授权(高级):

// 定义操作需求
public static class PublisherOperations
{
    public static OperationAuthorizationRequirement Edit = 
        new OperationAuthorizationRequirement { Name = nameof(Edit) };
}

// 实现授权处理器
public class PublisherAuthorizationHandler : 
    AuthorizationHandler<OperationAuthorizationRequirement, Publisher>
{
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement,
        Publisher resource)
    {
        // 检查是否是创建者
        if (resource.CreatorId == context.User.FindUserId())
        {
            context.Succeed(requirement);
            return;
        }

        // 检查是否有管理权限
        if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Publishers.Edit))
        {
            context.Succeed(requirement);
            return;
        }
    }
}

// 在应用服务中使用
public async Task<PublisherDto> UpdateAsync(Guid id, CreateUpdatePublisherDto input)
{
    var publisher = await Repository.GetAsync(id);
    
    // 基于资源的授权检查
    await AuthorizationService.CheckAsync(publisher, PublisherOperations.Edit);
    
    // 继续更新...
}

Q4: CheckAsyncIsGrantedAsync 有什么区别?

方法 返回值 无权限时的行为 适用场景
CheckAsync Task 抛出 AbpAuthorizationException 需要强制权限检查
IsGrantedAsync Task<bool> 返回 false 需要条件性逻辑

示例

// CheckAsync: 无权限会抛出异常
public async Task<string> GetStatisticsAsync()
{
    await AuthorizationService.CheckAsync(BookStorePermissions.Publishers.Default);
    // 如果没有权限,不会执行到这里
    return "...";
}

// IsGrantedAsync: 无权限返回 false
public async Task<PublisherDto> GetWithPermissionsAsync(Guid id)
{
    var publisher = await Repository.GetAsync(id);
    var dto = ObjectMapper.Map<Publisher, PublisherDto>(publisher);
    
    // 不会抛出异常
    dto.CanEdit = await AuthorizationService.IsGrantedAsync(
        BookStorePermissions.Publishers.Edit);
    
    return dto;
}

Q5: 如何调试权限问题?

方法1: 查看日志

[INF] Authorization: Checking permission: BookStore.Publishers.Create
[WRN] Authorization: Permission 'BookStore.Publishers.Create' was not granted for user: john.doe

方法2: 使用 Postman 测试

# 测试不同的用户/token
curl -X POST "https://localhost:44300/api/app/publisher" \
  -H "Authorization: Bearer TOKEN_1"

curl -X POST "https://localhost:44300/api/app/publisher" \
  -H "Authorization: Bearer TOKEN_2"

方法3: 在代码中添加日志

public async Task<PublisherDto> CreateAsync(CreateUpdatePublisherDto input)
{
    Logger.LogInformation($"用户 {CurrentUser.UserName} 尝试创建出版商");
    
    var hasPermission = await AuthorizationService.IsGrantedAsync(
        BookStorePermissions.Publishers.Create);
    
    Logger.LogInformation($"用户是否有创建权限: {hasPermission}");
    
    // ...
}

Q6: 权限检查会影响性能吗?

A: 权限检查通常很快,因为:

  1. 权限信息缓存在内存中
  2. 用户的权限列表在登录时加载
  3. 使用高效的哈希表查找

优化建议

// ❌ 不推荐:在循环中重复检查
public async Task ProcessPublishersAsync(List<Guid> ids)
{
    foreach (var id in ids)
    {
        // 每次循环都检查权限(不必要)
        await AuthorizationService.CheckAsync(BookStorePermissions.Publishers.Edit);
        // ...
    }
}

// ✅ 推荐:在循环外检查一次
public async Task ProcessPublishersAsync(List<Guid> ids)
{
    // 只检查一次
    await AuthorizationService.CheckAsync(BookStorePermissions.Publishers.Edit);
    
    foreach (var id in ids)
    {
        // 不需要重复检查
        // ...
    }
}

权限检查流程图

用户请求 API 方法
         ↓
    方法被调用
         ↓
   ┌─────────────────┐
   │ 方式选择        │
   └─────────────────┘
         ↓
   ┌────────┴────────┐
   │                 │
隐式方式          显式方式
(CrudAppService)   ([Authorize] 或 AuthorizationService)
   │                 │
   ↓                 ↓
检查属性值        检查特性或手动调用
   │                 │
   └────────┬────────┘
            ↓
   检查用户是否有权限
            ↓
      ┌─────┴─────┐
      │           │
   有权限      无权限
      │           │
      ↓           ↓
 执行方法    返回 403 Forbidden
      │
      ↓
   返回结果

📚 相关资源


✅ 总结

通过本指南,您已经学会了:

  • ✅ 如何定义和注册权限
  • ✅ 权限的三种使用方式及其适用场景
  • ✅ 如何选择合适的授权方式
  • ✅ 完整的代码实现示例
  • ✅ 如何测试和调试权限问题
  • ✅ 最佳实践和常见问题解答

关键要点

  1. 隐式使用 适合标准 CRUD 操作
  2. [Authorize] 特性 适合自定义 API 方法
  3. AuthorizationService 最灵活,适合复杂场景
  4. 根据实际需求选择合适的方式
  5. 为权限添加本地化,提升用户体验

现在您的Publishers模块已经拥有完整的权限控制!🎉


文档版本: v1.0
最后更新: 2026-01-09
适用版本: ABP v8.1

posted @ 2026-01-10 13:24  【唐】三三  阅读(17)  评论(0)    收藏  举报