1. 程式人生 > 實用技巧 >HangFire多叢集切換及DashBoard登入驗證

HangFire多叢集切換及DashBoard登入驗證

專案中是有多個叢集的,現在存在一個是:在切換web叢集時,如何切換HangFire的週期性任務。

先採取的解決辦法是:

  • 每個叢集分一個佇列,在週期性任務入隊時分配當前web叢集的叢集id單做佇列名稱。
  • 之前已存在的週期性任務,在其入隊時修正到正確的叢集執行

通過BackgroundJobServerOptions配置,只監聽當前web叢集的佇列(ps:可參考檔案:https://docs.hangfire.io/en/latest/background-processing/configuring-queues.html

 //只監聽當前叢集的佇列
var options = new BackgroundJobServerOptions()
{
Queues = new[] { GlobalConfigSection.Current.WebClusterId }
};
HangfireAspNet.Use(() => new[] { new BackgroundJobServer(options) });

通過實現IElectStateFilter,重寫OnStateElection方法,在任務入隊前修正其入當前叢集佇列執行。

配置使用自定義熟悉

GlobalJobFilters.Filters.Add(new CustomJobFilterAttribute());

重寫OnStateElection方法,通過修改enqueuedState的queue熟悉修正佇列

 /// <summary>
/// HangFire Filter
/// </summary>
public class CustomJobFilterAttribute : JobFilterAttribute, IElectStateFilter
{
public void OnStateElection(ElectStateContext context)
{
if (context.CandidateState is EnqueuedState enqueuedState)
{
var tenantConfigProvider = ObjectContainer.GetService<ITenantConfigProvider>();
var contextMessage = context.GetJobParameter<ContextMessage>("_ld_contextMessage");
var webClusterId = tenantConfigProvider.GetWebClusterIdByTenant(contextMessage.TenantId);
if (enqueuedState.Queue != webClusterId)//修正佇列
{
enqueuedState.Queue = webClusterId;
}
}
} }

ps(更多的filter可以參考檔案:https://docs.hangfire.io/en/latest/extensibility/using-job-filters.html)

附上HangFire執行失敗記錄日誌實現

  /// <summary>
/// HangFire Filter
/// </summary>
public class CustomJobFilterAttribute : JobFilterAttribute, IServerFilter
{ public void OnPerforming(PerformingContext filterContext)
{ } /// <summary>
/// 未成功執行的
/// </summary>
/// <param name="filterContext"></param>
public void OnPerformed(PerformedContext filterContext)
{
var error = filterContext.Exception;
if (error==null)
{
return;
}
//記錄日誌到後臺
ILoggerFactory loggerFactory = ObjectContainer.GetService<ILoggerFactory>();
ILogger logger;
if (error.TargetSite != null && error.TargetSite.DeclaringType != null)
{
logger = loggerFactory.Create(error.TargetSite.DeclaringType.GetUnProxyType());
}
else
{
logger = loggerFactory.Create(GetType());
}
var contextMessage = filterContext.GetJobParameter<ContextMessage>("_ld_contextMessage");
var message = GetLogMessage(contextMessage, error.ToString(), filterContext.BackgroundJob.Id); var logLevel = ErrorLevelType.Fatal; if (error.InnerException is AppException ex)
{
logLevel = ex.ErrorLevel;
} switch (logLevel)
{
case ErrorLevelType.Info:
logger.Info(message, error);
break;
case ErrorLevelType.Warning:
logger.Warn(message, error);
break;
case ErrorLevelType.Error:
logger.Error(message, error);
break;
default:
logger.Fatal(message, error);
break;
}
} /// <summary>
/// 獲取當前日誌物件
/// </summary>
/// <returns></returns>
private LogMessage GetLogMessage(ContextMessage contextMessage, string errorMessage, string backgroundJobId)
{
var logMessage = new LogMessage(contextMessage, errorMessage)
{
RawUrl = backgroundJobId
};
return logMessage;
} }

現在還有一個問題是,HangFire DashBoard 預設只支援localhost訪問,現在我需要可以很方便的在外網通過web叢集就能訪問到其對應的HangFire DashBoard。

通過檔案https://docs.hangfire.io/en/latest/configuration/using-dashboard.html,可以知道其提供了一個登入驗證的實現,但是由於其是直接寫死了密碼在程式碼中的,覺得不好。(ps:具體的實現可以參考:https://gitee.com/LucasDot/Hangfire.Dashboard.Authorizationhttps://www.cnblogs.com/lightmao/p/7910139.html

我實現的思路是,在web叢集介面直接開啟標籤頁訪問。在web集群后臺生成token並在url中攜帶,在hangfire站點中校驗token,驗證通過則放行。同時校驗url是否攜帶可修改的引數,如果有的話設定IsReadOnlyFunc放回false。(ps:可參考檔案:https://docs.hangfire.io/en/latest/configuration/using-dashboard.html

在startup頁面配置使用dashboard,通過DashboardOptions選擇配置我們自己實現的身份驗證,以及是否只讀設定。

 public void Configuration(IAppBuilder app)
{
try
{ Bootstrapper.Instance.Start(); var dashboardOptions = new DashboardOptions()
{
Authorization = new[] { new HangFireAuthorizationFilter() },
IsReadOnlyFunc = HangFireIsReadOnly
};
app.UseHangfireDashboard("/hangfire", dashboardOptions); }
catch (Exception ex)
{
Debug.WriteLine(ex);
} } /// <summary>
/// HangFire儀表盤是否只讀
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private bool HangFireIsReadOnly(DashboardContext context)
{
var owinContext = new OwinContext(context.GetOwinEnvironment());
if (owinContext.Request.Host.ToString().StartsWith("localhost"))
{
return false;
} try
{
var cookie = owinContext.Request.Cookies["Ld.HangFire"];
char[] spilt = { ',' };
var userData = FormsAuthentication.Decrypt(cookie)?.UserData.Split(spilt, StringSplitOptions.RemoveEmptyEntries);
if (userData != null)
{
var isAdmin = userData[].Replace("isAdmin:", "");
return !bool.Parse(isAdmin);
}
}
catch (Exception e)
{ } return true;
}

在HangFireAuthorizationFilter的具體實現中,先校驗是否已存在驗證後的cookie如果有就不再驗證,或者如果是通過localhost訪問也不進行校驗,否則驗證簽名是否正確,如果正確就將資訊寫入cookie。

 /// <summary>
/// HangFire身份驗證
/// </summary>
public class HangFireAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var owinContext = new OwinContext(context.GetOwinEnvironment()); if (owinContext.Request.Host.ToString().StartsWith("localhost")|| owinContext.Request.Cookies["Ld.HangFire"] != null)//通過localhost訪問不校驗,與cookie也不校驗
{
return true;
} var cluster = owinContext.Request.Query.Get("cluster");//叢集名稱
var isAdminS = owinContext.Request.Query.Get("isAdmin");//是否管理員(是則允許修改hangfire)
var token = owinContext.Request.Query.Get("token");
var t = owinContext.Request.Query.Get("t");//時間戳
if (!string.IsNullOrEmpty(token) && bool.TryParse(isAdminS, out var isAdmin) && long.TryParse(t, out var timestamp))
{
try
{
var isValid = LicenceHelper.ValidSignature($"{cluster}_{isAdmin}", token, timestamp, TimeSpan.FromMinutes());//五分鐘有效
if (isValid)
{
var ticket = new FormsAuthenticationTicket(, cluster, DateTime.Now, DateTime.Now + FormsAuthentication.Timeout, false, $"isAdmin:{isAdmin}");
var authToken = FormsAuthentication.Encrypt(ticket); owinContext.Response.Cookies.Append("Ld.HangFire", authToken, new CookieOptions()
{
HttpOnly = true,
Path = "/hangfire"
});
return true;
}
}
catch (Exception ex)
{ }
}
return false; } }

在web的管理後臺具體實現為,同選中叢集點選後臺任務直接訪問改叢集的HangFire DashBoard

點選後臺任務按鈕,後臺放回token相關資訊,然後js開啟一個新的標籤頁展示dashboard

 public ActionResult GetHangFireToken(string clusterName)
{
var isAdmin=WorkContext.User.IsAdmin;
var timestamp = DateTime.UtcNow.Ticks;
var token = LicenceHelper.Signature($"{clusterName}_{isAdmin}", timestamp);
return this.Direct(new
{
isAdmin,
token,
timestamp=timestamp.ToString()
});
}
 openHangFire:function() {
var me = this, rows = me.getGridSelection('檢視後臺任務的叢集', true);
if (!rows) {
return;
}
if (rows.length > 1) {
me.alert('只能選擇一行');
return;
}
var clusterName = rows[0].get('ClusterName');
var opts = {
actionName: 'GetHangFireToken',
extraParams: {
clusterName: clusterName
},
success: function (result) {
var data = result.result;
var url = Ext.String.format("{0}/hangfire?cluster={1}&isAdmin={2}&token={3}&t={4}",
rows[0].get("AccessUrl"),
clusterName,
data.isAdmin,
data.token,
data.timestamp);
window.open(url, clusterName);
}
};
me.directRequest(opts);
}