简易首页防暴力-字典计时器
有时候首页需要限制下相同账号的错误登录次数,防止暴力破解,实际而言,还是有一点点作用,虽然并不是很大,一定层度上也能扼杀一番,主要是调整起来方便,对于老旧系统改造起来比较快,核心是字典,一个记录失败次数,一个记录账号解锁的时间,在账号登录时先去字典里面校验,不用频繁的请求数据库. 需要注意的是,这个字典要设置为全局。否则切换客服端就会失效.
//次数字典 private static Dictionary<string, int> errorCounts = new Dictionary<string, int>(); //时间字典 private static Dictionary<string, DateTime> lockoutTimes = new Dictionary<string, DateTime>();
字典设置完毕,接下来就是在登录的时机点上校验次数,基本思路是,每个登录进来的账号无论密码,先加到字典中,设置一个初始时间,后边后续统一判断
public ActionResult ICCLogin(string userName, string password) { int result; DateTime dateTime; //先加时间字典 if (!errorCounts.TryGetValue(userName, out result)) { errorCounts[userName] = 0; lockoutTimes[userName] = DateTime.MinValue; } //判断次数字典 if (IsLockedOut(userName, out dateTime)) { // 计算两个日期时间之间的时间间隔 TimeSpan timeDifference = dateTime.Subtract(DateTime.Now); // 计算总分钟数并向上取整 int totalMinutes = (int)Math.Ceiling(timeDifference.TotalMinutes); // 如果向上取整后的分钟数小于1,设为30 if (totalMinutes == 1) { totalMinutes = 30; } string suf = totalMinutes == 30 ? "s" : "分钟"; LibExceptionManagent.ThrowErr(string.Format("验证失败次数过多,账户已被锁定,{0}{1}后重试", totalMinutes, suf)); return View(); }
次数字典方法
private bool IsLockedAccount(string username, out DateTime dateTime) { //先加次数字典 if (!lockoutTimes.ContainsKey(username)) { lockoutTimes[username] = DateTime.MinValue; } //获取当前账号的可放开时间 dateTime = lockoutTimes[username]; return lockoutTimes[username] > DateTime.Now; }
贸然看去,貌似没啥子问题,实际测试中可能存在一个隐藏问题,比如5分钟内错误3次,超过5分钟后,再次输错,应该从0开始计算,而不是变成第四次
private bool IsLockedOut(string username, out DateTime dateTime) { //先加次数字典 if (!lockoutTimes.ContainsKey(username)) { lockoutTimes[username] = DateTime.MinValue; } //获取当前账号的可放开时间 int times = 0; dateTime = lockoutTimes[username]; TimeSpan timeDifference = DateTime.Now.Subtract(dateTime); int temp = (int)Math.Ceiling(timeDifference.TotalMinutes); bool bol = temp>=0 && lockoutTimes[username]!= DateTime.MinValue; if (bol && errorCounts.TryGetValue(username, out times)) { errorCounts[username] = 0; } return bol; }
实际上还缺少一个归零的操作,到达账号解封时间后, 需要置空错误次数,否则就会无限循环,5分钟结束后又从头开始
private bool IsLockedOut(string username, out DateTime dateTime) { //先加次数字典 if (!lockoutTimes.ContainsKey(username)) { lockoutTimes[username] = DateTime.MinValue; } dateTime = lockoutTimes[username]; bool bol = lockoutTimes[username] > DateTime.Now; //获取当前账号的可放开时间 if (bol && errorCounts.TryGetValue(username, out _)) { errorCounts[username] = 0; } return bol; }
写到次数,貌似没啥子问题,实际上存在漏洞,没有对时间,次数有完整的管理,使用过程中数据会错乱,重新捋一下逻辑,完整的判断方法如下
private bool IsLockedOut(string userName, out DateTime dateTime) { //时间字典初始化 if (!lockoutTimes.ContainsKey(userName)) { lockoutTimes[userName] = DateTime.MinValue; } //次数字典初始化 if (!errorCounts.ContainsKey(userName)) { errorCounts[userName] = 0; } //判断时间, dateTime = lockoutTimes[userName]; if (dateTime == DateTime.MinValue) { return false; //次数未锁定 } //计算解锁时间,满足则清空次数,重置时间 if (dateTime <= DateTime.Now) { //5次满足解锁时间 //不足5次满足间隔5分钟 if (errorCounts[userName] >= 5 || (DateTime.Now - dateTime).TotalSeconds >= 300) { errorCounts[userName] = 0; lockoutTimes[userName] = DateTime.MinValue; return false; } return false; } return true; }
如此基本上满足次数校验,为了形成一个小小的闭环,全局静态字典需要回收,再写一个定时任务清空字典,避免字典值越来越大
private static readonly Timer timer = new Timer(ClearDictionary, null, TimeSpan.Zero, TimeSpan.FromMinutes(30)); private static void ClearDictionary(object state) { // 在定时器触发时清空字典 //Dictionary<string, DateTime> keysToRemove = lockoutTimes.Where(pair => pair.Value != DateTime.MinValue && pair.Value <= DateTime.Now); foreach (var dic in lockoutTimes) { if (dic.Value == DateTime.MinValue) { continue; } double doble = (DateTime.Now - dic.Value).TotalSeconds; if (doble >= 300) { lockoutTimes.Remove(dic.Key); int count; if (errorCounts.TryGetValue(dic.Key, out count)) { errorCounts.Remove(dic.Key); } } } }
失败次数的累加相对比较简单,贴一下代码,不做特殊说明
else { lockoutTimes[userName] = DateTime.Now; if (!errorCounts.ContainsKey(userName)) { errorCounts[userName] = 1; } else { errorCounts[userName]++; if (errorCounts[userName] >= 5) { lockoutTimes[userName] = DateTime.Now.AddMinutes(5); // 锁定账户5分钟 LibExceptionManagent.ThrowErr("验证失败次数过多,账户已被锁定,5分钟后重试"); } } } LibExceptionManagent.ThrowErr("提供的用户名或密码不正确"); return View();
ok , 搞定
大家好,我是新来的小白,文未佳,却已创。转载请声明(博客园-郎中令)出处,谢谢
---市人皆大笑,举手揶揄之(手动链接博客园)