using System.Net;
using System.Security.Claims;
using Dpz.Core.EnumLibrary;
using Microsoft.AspNetCore.Authentication;
using OpenIddict.Client.AspNetCore;
namespace Dpz.Core.Web.Jobs.Security;
/// <summary>
/// Hangfire Dashboard 访问控制中间件:对 Dashboard 路径进行认证和权限校验。
/// </summary>
/// <param name="next">下一个中间件。</param>
/// <param name="webHostEnvironment">宿主环境,用于读取静态文件。</param>
public class HangfireDashboardAccessMiddleware(
RequestDelegate next,
IWebHostEnvironment webHostEnvironment
)
{
private const string ForbiddenPagePath = "template/HangfireForbidden.html";
/// <summary>
/// 中间件入口:仅对 Dashboard 路径生效,依次执行认证检查、权限检查。
/// </summary>
/// <param name="context">当前 HTTP 上下文。</param>
public async Task InvokeAsync(HttpContext context)
{
// 非 Dashboard 路径直接放行
if (!context.Request.Path.StartsWithSegments(Program.ConsoleUrl))
{
await next(context);
return;
}
// 未登录 → 跳转 OpenIddict 认证
if (context.User.Identity?.IsAuthenticated != true)
{
await ChallengeAsync(context);
return;
}
// 无 System 权限 → 返回 403 禁止页面
if (!HangfireDashboardPermission.HasPermission(context.User, Permissions.System))
{
await WriteForbiddenPageAsync(context, webHostEnvironment);
return;
}
await next(context);
}
private static Task ChallengeAsync(HttpContext context)
{
var returnUrl =
context.Request.PathBase + context.Request.Path + context.Request.QueryString;
var properties = new AuthenticationProperties { RedirectUri = returnUrl };
return context.ChallengeAsync(
OpenIddictClientAspNetCoreDefaults.AuthenticationScheme,
properties
);
}
private static async Task WriteForbiddenPageAsync(
HttpContext context,
IWebHostEnvironment webHostEnvironment
)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = "text/html; charset=utf-8";
context.Response.Headers.CacheControl = "no-store";
var displayName = context.User.FindFirst("displayName")?.Value;
var accountLabel = string.IsNullOrWhiteSpace(displayName)
? "当前账号"
: WebUtility.HtmlEncode(displayName);
var fileInfo = webHostEnvironment.WebRootFileProvider.GetFileInfo(ForbiddenPagePath);
if (!fileInfo.Exists)
{
await context.Response.WriteAsync("没有对应的权限");
return;
}
await using var stream = fileInfo.CreateReadStream();
using var reader = new StreamReader(stream);
var html = await reader.ReadToEndAsync(context.RequestAborted);
// 将模板中的 {{accountLabel}} 占位符替换为当前用户名
await context.Response.WriteAsync(
html.Replace("{{accountLabel}}", accountLabel, StringComparison.Ordinal),
context.RequestAborted
);
}
}
/// <summary>
/// <see cref="HangfireDashboardAccessMiddleware"/> 的 IApplicationBuilder 扩展方法。
/// </summary>
public static class HangfireDashboardAccessMiddlewareExtensions
{
/// <summary>
/// 注册 Hangfire Dashboard 访问控制中间件。
/// </summary>
/// <param name="app">应用构建器。</param>
public static IApplicationBuilder UseHangfireDashboardAccess(this IApplicationBuilder app)
{
return app.UseMiddleware<HangfireDashboardAccessMiddleware>();
}
}
/// <summary>
/// Hangfire Dashboard 权限校验工具类,基于 Claims 中的 Permission 声明做位运算判断。
/// </summary>
public static class HangfireDashboardPermission
{
/// <summary>
/// 检查用户是否拥有指定权限(位掩码匹配)。
/// </summary>
/// <param name="user">当前用户主体。</param>
/// <param name="permission">需要校验的权限位。</param>
/// <returns>用户已认证且拥有该权限时返回 <c>true</c>。</returns>
public static bool HasPermission(ClaimsPrincipal user, Permissions permission)
{
var permissionClaim = user.FindFirst("Permission")?.Value;
return !string.IsNullOrWhiteSpace(permissionClaim)
&& Enum.TryParse<Permissions>(permissionClaim, ignoreCase: true, out var permissions)
&& (permissions & permission) == permission;
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个用于保护 Hangfire Dashboard 访问的 ASP.NET Core 中间件实现,提供了基于 OpenIddict 的身份认证和基于 Claims 的权限授权功能。
核心组件
1. HangfireDashboardAccessMiddleware - 访问控制中间件
这是主要的中间件类,负责拦截对 Hangfire Dashboard 的请求并执行安全检查。
构造函数参数:
next: 管道中的下一个中间件webHostEnvironment: 用于访问静态文件(如 403 禁止页面模板)
核心逻辑(InvokeAsync 方法):
请求流程:
┌─────────────────────────────────────┐
│ 1. 检查路径是否为 Dashboard 路径 │
│ (Program.ConsoleUrl) │
└──────────┬──────────────────────────┘
│ 否 → 直接放行
│ 是 ↓
┌──────────▼──────────────────────────┐
│ 2. 检查用户是否已登录 │
└──────────┬──────────────────────────┘
│ 否 → 跳转 OpenIddict 登录
│ 是 ↓
┌──────────▼──────────────────────────┐
│ 3. 检查是否拥有 System 权限 │
└──────────┬──────────────────────────┘
│ 否 → 返回 403 禁止页面
│ 是 ↓
┌──────────▼──────────────────────────┐
│ 4. 允许访问 Dashboard │
└─────────────────────────────────────┘
2. ChallengeAsync - 认证挑战方法
当用户未登录时,触发 OpenIddict 认证流程:
- 保存当前请求的完整 URL(
PathBase+Path+QueryString) - 设置为认证成功后的返回地址
- 调用 OpenIddict 客户端认证方案
3. WriteForbiddenPageAsync - 403 页面渲染
当用户已登录但无权限时:
- 设置 HTTP 状态码为 403
- 从
wwwroot/template/HangfireForbidden.html读取模板 - 提取用户的
displayNameClaim - 将模板中的
{{accountLabel}}占位符替换为用户名 - 返回个性化的禁止访问页面
安全细节:
- 使用
WebUtility.HtmlEncode防止 XSS 攻击 - 设置
Cache-Control: no-store防止页面被缓存
4. HangfireDashboardPermission - 权限校验工具类
提供静态方法 HasPermission 进行位运算权限检查:
// 权限检查逻辑:
// 1. 从用户 Claims 中提取 "Permission" 值
// 2. 尝试解析为 Permissions 枚举(忽略大小写)
// 3. 使用位与运算检查是否包含指定权限
(permissions & permission) == permission
示例:
// 假设用户权限为:Permissions.Read | Permissions.System = 0b11
// 检查 Permissions.System (0b10)
// 0b11 & 0b10 = 0b10 → 匹配成功
5. HangfireDashboardAccessMiddlewareExtensions - 扩展方法
提供便捷的注册方法:
app.UseHangfireDashboardAccess();
使用场景
在 Program.cs 或 Startup.cs 中:
app.UseAuthentication();
app.UseAuthorization();
app.UseHangfireDashboardAccess(); // 必须在认证授权之后
app.UseHangfireDashboard(Program.ConsoleUrl);
依赖技术栈
- OpenIddict: 提供 OAuth 2.0/OpenID Connect 认证
- Claims-Based Authorization: 基于声明的权限模型
- Enum Flags Pattern: 使用枚举位标志进行权限控制(
Permissions枚举需标记[Flags]) - ASP.NET Core Middleware Pipeline: 标准中间件模式
安全特性
- ✅ 认证隔离:未登录用户无法访问
- ✅ 精细权限控制:支持位运算的组合权限
- ✅ XSS 防护:用户名经过 HTML 编码
- ✅ 缓存控制:禁止缓存敏感页面
- ✅ 友好体验:提供定制化的 403 页面
AI 正在分析代码…
评论加载中...