做过后端的比较熟悉,CAP面板有个界面,可以通过域名加cap访问:
但是这个面板直接通过url就可以访问了。
Hangfire Dashboard有自己的面板,可以使用用户名和密码做简单的认证。
LogDashboard也有自己的面板,可以使用用户名和密码做简单的认证。
如下图:
但是CAP的面板是裸露的,没有直接的认证功能。
官方提供了文档,但是没有简单的用户名和密码的认证示例。
https://cap.dotnetcore.xyz/user-guide/zh/monitoring/dashboard/
简单到不知道它在表达什么:
于是只能自己摸索了。
CAP面板引用的包:
DotNetCore.CAP.Dashboard
认证需要另外引用一个包:
Microsoft.AspNetCore.Authentication
然后创建自己的认证处理器:
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Extensions
{
/// <summary>
/// 自定义面板认证
/// </summary>
public static class MyAuthDefaults
{
/// <summary>
/// 自定义面板认证-协议名
/// </summary>
public const string Policy = "MyAuthPolicy";
/// <summary>
/// 自定义面板认证-方案名
/// </summary>
public const string Scheme = "MyAuthScheme";
}
/// <summary>
/// 自定义面板认证配置
/// </summary>
public class MyAuthSchemeOptions : AuthenticationSchemeOptions { }
/// <summary>
/// 自定义面板认证处理器
/// </summary>
public class MyAuthHandler : AuthenticationHandler<MyAuthSchemeOptions>
{
/// <summary>
/// 自定义面板认证处理器
/// </summary>
public MyAuthHandler(IOptionsMonitor<MyAuthSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
/// <summary>
/// 自定义面板认证-验证
/// </summary>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
AuthenticateResult authResult = null;
if (!Request.Headers.ContainsKey("Authorization"))
{
authResult = AuthenticateResult.NoResult();
return await Task.FromResult(authResult);
}
try
{
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
var credentialsBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialsBytes).Split(':');
var username = credentials[0];
var password = credentials[1];
if (username != "admin" || password != "123456")
{
authResult = AuthenticateResult.Fail("Invalid Username or Password");
return await Task.FromResult(authResult);
}
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, username),
new Claim(ClaimTypes.Name, username),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
authResult = AuthenticateResult.Success(ticket);
return await Task.FromResult(authResult);
}
catch (Exception ex)
{
authResult = AuthenticateResult.Fail("Invalid Authorization Header");
return await Task.FromResult(authResult);
}
}
/// <summary>
/// 自定义面板认证-变动
/// </summary>
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{Scheme.Name}\", charset=\"UTF-8\"";
return base.HandleChallengeAsync(properties);
}
}
}
代码里面设置了默认账号:
admin/123456
如果账号来自config配置,则需要做如下修改:
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Extensions
{
/// <summary>
/// 自定义面板认证
/// </summary>
public static class MyAuthDefaults
{
/// <summary>
/// 自定义面板认证-协议名
/// </summary>
public const string Policy = "MyAuthPolicy";
/// <summary>
/// 自定义面板认证-方案名
/// </summary>
public const string Scheme = "MyAuthScheme";
}
/// <summary>
/// 自定义面板认证配置
/// </summary>
public class MyAuthSchemeOptions : AuthenticationSchemeOptions { }
/// <summary>
/// 自定义面板认证处理器
/// </summary>
public class MyAuthHandler : AuthenticationHandler<MyAuthSchemeOptions>
{
private readonly IConfiguration _configuration;
/// <summary>
/// 自定义面板认证处理器
/// </summary>
public MyAuthHandler(IOptionsMonitor<MyAuthSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IConfiguration configuration)
: base(options, logger, encoder, clock)
{
_configuration = configuration;
}
/// <summary>
/// 自定义面板认证-验证
/// </summary>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
AuthenticateResult authResult = null;
if (!Request.Headers.ContainsKey("Authorization"))
{
authResult = AuthenticateResult.NoResult();
return await Task.FromResult(authResult);
}
try
{
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
var credentialsBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialsBytes).Split(':');
var username = credentials[0];
var password = credentials[1];
var checkUser = _configuration["Auth:User"];
var checkPwd = _configuration["Auth:Pwd"];
if (username != checkUser || password != checkPwd)
{
authResult = AuthenticateResult.Fail("Invalid Username or Password");
return await Task.FromResult(authResult);
}
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, username),
new Claim(ClaimTypes.Name, username),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
authResult = AuthenticateResult.Success(ticket);
return await Task.FromResult(authResult);
}
catch (Exception ex)
{
authResult = AuthenticateResult.Fail("Invalid Authorization Header");
return await Task.FromResult(authResult);
}
}
/// <summary>
/// 自定义面板认证-变动
/// </summary>
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{Scheme.Name}\", charset=\"UTF-8\"";
return base.HandleChallengeAsync(properties);
}
}
}
然后创建一个异常处理中间件里面,里面专门针对cap面板的路径做处理:
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Extensions;
namespace Middleware
{
/// <summary>
/// 全局异常处理 中间件
/// </summary>
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate next;
private readonly ILogger logger;
/// <summary>
/// 全局异常处理 中间件
/// </summary>
public ExceptionHandlerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
this.next = next;
logger = loggerFactory.CreateLogger("globeException");
}
/// <summary>
/// 全局异常处理 中间件
/// </summary>
public async Task Invoke(HttpContext context)
{
try
{
//CAP面板认证
if (context.Request.Path.ToString().ToLower().Contains("/cap/"))
{
var authResult = await context.AuthenticateAsync(MyAuthDefaults.Scheme);
if (authResult.Succeeded == false) //认证失败
{
await context.ChallengeAsync(MyAuthDefaults.Scheme);
return;
}
context.User = authResult.Principal; //成功后用户信息返回
await next(context); // 如果需要继续处理请求,可以调用下一个中间件
return;
}
else
{
await next(context);
}
}
catch (Exception ex)
{
string exStr = ex.Message;
}
}
}
}
添加针对面板的扩展:
using Extensions;
namespace Extensions
{
public static class CapDashboardExtensions
{
/// <summary>
/// 带认证的面板
/// </summary>
public static void AddCapDashboard(this IServiceCollection services, WebApplicationBuilder builder)
{
services.AddAuthentication(MyAuthDefaults.Scheme)
.AddScheme<MyAuthSchemeOptions, MyAuthHandler>(MyAuthDefaults.Scheme, options => { });
services.AddAuthorization(options =>
{
options.AddPolicy(MyAuthDefaults.Policy, policy =>
{
policy.AddAuthenticationSchemes(MyAuthDefaults.Scheme).RequireAuthenticatedUser();
});
});
services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.AllowCredentials().AllowAnyHeader().AllowAnyMethod();
});
});
services.AddCap(options =>
{
options.FailedRetryInterval = 60;
options.FailedRetryCount = 50;
//需要认证
options.UseDashboard(d =>
{
d.PathMatch = "/Cap";
d.UseChallengeOnAuth = true;
d.DefaultChallengeScheme = MyAuthDefaults.Scheme;
});
});
}
}
}
最后在Program里面加上面板的扩展:
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services.AddCapDashboard(builder); //添加Cap面板
var app = builder.Build();
app.UseCors();
app.UseAuthentication(); // 启用认证中间件
app.UseAuthorization();
然后项目启动后,域名加上cap:
http://127.0.0.1:80/cap
就可以进入认证界面了:
大功搞成!输入代码里的账号,就可以进入CAP面板了:
因为网上找不到简单的CAP认证实现功能,所以这个功能摸索着开发了好久。
创作不易,给点鼓励,谢谢!