由于最近项目发现有尝试密码登录的操作,需要设置密码复杂度及账号多次登录失败,将账号锁定N分钟后,才可以继续登录操作。
开始思路是使用登录记录数据处理连续登录失败的问题,如果频繁请求可能会导致数据库查询变慢,影响数据库性能,但接口及查询功能实现了,mysql语句如下:
string sSql = "SELECT COUNT(dlsfcg) AS dlsbCount FROM "+ " (SELECT id,dlsfcg,"+ " IF(@lastNum!=dlsfcg,@group:=@group+1,@group) AS g,"+ " IF(@lastNum!=dlsfcg,@lastNum:=dlsfcg,@lastNum) AS c FROM "+ " (SELECT id,dlsfcg FROM rz_dlrz WHERE dlsj>='"+ startTime + "' AND dlsj<='"+ endTime + "' AND yhm='"+ yhm + "') a,"+ " (SELECT @lastNum:=0,@group:=0 ) b ) m "+ " GROUP BY g HAVING COUNT(1)>="+ tjcs + " ORDER BY id";
在数据库执行查询的数据看着是正常,可以查询出连续失败的记录,但是实际业务中未使用此接口及查询方法,如果实际业务中使用,请进行完整测试。
经过一番思考过后,突然想起项目中使用redis缓存,突发奇想地开始使用redis做文章,看看怎么实现缓存登录失败记录的问题,说干就干,于是开始编写代码。
appsettings.json配置:
"AccountLock": {"IsEnableLock": 1, //是否启用锁定 1-启用 其它值禁用"LockMinuties": 5, //锁定分钟数"LockNum": 5, //连续登录失败最大次数"CacheLockDataMinutes": 10, //缓存登录失败锁定数据分钟数"IsEnablePwdLength": 1, //是否启用密码长度 1-启用 其它值禁用"PasswordLength": 8//密码长度}
获取配置:
/// <summary>/// 账号登录失败锁定配置/// </summary>public static class AccountLockSet{/// <summary>/// 是否启用锁定 1-启用 其它值禁用/// </summary>public static int IsEnableLock => Configuration["AccountLock:IsEnableLock"].ObjToInt();/// <summary>/// 锁定分钟数/// </summary>public static double LockMinuties => Configuration["AccountLock:LockMinuties"].ObjToMoney();/// <summary>/// 连续登录失败最大次数(锁定最大数)/// </summary>public static int LockNum => Configuration["AccountLock:LockNum"].ObjToInt();/// <summary>/// 缓存登录失败锁定数据分钟数/// </summary>public static int CacheLockDataMinutes => Configuration["AccountLock:CacheLockDataMinutes"].ObjToInt();/// <summary>/// 是否启用密码长度 1-启用 其它值禁用/// </summary>public static int IsEnablePwdLength => Configuration["AccountLock:IsEnablePwdLength"].ObjToInt();/// <summary>/// 密码长度/// </summary>public static int PasswordLength => Configuration["AccountLock:PasswordLength"].ObjToInt();}
上面我写成自定义的配置类,你可以根据自己的情况进行配置及读取配置文件内容。
编写方法实现:
#region 登录失败大于指定次数锁定账号/// <summary>/// 登录失败大于指定次数锁定账号/// </summary>/// <param name="_sLockKey">redis key</param>/// <param name="_dLockMinuties">锁定时间 单位:分钟</param>/// <param name="_nLockNum">锁定次数</param>/// <param name="_nIsVerify">是否验证 1-验证锁定 默认-0 处理锁定数据</param>/// <param name="_nCacheLockDataMinutes">缓存锁定数据时间 单位:分钟,默认10分钟</param>/// <returns>Tuple item1:剩余次数,item2:锁定后解锁时间,item3:是否锁定 1-锁定 0-未锁定 </returns>[NonAction]public Tuple<int, double, int> fnAccountLock(string _sLockKey, double _dLockMinuties, int _nLockNum, int _nIsVerify = 0, int _nCacheLockDataMinutes = 10){Tuple<int, DateTime> tupAccLock = null;if (this._cacheHelper.Exists(_sLockKey)){var vLockModel = this._cacheHelper.GetCache(_sLockKey);if (null != vLockModel){string sLockJson = JsonHelper.ToJson(vLockModel);dynamic dyObj = JsonHelper.GetJSON<dynamic>(sLockJson);int nLockNum = dyObj.Item1;DateTime dtLockTime = dyObj.Item2;double dLockMinutes = (dtLockTime - DateTime.Now).TotalMinutes;if (nLockNum < _nLockNum)//缓存锁定次数小于指定锁定次数{int nResidueNum = _nLockNum - nLockNum;//剩余次数if (_nIsVerify == 0){nLockNum++;nResidueNum = _nLockNum - nLockNum;//剩余次数tupAccLock = new Tuple<int, DateTime>(nLockNum, DateTime.Now.AddMinutes(_dLockMinuties));this._cacheHelper.Add(_sLockKey, tupAccLock, TimeSpan.FromMinutes(_nCacheLockDataMinutes));}if (nLockNum == _nLockNum)//缓存锁定次数等于指定锁定次数 返回锁定状态{return Tuple.Create(nResidueNum, dLockMinutes, 1);}return Tuple.Create(nResidueNum, dLockMinutes, 0);}else{if (dLockMinutes <= 0){//小于指定锁定时间 删除锁定数据this._cacheHelper.Remove(_sLockKey);return Tuple.Create(0, dLockMinutes, 0);}return Tuple.Create(0, dLockMinutes, 1);}}else{if (_nIsVerify == 0){tupAccLock = new Tuple<int, DateTime>(1, DateTime.Now.AddMinutes(_dLockMinuties));this._cacheHelper.Add(_sLockKey, tupAccLock, TimeSpan.FromMinutes(_nCacheLockDataMinutes));return Tuple.Create(4, _dLockMinuties, 0);}return Tuple.Create(_nLockNum, _dLockMinuties, 0);}}else{if (_nIsVerify == 0){tupAccLock = new Tuple<int, DateTime>(1, DateTime.Now.AddMinutes(_dLockMinuties));this._cacheHelper.Add(_sLockKey, tupAccLock, TimeSpan.FromMinutes(_nCacheLockDataMinutes));return Tuple.Create(4, _dLockMinuties, 0);}return Tuple.Create(_nLockNum, _dLockMinuties, 0);}}#endregion
由于此方法只在登录控制器使用,所有没有单独封装,直接写在了登录控制器内。
经测试,此方法貌似还没发现问题,如有问题可以留言给我,谢谢。
在登录接口中使用:
int IsEnablePwdLength = AccountLockSet.IsEnablePwdLength;int PasswordLength = AccountLockSet.PasswordLength;if (IsEnablePwdLength == 1 && model.mm?.Length < PasswordLength){return new MessageModel<object>(){code = 400,success = false,msg = "登录密码不符合要求!"};}int IsEnableLock = AccountLockSet.IsEnableLock;double dLockMinuties = AccountLockSet.LockMinuties;int nLockNum = AccountLockSet.LockNum;int nCacheLockDataMinutes = AccountLockSet.CacheLockDataMinutes;if (IsEnableLock == 1){var vAccountLock = this.fnAccountLock(sLockKey, dLockMinuties, nLockNum, 1, nCacheLockDataMinutes);if (vAccountLock.Item3 == 1){return new MessageModel<object>(){code = 400,success = false,msg = "登录失败" + nLockNum + "次锁定" + dLockMinuties + "分钟,请在" + Math.Ceiling(vAccountLock.Item2) + "分钟后再试!"};}}
登录失败提示:
if (null == vUserModel){yhdlrz.ms = "登录失败,用户名不存在";await _logService.Add(yhdlrz);var vAccountLock = this.fnAccountLock(sLockKey, dLockMinuties, nLockNum, _nCacheLockDataMinutes: nCacheLockDataMinutes);string sMsgRet = "还有" + vAccountLock?.Item1 + "次机会!";if (vAccountLock.Item1 <= 0){sMsgRet = "当前账号已锁定,锁定" + dLockMinuties + "分钟!";}return new MessageModel<object>(){code = 400,success = false,msg = "登录失败,用户名不存在!" + sMsgRet};}
vUserModel是根据用户名查询用户返回的对象,方法可在登录失败的地方多次调用,yhdlrz-记录登录日志,此代码你可以删除。
希望本文对你有帮助。