项目

Hangfire 背景作业管理器

Hangfire 是一款先进的背景作业管理器。您可以将 Hangfire 与 ABP 框架集成,用以替代 默认的背景作业管理器 。通过这种方式,您可以使用统一的背景作业 API,使代码与 Hangfire 解耦。当然,您也可以直接使用 Hangfire 的原生 API。

关于如何使用背景作业系统,请参阅 背景作业文档 。本文档仅展示如何安装和配置 Hangfire 集成。

安装

建议使用 ABP CLI 安装此包。

使用 ABP CLI

在项目文件夹(.csproj 文件所在目录)打开命令行窗口,输入以下命令:

abp add-package Volo.Abp.BackgroundJobs.HangFire

如果您尚未安装 ABP CLI,请先进行安装。关于其他安装方式,请参阅包详情页面

手动安装

如需手动安装:

  1. Volo.Abp.BackgroundJobs.HangFire NuGet 包添加到您的项目:

    dotnet add package Volo.Abp.BackgroundJobs.HangFire
    
  2. 在模块的依赖列表中添加 AbpBackgroundJobsHangfireModule

[DependsOn(
    //...其他依赖
    typeof(AbpBackgroundJobsHangfireModule) //添加新模块依赖
    )]
public class YourModule : AbpModule
{
}

配置

您可以为 Hangfire 安装任意存储方式,最常见的是 SQL Server(参见 Hangfire.SqlServer NuGet 包)。

安装这些 NuGet 包后,需配置项目以使用 Hangfire。

  1. 首先,修改 Module 类(例如:<YourProjectName>HttpApiHostModule),在 ConfigureServices 方法中添加 Hangfire 的存储和连接字符串配置:
  public override void ConfigureServices(ServiceConfigurationContext context)
  {
      var configuration = context.Services.GetConfiguration();
      var hostingEnvironment = context.Services.GetHostingEnvironment();

      //... 其他配置

      ConfigureHangfire(context, configuration);
  }

  private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
  {
      context.Services.AddHangfire(config =>
      {
          config.UseSqlServerStorage(configuration.GetConnectionString("Default"));
      });
  }

必须为 Hangfire 配置存储。

  1. 如需使用 Hangfire 仪表板,可在 Module 类的 OnApplicationInitialization 方法中添加 UseAbpHangfireDashboard 调用:
 public override void OnApplicationInitialization(ApplicationInitializationContext context)
 {
    var app = context.GetApplicationBuilder();
            
    // ... 其他中间件
    
    app.UseAbpHangfireDashboard(); //应在 app.UseConfiguredEndpoints() 之前添加到请求管道
    app.UseConfiguredEndpoints();
 }

AbpHangfireOptions

您可以通过配置 AbpHangfireOptions 中的 BackgroundJobServerOptions 来自定义服务器:

Configure<AbpHangfireOptions>(options =>
{
    // 若不设置 ServerOptions,ABP 将使用默认的 BackgroundJobServerOptions 实例
    options.ServerOptions = new BackgroundJobServerOptions
    {
        Queues = ["default", "alpha"],
        //... 其他属性
    };
});

无需调用 AddHangfireServer 方法,ABP 将使用 AbpHangfireOptions 的 ServerOptions 创建服务器。

指定队列

可使用 QueueAttribute 指定队列:

using System.Threading.Tasks;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing;

namespace MyProject
{
    [Queue("alpha")]
    public class EmailSendingJob
        : AsyncBackgroundJob<EmailSendingArgs>, ITransientDependency
    {
        private readonly IEmailSender _emailSender;

        public EmailSendingJob(IEmailSender emailSender)
        {
            _emailSender = emailSender;
        }

        public override async Task ExecuteAsync(EmailSendingArgs args)
        {
            await _emailSender.SendAsync(
                args.EmailAddress,
                args.Subject,
                args.Body
            );
        }
    }
}

仪表板授权

Hangfire 仪表板提供背景作业的详细信息,包括方法名称和序列化参数,并允许执行重试、删除、触发等管理操作。因此,限制对仪表板的访问至关重要。 默认情况下仅允许本地请求,但您可参照 Hangfire 的官方文档进行调整。

通过 AbpHangfireAuthorizationFilter 类,可将 Hangfire 仪表板集成到 ABP 授权系统。该类定义于 Volo.Abp.Hangfire 包中。以下示例检查当前用户是否已登录应用:

app.UseAbpHangfireDashboard("/hangfire", options =>
{
    options.AsyncAuthorization = new[] { new AbpHangfireAuthorizationFilter() };
});
  • AbpHangfireAuthorizationFilter 是授权过滤器的实现。

AbpHangfireAuthorizationFilter

AbpHangfireAuthorizationFilter 类包含以下字段:

  • enableTenantbool,默认:false): 启用/禁用租户用户访问 Hangfire 仪表板。
  • requiredPermissionNamestring,默认:null): 仅当当前用户拥有指定权限时才可访问仪表板。
  • requiredRoleNamesstring[],默认:[]): 仅当当前用户拥有指定角色之一时才可访问仪表板。

如需更多策略,可使用 AbpHangfireAuthorizationFilter 类的 PolicyBuilder 属性:

app.UseAbpHangfireDashboard("/hangfire", options =>
{
    var hangfireAuthorizationFilter = new AbpHangfireAuthorizationFilter(requiredPermissionName: "MyHangFireDashboardPermissionName");

    //hangfireAuthorizationFilter.PolicyBuilder.AddRequirements(new PermissionRequirement("YourPermissionName"));
    //hangfireAuthorizationFilter.PolicyBuilder.RequireRole("YourCustomRole");
    //hangfireAuthorizationFilter.PolicyBuilder.Requirements.Add(new YourCustomRequirement());

    options.AsyncAuthorization = new[]
    {
        hangfireAuthorizationFilter
    };
});

重要提示UseAbpHangfireDashboard 应在 Startup 类中的认证和授权中间件之后调用(通常位于最后一行),否则授权将始终失败!

API 项目中的仪表板授权

若在采用非 Cookie 认证(如 JWT Bearer)的 API 项目中使用 Hangfire 仪表板,/hangfire 页面无法认证用户。

此时,可添加 Cookie 授权方案进行用户认证。最佳实践是结合使用 CookieOpenIdConnect 认证方案,这需要创建新的 OAuth2 客户端并在 appsettings.json 文件的 AuthServer 部分添加 ClientIdClientSecret 属性。

最终代码示例如下:

private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddAbpJwtBearer(options =>
        {
            options.Authority = configuration["AuthServer:Authority"];
            options.RequireHttpsMetadata = configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata");
            options.Audience = "MyProjectName";

            options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase)
                ? CookieAuthenticationDefaults.AuthenticationScheme
                : null;
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            options.Authority = configuration["AuthServer:Authority"];
            options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
            options.ResponseType = OpenIdConnectResponseType.Code;

            options.ClientId = configuration["AuthServer:HangfireClientId"];
            options.ClientSecret = configuration["AuthServer:HangfireClientSecret"];

            options.UsePkce = true;
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;

            options.Scope.Add("roles");
            options.Scope.Add("email");
            options.Scope.Add("phone");
            options.Scope.Add("MyProjectName");

            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        });
}
app.Use(async (httpContext, next) =>
{
    if (httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase))
    {
        var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        if (!authenticateResult.Succeeded)
        {
            await httpContext.ChallengeAsync(
                OpenIdConnectDefaults.AuthenticationScheme,
                new AuthenticationProperties
                {
                    RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString
                });
            return;
        }
    }
    await next.Invoke();
});
app.UseAbpHangfireDashboard("/hangfire", options =>
{
    options.AsyncAuthorization = new[]
    {
        new AbpHangfireAuthorizationFilter()
    };
});
在本文档中