XFEExtension.NetCore.ServerInteractive
[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig
关注
0
Fork
0
Star
0
using System.Collections.Concurrent;
using System.Net;
using System.Text.Json;
using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Models.UserModels;
using XFEExtension.NetCore.ServerInteractive.Utilities.Helpers;
using XFEExtension.NetCore.StringExtension;
namespace XFEExtension.NetCore.ServerInteractive.Utilities.Server.Services.CoreService;
/// <summary>使用 PBKDF2 密码和不透明会话令牌的登录服务。</summary>
public partial class UserLoginService<T> : ServerCoreUserLoginServiceBase<T> where T : class
{
private static readonly ConcurrentDictionary<string, LoginAttemptWindow> s_loginAttempts = new(StringComparer.Ordinal);
[EntryPoint("user/login")]
public async Task Login()
{
Console.Write("登录请求");
var account = Json?["account"]?.GetString();
var password = Json?["password"]?.GetString();
var deviceInfo = Json?["deviceInfo"]?.GetString();
if (account.IsNullOrWhiteSpace()) throw Error("账户名不能为空");
if (password.IsNullOrWhiteSpace()) throw Error("登录密码不能为空");
if (deviceInfo.IsNullOrWhiteSpace()) throw Error("电脑信息不能为空");
var accountRateKey = $"account:{account.Trim().ToUpperInvariant()}";
var ipRateKey = $"ip:{ReturnArgs.ClientIP}";
if (!TryBeginLogin(accountRateKey) || !TryBeginLogin(ipRateKey))
throw Error("登录尝试过于频繁,请稍后重试", (HttpStatusCode)429);
var result = UserHelper.GetUser(account, password, GetUserFunction(), out var user, out var passwordUpgraded);
if (result is UserOperateResult.UserNotFound or UserOperateResult.InvalidPassword)
{
RecordFailure(accountRateKey);
RecordFailure(ipRateKey);
throw Error("账号或密码错误", HttpStatusCode.Forbidden);
}
if (result != UserOperateResult.Success || user is null)
{
RecordFailure(accountRateKey);
RecordFailure(ipRateKey);
throw Error(UserHelper.OutPutResult(result), HttpStatusCode.Forbidden);
}
if (passwordUpgraded) UpdateUserFunction(user);
s_loginAttempts.TryRemove(accountRateKey, out _);
s_loginAttempts.TryRemove(ipRateKey, out _);
var token = SessionTokenHelper.Create(out var tokenHash);
var expiresAt = DateTimeOffset.UtcNow.AddDays(GetLoginKeepDays());
var login = new EncryptedUserLoginModel
{
TokenHash = tokenHash,
CreatedAtUtc = DateTimeOffset.UtcNow,
ExpiresAtUtc = expiresAt,
UserLoginModel = new()
{
Uid = user.Id,
DeviceInfo = deviceInfo,
LastIPAddress = ReturnArgs.ClientIP,
EndDateTime = expiresAt.UtcDateTime
}
};
lock (SessionStoreSynchronization.Gate)
{
AddEncryptedUserLoginModelFunction(login);
// 每个用户最多保留 10 个活跃会话,优先删除已过期及最旧会话。
var userSessions = GetEncryptedUserLoginModelFunction()
.Where(session => session.UserLoginModel.Uid == user.Id)
.OrderByDescending(session => session.ExpiresAtUtc <= DateTimeOffset.UtcNow || session.RevokedAtUtc is not null)
.ThenBy(session => session.CreatedAtUtc)
.ToArray();
foreach (var excessSession in userSessions.Take(Math.Max(0, userSessions.Length - 10)))
RemoveEncryptedUserLoginModelFunction(excessSession);
}
var preview = deviceInfo.Length <= 10 ? deviceInfo : $"{deviceInfo[..10]}...";
Console.Write($"{account}({preview}),到期时间 {expiresAt:O}");
await ReturnArgs.CloseJson(new
{
session = token,
expireDate = expiresAt,
userInfo = LoginResultConvertFunction(user)
}, JsonSerializerOptions);
}
private static bool TryBeginLogin(string key)
{
var now = DateTimeOffset.UtcNow;
if (!s_loginAttempts.TryGetValue(key, out var state)) return true;
lock (state)
{
if (state.LockedUntilUtc > now) return false;
if (now - state.WindowStartUtc > TimeSpan.FromMinutes(1))
{
state.WindowStartUtc = now;
state.Failures = 0;
}
return state.Failures < 5;
}
}
private static void RecordFailure(string key)
{
var now = DateTimeOffset.UtcNow;
if (s_loginAttempts.Count > 10_000)
{
foreach (var stale in s_loginAttempts.Where(pair => now - pair.Value.WindowStartUtc > TimeSpan.FromMinutes(10)).Select(pair => pair.Key).Take(1_000))
s_loginAttempts.TryRemove(stale, out _);
}
var state = s_loginAttempts.GetOrAdd(key, _ => new() { WindowStartUtc = now });
lock (state)
{
if (now - state.WindowStartUtc > TimeSpan.FromMinutes(1))
{
state.WindowStartUtc = now;
state.Failures = 0;
}
state.Failures++;
if (state.Failures >= 5) state.LockedUntilUtc = now.AddMinutes(1);
}
}
private sealed class LoginAttemptWindow
{
public DateTimeOffset WindowStartUtc { get; set; }
public DateTimeOffset LockedUntilUtc { get; set; }
public int Failures { get; set; }
}
}
using System.Collections.Concurrent;
using System.Net;
using System.Text.Json;
using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Models.UserModels;
using XFEExtension.NetCore.ServerInteractive.Utilities.Helpers;
using XFEExtension.NetCore.StringExtension;
namespace XFEExtension.NetCore.ServerInteractive.Utilities.Server.Services.CoreService;
/// <summary>使用 PBKDF2 密码和不透明会话令牌的登录服务。</summary>
public partial class UserLoginService<T> : ServerCoreUserLoginServiceBase<T> where T : class
{
private static readonly ConcurrentDictionary<string, LoginAttemptWindow> s_loginAttempts = new(StringComparer.Ordinal);
[EntryPoint("user/login")]
public async Task Login()
{
Console.Write("登录请求");
var account = Json?["account"]?.GetString();
var password = Json?["password"]?.GetString();
var deviceInfo = Json?["deviceInfo"]?.GetString();
if (account.IsNullOrWhiteSpace()) throw Error("账户名不能为空");
if (password.IsNullOrWhiteSpace()) throw Error("登录密码不能为空");
if (deviceInfo.IsNullOrWhiteSpace()) throw Error("电脑信息不能为空");
var accountRateKey = $"account:{account.Trim().ToUpperInvariant()}";
var ipRateKey = $"ip:{ReturnArgs.ClientIP}";
if (!TryBeginLogin(accountRateKey) || !TryBeginLogin(ipRateKey))
throw Error("登录尝试过于频繁,请稍后重试", (HttpStatusCode)429);
var result = UserHelper.GetUser(account, password, GetUserFunction(), out var user, out var passwordUpgraded);
if (result is UserOperateResult.UserNotFound or UserOperateResult.InvalidPassword)
{
RecordFailure(accountRateKey);
RecordFailure(ipRateKey);
throw Error("账号或密码错误", HttpStatusCode.Forbidden);
}
if (result != UserOperateResult.Success || user is null)
{
RecordFailure(accountRateKey);
RecordFailure(ipRateKey);
throw Error(UserHelper.OutPutResult(result), HttpStatusCode.Forbidden);
}
if (passwordUpgraded) UpdateUserFunction(user);
s_loginAttempts.TryRemove(accountRateKey, out _);
s_loginAttempts.TryRemove(ipRateKey, out _);
var token = SessionTokenHelper.Create(out var tokenHash);
var expiresAt = DateTimeOffset.UtcNow.AddDays(GetLoginKeepDays());
var login = new EncryptedUserLoginModel
{
TokenHash = tokenHash,
CreatedAtUtc = DateTimeOffset.UtcNow,
ExpiresAtUtc = expiresAt,
UserLoginModel = new()
{
Uid = user.Id,
DeviceInfo = deviceInfo,
LastIPAddress = ReturnArgs.ClientIP,
EndDateTime = expiresAt.UtcDateTime
}
};
lock (SessionStoreSynchronization.Gate)
{
AddEncryptedUserLoginModelFunction(login);
// 每个用户最多保留 10 个活跃会话,优先删除已过期及最旧会话。
var userSessions = GetEncryptedUserLoginModelFunction()
.Where(session => session.UserLoginModel.Uid == user.Id)
.OrderByDescending(session => session.ExpiresAtUtc <= DateTimeOffset.UtcNow || session.RevokedAtUtc is not null)
.ThenBy(session => session.CreatedAtUtc)
.ToArray();
foreach (var excessSession in userSessions.Take(Math.Max(0, userSessions.Length - 10)))
RemoveEncryptedUserLoginModelFunction(excessSession);
}
var preview = deviceInfo.Length <= 10 ? deviceInfo : $"{deviceInfo[..10]}...";
Console.Write($"{account}({preview}),到期时间 {expiresAt:O}");
await ReturnArgs.CloseJson(new
{
session = token,
expireDate = expiresAt,
userInfo = LoginResultConvertFunction(user)
}, JsonSerializerOptions);
}
private static bool TryBeginLogin(string key)
{
var now = DateTimeOffset.UtcNow;
if (!s_loginAttempts.TryGetValue(key, out var state)) return true;
lock (state)
{
if (state.LockedUntilUtc > now) return false;
if (now - state.WindowStartUtc > TimeSpan.FromMinutes(1))
{
state.WindowStartUtc = now;
state.Failures = 0;
}
return state.Failures < 5;
}
}
private static void RecordFailure(string key)
{
var now = DateTimeOffset.UtcNow;
if (s_loginAttempts.Count > 10_000)
{
foreach (var stale in s_loginAttempts.Where(pair => now - pair.Value.WindowStartUtc > TimeSpan.FromMinutes(10)).Select(pair => pair.Key).Take(1_000))
s_loginAttempts.TryRemove(stale, out _);
}
var state = s_loginAttempts.GetOrAdd(key, _ => new() { WindowStartUtc = now });
lock (state)
{
if (now - state.WindowStartUtc > TimeSpan.FromMinutes(1))
{
state.WindowStartUtc = now;
state.Failures = 0;
}
state.Failures++;
if (state.Failures >= 5) state.LockedUntilUtc = now.AddMinutes(1);
}
}
private sealed class LoginAttemptWindow
{
public DateTimeOffset WindowStartUtc { get; set; }
public DateTimeOffset LockedUntilUtc { get; set; }
public int Failures { get; set; }
}
}