实现思路
0.在请求登录接口时判断是否登录成功,
1.如果登录失败将记录当前的用户登录次数其有效期为一分钟,一分钟后重新计次,
查找在用户10分钟内的登录记录,超过设定的次数后禁止登录,
在规定时间内登录成功,取消所有计次,并解锁用户
2.如果登录成功,取消所有计次,并解锁用户登录
3.实现仅依赖于系统缓存服务除此无任何依赖
代码
#region AuthorInfo
// Author: sjx
// Date: 2023-08-10 16:22
#endregion
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace Api_Limit.Controllers;
[ApiController]
[Route("[controller]/[action]")]
public class UserAuthorController : ControllerBase
{
#region Ctor
public UserAuthorController(IMemoryCache memoryCache)
{
// 注入缓存,并设置缓存
LimitUserLogin.MemoryCache = memoryCache;
}
#endregion
#region Action
#region 登录
[AllowAnonymous]
[HttpPost(Name = "UserLogin")]
public string UserLogin(UserLogInfo input)
{
LimitUserLogin.FailCount = 5;
LimitUserLogin.LockTime = 30;
// 模拟用户数据
var userList = new List<UserLogInfo>()
{
new UserLogInfo()
{
Account = "admin",
Password = "123456"
},
new UserLogInfo()
{
Account = "user",
Password = "123456"
},
new UserLogInfo()
{
Account = "test",
Password = "123456"
}
};
// 判断是否被锁定
var isLock = LimitUserLogin.IsForbidden(input.Account);
if (isLock) return "账号已被锁定,请30分钟后再试";
// 验证账号密码是否正确
var user = userList.FirstOrDefault(x => x.Account == input.Account && x.Password == input.Password);
var isOk = user != null;
if (!isOk)
{
// 登录失败,计次
LimitUserLogin.SetFailCounter(input.Account);
return "账号或密码错误";
}
// 登录成功,解锁
LimitUserLogin.UnLock(input.Account);
return "登录成功";
}
#endregion
#endregion
}
/// <summary>
/// 模拟用户数据
/// </summary>
public class UserLogInfo
{
public string Account { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
/// <summary>
/// 用户登录限制
/// </summary>
public static class LimitUserLogin
{
#region Field
private const string FailCountRedisKey = "login_fail_count";
private const string Separator = ":";
/// <summary>
/// 系统缓存
/// </summary>
public static IMemoryCache MemoryCache = null!;
/// <summary>
/// 锁定时间
/// </summary>
public static int LockTime = 30;
/// <summary>
/// 失败次数
/// </summary>
public static int FailCount = 5;
#endregion
#region Methods
#region 是否被锁定
/// <summary>
/// 是否被锁定
/// </summary>
/// <param name="account">用户名</param>
/// <returns></returns>
public static bool IsForbidden(string account)
{
CheckCache();
try
{
// 判断是否被锁定
return MemoryCache.TryGetValue(string.Join(Separator, FailCountRedisKey, account), out int failCount);
}
catch (Exception e)
{
Console.WriteLine(e);
}
return false;
}
#endregion
#region 登录失败计次
/// <summary>
/// 失败计次
/// </summary>
/// <param name="username">用户名</param>
public static void SetFailCounter(string username)
{
CheckCache();
// 获取当前时间
var now = DateTime.Now;
// 获取当前时间的分钟数
var minute = now.ToString("yyyyMMddHHmm");
// 登录失败次数 + 1
var key = string.Join(Separator, FailCountRedisKey, username, minute);
var count = MemoryCache.Get<int?>(key) ?? 0;
// 如果key不存在的话就会以增量形式存储进来
MemoryCache.Set(key, count + 1, TimeSpan.FromMinutes(1));
// 如果失败次数大于5次,锁定账户
var windowsKeys = new List<string>();
// 获取当前时间前10分钟的key
for (var i = 0; i < 10; i++)
{
windowsKeys.Add(string.Join(Separator, FailCountRedisKey, username, now.ToString("yyyyMMddHHmm")));
now = now.AddMinutes(-1);
}
var countList = new List<int>();
// 获取当前时间前10分钟的失败次数
foreach (var key1 in windowsKeys)
{
if (MemoryCache.TryGetValue(key1, out int count1))
{
countList.Add(count1);
}
}
// 获取当前时间前10分钟的失败次数总和
var total = countList.Sum();
// 如果失败次数大于5次,锁定账户
if (total >= FailCount)
{
Forbidden(username);
}
}
#endregion
#region 禁止登录
/// <summary>
/// 禁止登录30分钟
/// </summary>
/// <param name="account">用户名</param>
private static void Forbidden(string account)
{
CheckCache();
// 设置缓存
var key = string.Join(Separator, FailCountRedisKey, account);
// 设置30分钟过期
MemoryCache.Set(key, 1, TimeSpan.FromMinutes(LockTime));
}
#endregion
#region 解锁
/// <summary>
/// 解锁
/// </summary>
/// <param name="account">用户名</param>
public static void UnLock(string account)
{
CheckCache();
// 获取当前时间的分钟数
var minute = DateTime.Now.Minute;
var windowsKey = new List<string>();
for (var i = 0; i <= 10; i++)
{
windowsKey.Add(string.Join(Separator, FailCountRedisKey, account, DateTime.Now.Minute - i));
}
// 删除缓存
foreach (var key in windowsKey)
{
MemoryCache.Remove(key);
}
}
#endregion
#region 检查缓存是否存在
private static void CheckCache()
{
if (MemoryCache == null) throw new Exception("请先初始化MemoryCache");
}
#endregion
#endregion
}