我需要做什么
我正在使用ASP.NET CORE开发应用程序,实际上我在使用Identity
实现时遇到了问题。
事实上,在官方文档中没有关于多会话的参考,这很糟糕,因为我开发了SaaS
应用程序;特别是用户订阅了付费计划以访问一组特定功能,并且他可以将其凭据提供给其他用户,以便他们可以免费使用,这是一个非常糟糕的情况,我会浪费很多金钱和时间。
我该怎么办
在网上搜索了很多之后,我发现了许多针对较旧版本的ASP.NET CORE的解决方案,因此我无法进行测试,但我了解通常此问题的解决方案与存储用户有关数据库中的时间戳(这是登录时生成的GUID),因此每次用户访问受限页面并且有更多会话(具有不同的用户时间戳)时,旧会话都会关闭。
我不喜欢这种解决方案,因为用户可以轻松复制浏览器的cookie
并将其共享给其他用户。
我虽然将登录的用户会话的信息存储在数据库中,但这也需要大量的连接。.因此,我对ASP.NET CORE的经验不足以及网络资源的匮乏使我陷入了困境。混乱。
有人可以分享一个通用的想法来实施一种安全的解决方案,以防止多用户登录吗?
答案 0 :(得分:2)
我创建了一个github存储库,其中更改了仅允许单个会话所需的默认.net core 2.1模板。 https://github.com/xKloc/IdentityWithSession
这是要点。
首先,使用自定义类覆盖默认的UserClaimsPrincipalFactory<IdentityUser>
类,该类会将您的会话添加到用户声明中。声明只是一个键/值对,将存储在用户的Cookie中,也存储在AspNetUserClaims
表下的服务器上。
将此类添加到项目中的任何地方。
public class ApplicationClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser>
{
private readonly UserManager<IdentityUser> _userManager;
public ApplicationClaimsPrincipalFactory(UserManager<IdentityUser> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor)
{
_userManager = userManager;
}
public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
// find old sessions and remove
var claims = await _userManager.GetClaimsAsync(user);
var session = claims.Where(e => e.Type == "session");
await _userManager.RemoveClaimsAsync(user, session);
// add new session claim
await _userManager.AddClaimAsync(user, new Claim("session", Guid.NewGuid().ToString()));
// create principal
var principal = await base.CreateAsync(user);
return principal;
}
}
接下来,我们将创建一个授权处理程序,该处理程序将检查每个请求的会话是否有效。
处理程序会将用户cookie中的会话声明与存储在数据库中的会话声明进行匹配。如果它们匹配,则授权用户继续。如果不匹配,则用户将收到“拒绝访问”消息。
在项目的任何位置添加这两个类。
public class ValidSessionRequirement : IAuthorizationRequirement
{
}
public class ValidSessionHandler : AuthorizationHandler<ValidSessionRequirement>
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
public ValidSessionHandler(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidSessionRequirement requirement)
{
// if the user isn't authenticated then no need to check session
if (!context.User.Identity.IsAuthenticated)
return;
// get the user and session claim
var user = await _userManager.GetUserAsync(context.User);
var claims = await _userManager.GetClaimsAsync(user);
var serverSession = claims.First(e => e.Type == "session");
var clientSession = context.User.FindFirst("session");
// if the client session matches the server session then the user is authorized
if (serverSession?.Value == clientSession?.Value)
{
context.Succeed(requirement);
}
return;
}
}
最后,只需在启动时注册这些新类,即可对其进行调用。
在Startup
下方的ConfigureServices
方法下将此代码添加到您的services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
类中
// build default authorization policy
var defaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new ValidSessionRequirement())
.Build();
// add authorization to the pipe
services.AddAuthorization(options =>
{
options.DefaultPolicy = defaultPolicy;
});
// register new claims factory
services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, ApplicationClaimsPrincipalFactory>();
// register valid session handler
services.AddTransient<IAuthorizationHandler, ValidSessionHandler>();
答案 1 :(得分:1)
最好的方法是执行与Google,Facebook和其他公司相似的操作-检测用户是否从其他设备登录。对于您的情况,我相信您会希望有一种稍微不同的行为-您可能会拒绝,而不是要求访问权限。就像您要创建“每台设备”许可证或“单租户”许可证一样。
此Stack Overflow thread讨论了此解决方案。
检测设备更改的最可靠方法是创建一个 fingerprint个正在运行浏览器的浏览器/设备。这是一个 复杂的主题可以使您100%正确,并且有商业产品 相当不错,但并非完美无瑕。
注意:如果您想从简单开始,则可以以Secure cookie开头,该{{3}}不太可能通过窃听而被cookie盗用。例如,您可以存储哈希指纹。
答案 2 :(得分:0)
您可以使用UpdateSecurityStamp
使所有现有的身份验证Cookie无效。例如:
public async Task<IActionResult> Login(LoginViewModel model)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
ModelState.AddModelError(string.Empty, "Invalid username/password.");
return View();
}
if (await _userManager.ValidatePasswordAsync(user, model.Password))
{
await _userManager.UpdateSecurityStampAsync(user);
var result = await _signInManager.SignInAsync(user, isPersistent: false);
// handle `SignInResult` cases
}
}
通过更新安全标记,将导致所有现有的身份验证cookie无效,基本上注销用户已登录的所有其他设备。然后,在此当前设备上登录用户。
答案 3 :(得分:0)
有一些访问管理解决方案(ForgeRock,Oracle访问管理)实现了此会话配额功能。 ForgeRock具有社区版本,其源代码可在Github上找到,也许您可以看看它的实现方式。他们还提供了一篇博客文章,广泛介绍了功能(https://blogs.forgerock.org/petermajor/2013/01/session-quota-basics/)
如果这对于您的用例而言太复杂了,我将把您描述的“共享内存”方法与身份函数相结合,类似于法比奥在另一个答案中指出的那样。