如何在 ASP.NET Core Identity 中扩展和验证会话?
How to extend and validate session in ASP.NET Core Identity?
我们希望让用户管理他们的登录会话。
到目前为止,使用 ASP.NET Core 且不使用 Identity Extensions 非常容易。
但是我们如何使用 ASP.NET Core Identity 调用此验证?
我们遇到的问题:
- 我们如何存储基于登录会话的信息,例如浏览器版本、设备类型和用户位置?我们是否扩展任何类型或想法是什么?
- 我们如何根据特定用户动态设置 cookie 过期时间?
- 我们如何从后端使 Cookie 无效(如上面的 link 所示)?
- 我们如何要求特殊功能的额外密码提示?
感觉 ASP.NET Core Identity 仍然没有那么可扩展和灵活:(
不幸的是,ASP.NET 身份的这个区域没有很好的记录,我个人认为这是一个敏感区域的风险。
在我更多地接触源代码之后,解决方案似乎是使用 SignIn Manager 的 SignIn 进程。
基本问题是将您的自定义声明放入 cookie 的 ClaimsIdentity 中并不那么容易。没有办法。
在任何情况下都不得将此值存储在数据库中用户的声明中,否则每次登录都会收到这些声明 - 会很糟糕。
所以我创建了自己的方法,首先在数据库中搜索用户,然后使用 SignInManager 的现有方法。
在拥有登录管理器创建的 ClaimsIdentity 之后,您可以使用自己的声明来丰富该身份。
为此,我在数据库中保存了带有 Guid 的登录会话,并将 id 作为 cookie 中的声明。
public async Task<SignInResult> SignInUserAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
{
DateTimeOffset createdLoginOn = DateTimeOffset.UtcNow;
DateTimeOffset validTo = createdLoginOn.AddSeconds(_userAuthOptions.ExpireTimeSeconds);
// search for user
var user = await _userManager.FindByNameAsync(userName);
if (user is null) { return SignInResult.Failed; }
// CheckPasswordSignInAsync checks if user is allowed to sign in and if user is locked
// also it checks and counts the failed login attempts
var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
if (attempt.Succeeded)
{
// TODO: Check 2FA here
// create a unique login entry in the backend
string browserAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"];
Guid loginId = await _eventDispatcher.Send(new AddUserLoginCommand(user.Id, user.UserName, createdLoginOn, validTo, browserAgent));
// Write the login id in the login claim, so we identify the login context
Claim[] customClaims = { new Claim(CustomUserClaims.UserLoginSessionId, loginId.ToString()) };
// Signin User
await SignInWithClaimsAsync(user, isPersistent, customClaims);
return SignInResult.Success;
}
return attempt;
}
对于每个请求,我都可以验证 ClaimsIdentity 并搜索登录 ID。
public class CookieSessionValidationHandler : CookieAuthenticationEvents
{
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
ClaimsPrincipal userPrincipal = context.Principal;
if (!userPrincipal.TryGetUserSessionInfo(out int userId, out Guid sessionId))
{
// session format seems to be invalid
context.RejectPrincipal();
}
else
{
IEventDispatcher eventDispatcher = context.HttpContext.RequestServices.GetRequiredService<IEventDispatcher>();
bool succeeded = await eventDispatcher.Send(new UserLoginUpdateLoginSessionCommand(userId, sessionId));
if (!succeeded)
{
// session expired or was killed
context.RejectPrincipal();
}
}
}
}
我们希望让用户管理他们的登录会话。 到目前为止,使用 ASP.NET Core 且不使用 Identity Extensions 非常容易。
但是我们如何使用 ASP.NET Core Identity 调用此验证?
我们遇到的问题:
- 我们如何存储基于登录会话的信息,例如浏览器版本、设备类型和用户位置?我们是否扩展任何类型或想法是什么?
- 我们如何根据特定用户动态设置 cookie 过期时间?
- 我们如何从后端使 Cookie 无效(如上面的 link 所示)?
- 我们如何要求特殊功能的额外密码提示?
感觉 ASP.NET Core Identity 仍然没有那么可扩展和灵活:(
不幸的是,ASP.NET 身份的这个区域没有很好的记录,我个人认为这是一个敏感区域的风险。
在我更多地接触源代码之后,解决方案似乎是使用 SignIn Manager 的 SignIn 进程。
基本问题是将您的自定义声明放入 cookie 的 ClaimsIdentity 中并不那么容易。没有办法。 在任何情况下都不得将此值存储在数据库中用户的声明中,否则每次登录都会收到这些声明 - 会很糟糕。
所以我创建了自己的方法,首先在数据库中搜索用户,然后使用 SignInManager 的现有方法。
在拥有登录管理器创建的 ClaimsIdentity 之后,您可以使用自己的声明来丰富该身份。 为此,我在数据库中保存了带有 Guid 的登录会话,并将 id 作为 cookie 中的声明。
public async Task<SignInResult> SignInUserAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
{
DateTimeOffset createdLoginOn = DateTimeOffset.UtcNow;
DateTimeOffset validTo = createdLoginOn.AddSeconds(_userAuthOptions.ExpireTimeSeconds);
// search for user
var user = await _userManager.FindByNameAsync(userName);
if (user is null) { return SignInResult.Failed; }
// CheckPasswordSignInAsync checks if user is allowed to sign in and if user is locked
// also it checks and counts the failed login attempts
var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
if (attempt.Succeeded)
{
// TODO: Check 2FA here
// create a unique login entry in the backend
string browserAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"];
Guid loginId = await _eventDispatcher.Send(new AddUserLoginCommand(user.Id, user.UserName, createdLoginOn, validTo, browserAgent));
// Write the login id in the login claim, so we identify the login context
Claim[] customClaims = { new Claim(CustomUserClaims.UserLoginSessionId, loginId.ToString()) };
// Signin User
await SignInWithClaimsAsync(user, isPersistent, customClaims);
return SignInResult.Success;
}
return attempt;
}
对于每个请求,我都可以验证 ClaimsIdentity 并搜索登录 ID。
public class CookieSessionValidationHandler : CookieAuthenticationEvents
{
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
ClaimsPrincipal userPrincipal = context.Principal;
if (!userPrincipal.TryGetUserSessionInfo(out int userId, out Guid sessionId))
{
// session format seems to be invalid
context.RejectPrincipal();
}
else
{
IEventDispatcher eventDispatcher = context.HttpContext.RequestServices.GetRequiredService<IEventDispatcher>();
bool succeeded = await eventDispatcher.Send(new UserLoginUpdateLoginSessionCommand(userId, sessionId));
if (!succeeded)
{
// session expired or was killed
context.RejectPrincipal();
}
}
}
}