由于最近项目发现有尝试密码登录的操作,需要设置密码复杂度及账号多次登录失败,将账号锁定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-记录登录日志,此代码你可以删除。
补充:
//如果在锁定之前登录成功,则删除锁定数据
this._cacheHelper.Remove(sLockKey);
希望本文对你有帮助。