HangFire多集群切换及DashBoard登录验证
Posted cyril-hcj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HangFire多集群切换及DashBoard登录验证相关的知识,希望对你有一定的参考价值。
项目中是有多个集群的,现在存在一个是:在切换web集群时,如何切换HangFire的周期性任务。
先采取的解决办法是:
- 每个集群分一个队列,在周期性任务入队时分配当前web集群的集群id单做队列名称。
- 之前已存在的周期性任务,在其入队时修正到正确的集群执行
通过BackgroundJobServerOptions配置,只监听当前web集群的队列(ps:可参考文档:https://docs.hangfire.io/en/latest/background-processing/configuring-queues.html)
1 //只监听当前集群的队列 2 var options = new BackgroundJobServerOptions() 3 { 4 Queues = new[] { GlobalConfigSection.Current.WebClusterId } 5 }; 6 HangfireAspNet.Use(() => new[] { new BackgroundJobServer(options) });
通过实现IElectStateFilter,重写OnStateElection方法,在任务入队前修正其入当前集群队列执行。
配置使用自定义属性
GlobalJobFilters.Filters.Add(new CustomJobFilterAttribute());
重写OnStateElection方法,通过修改enqueuedState的queue熟悉修正队列
1 /// <summary> 2 /// HangFire Filter 3 /// </summary> 4 public class CustomJobFilterAttribute : JobFilterAttribute, IElectStateFilter 5 { 6 public void OnStateElection(ElectStateContext context) 7 { 8 if (context.CandidateState is EnqueuedState enqueuedState) 9 { 10 var tenantConfigProvider = ObjectContainer.GetService<ITenantConfigProvider>(); 11 var contextMessage = context.GetJobParameter<ContextMessage>("_ld_contextMessage"); 12 var webClusterId = tenantConfigProvider.GetWebClusterIdByTenant(contextMessage.TenantId); 13 if (enqueuedState.Queue != webClusterId)//修正队列 14 { 15 enqueuedState.Queue = webClusterId; 16 } 17 } 18 } 19 20 }
ps(更多的filter可以参考文档:https://docs.hangfire.io/en/latest/extensibility/using-job-filters.html)
附上HangFire执行失败记录日志实现
1 /// <summary> 2 /// HangFire Filter 3 /// </summary> 4 public class CustomJobFilterAttribute : JobFilterAttribute, IServerFilter 5 { 6 7 public void OnPerforming(PerformingContext filterContext) 8 { 9 10 } 11 12 /// <summary> 13 /// 未成功执行的 14 /// </summary> 15 /// <param name="filterContext"></param> 16 public void OnPerformed(PerformedContext filterContext) 17 { 18 var error = filterContext.Exception; 19 if (error==null) 20 { 21 return; 22 } 23 //记录日志到后台 24 ILoggerFactory loggerFactory = ObjectContainer.GetService<ILoggerFactory>(); 25 ILogger logger; 26 if (error.TargetSite != null && error.TargetSite.DeclaringType != null) 27 { 28 logger = loggerFactory.Create(error.TargetSite.DeclaringType.GetUnProxyType()); 29 } 30 else 31 { 32 logger = loggerFactory.Create(GetType()); 33 } 34 var contextMessage = filterContext.GetJobParameter<ContextMessage>("_ld_contextMessage"); 35 var message = GetLogMessage(contextMessage, error.ToString(), filterContext.BackgroundJob.Id); 36 37 var logLevel = ErrorLevelType.Fatal; 38 39 if (error.InnerException is AppException ex) 40 { 41 logLevel = ex.ErrorLevel; 42 } 43 44 switch (logLevel) 45 { 46 case ErrorLevelType.Info: 47 logger.Info(message, error); 48 break; 49 case ErrorLevelType.Warning: 50 logger.Warn(message, error); 51 break; 52 case ErrorLevelType.Error: 53 logger.Error(message, error); 54 break; 55 default: 56 logger.Fatal(message, error); 57 break; 58 } 59 } 60 61 62 63 /// <summary> 64 /// 获取当前日志对象 65 /// </summary> 66 /// <returns></returns> 67 private LogMessage GetLogMessage(ContextMessage contextMessage, string errorMessage, string backgroundJobId) 68 { 69 var logMessage = new LogMessage(contextMessage, errorMessage) 70 { 71 RawUrl = backgroundJobId 72 }; 73 return logMessage; 74 } 75 76 77 }
现在还有一个问题是,HangFire DashBoard 默认只支持localhost访问,现在我需要可以很方便的在外网通过web集群就能访问到其对应的HangFire DashBoard。
通过文档https://docs.hangfire.io/en/latest/configuration/using-dashboard.html,可以知道其提供了一个登录验证的实现,但是由于其是直接写死了密码在代码中的,觉得不好。(ps:具体的实现可以参考:https://gitee.com/LucasDot/Hangfire.Dashboard.Authorization,https://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选择配置我们自己实现的身份验证,以及是否只读设置。
1 public void Configuration(IAppBuilder app) 2 { 3 try 4 { 5 6 Bootstrapper.Instance.Start(); 7 8 var dashboardOptions = new DashboardOptions() 9 { 10 Authorization = new[] { new HangFireAuthorizationFilter() }, 11 IsReadOnlyFunc = HangFireIsReadOnly 12 }; 13 app.UseHangfireDashboard("/hangfire", dashboardOptions); 14 15 16 } 17 catch (Exception ex) 18 { 19 Debug.WriteLine(ex); 20 } 21 22 } 23 24 /// <summary> 25 /// HangFire仪表盘是否只读 26 /// </summary> 27 /// <param name="context"></param> 28 /// <returns></returns> 29 private bool HangFireIsReadOnly(DashboardContext context) 30 { 31 var owinContext = new OwinContext(context.GetOwinEnvironment()); 32 if (owinContext.Request.Host.ToString().StartsWith("localhost")) 33 { 34 return false; 35 } 36 37 try 38 { 39 var cookie = owinContext.Request.Cookies["Ld.HangFire"]; 40 char[] spilt = { ‘,‘ }; 41 var userData = FormsAuthentication.Decrypt(cookie)?.UserData.Split(spilt, StringSplitOptions.RemoveEmptyEntries); 42 if (userData != null) 43 { 44 var isAdmin = userData[0].Replace("isAdmin:", ""); 45 return !bool.Parse(isAdmin); 46 } 47 } 48 catch (Exception e) 49 { 50 51 } 52 53 return true; 54 }
在HangFireAuthorizationFilter的具体实现中,先校验是否已存在验证后的cookie如果有就不再验证,或者如果是通过localhost访问也不进行校验,否则验证签名是否正确,如果正确就将信息写入cookie。
1 /// <summary> 2 /// HangFire身份验证 3 /// </summary> 4 public class HangFireAuthorizationFilter : IDashboardAuthorizationFilter 5 { 6 public bool Authorize(DashboardContext context) 7 { 8 var owinContext = new OwinContext(context.GetOwinEnvironment()); 9 10 if (owinContext.Request.Host.ToString().StartsWith("localhost")|| owinContext.Request.Cookies["Ld.HangFire"] != null)//通过localhost访问不校验,与cookie也不校验 11 { 12 return true; 13 } 14 15 var cluster = owinContext.Request.Query.Get("cluster");//集群名称 16 var isAdminS = owinContext.Request.Query.Get("isAdmin");//是否管理员(是则允许修改hangfire) 17 var token = owinContext.Request.Query.Get("token"); 18 var t = owinContext.Request.Query.Get("t");//时间戳 19 if (!string.IsNullOrEmpty(token) && bool.TryParse(isAdminS, out var isAdmin) && long.TryParse(t, out var timestamp)) 20 { 21 try 22 { 23 var isValid = LicenceHelper.ValidSignature($"{cluster}_{isAdmin}", token, timestamp, TimeSpan.FromMinutes(5));//五分钟有效 24 if (isValid) 25 { 26 var ticket = new FormsAuthenticationTicket(0, cluster, DateTime.Now, DateTime.Now + FormsAuthentication.Timeout, false, $"isAdmin:{isAdmin}"); 27 var authToken = FormsAuthentication.Encrypt(ticket); 28 29 owinContext.Response.Cookies.Append("Ld.HangFire", authToken, new CookieOptions() 30 { 31 HttpOnly = true, 32 Path = "/hangfire" 33 }); 34 return true; 35 } 36 } 37 catch (Exception ex) 38 { 39 40 } 41 } 42 return false; 43 44 } 45 46 }
在web的管理后台具体实现为,同选中集群点击后台任务直接访问改集群的HangFire DashBoard
点击后台任务按钮,后台放回token相关信息,然后js打开一个新的标签页展示dashboard
1 public ActionResult GetHangFireToken(string clusterName) 2 { 3 var isAdmin=WorkContext.User.IsAdmin; 4 var timestamp = DateTime.UtcNow.Ticks; 5 var token = LicenceHelper.Signature($"{clusterName}_{isAdmin}", timestamp); 6 return this.Direct(new 7 { 8 isAdmin, 9 token, 10 timestamp=timestamp.ToString() 11 }); 12 }
1 openHangFire:function() { 2 var me = this, rows = me.getGridSelection(‘查看后台任务的集群‘, true); 3 if (!rows) { 4 return; 5 } 6 if (rows.length > 1) { 7 me.alert(‘只能选择一行‘); 8 return; 9 } 10 var clusterName = rows[0].get(‘ClusterName‘); 11 var opts = { 12 actionName: ‘GetHangFireToken‘, 13 extraParams: { 14 clusterName: clusterName 15 }, 16 success: function (result) { 17 var data = result.result; 18 var url = Ext.String.format("{0}/hangfire?cluster={1}&isAdmin={2}&token={3}&t={4}", 19 rows[0].get("AccessUrl"), 20 clusterName, 21 data.isAdmin, 22 data.token, 23 data.timestamp); 24 window.open(url, clusterName); 25 } 26 }; 27 me.directRequest(opts); 28 }
以上是关于HangFire多集群切换及DashBoard登录验证的主要内容,如果未能解决你的问题,请参考以下文章
centos7下的k8s+dashboard集群部署---环境准备及docker部署