如何防止SAAS应用程序中的多次登录?

时间:2018-09-04 14:27:19

标签: asp.net-core

我需要做什么

我正在使用ASP.NET CORE开发应用程序,实际上我在使用Identity实现时遇到了问题。

事实上,在官方文档中没有关于多会话的参考,这很糟糕,因为我开发了SaaS应用程序;特别是用户订阅了付费计划以访问一组特定功能,并且他可以将其凭据提供给其他用户,以便他们可以免费使用,这是一个非常糟糕的情况,我会浪费很多金钱和时间。

我该怎么办

在网上搜索了很多之后,我发现了许多针对较旧版本的ASP.NET CORE的解决方案,因此我无法进行测试,但我了解通常此问题的解决方案与存储用户有关数据库中的时间戳(这是登录时生成的GUID),因此每次用户访问受限页面并且有更多会话(具有不同的用户时间戳)时,旧会话都会关闭。

我不喜欢这种解决方案,因为用户可以轻松复制浏览器的cookie并将其共享给其他用户。

我虽然将登录的用户会话的信息存储在数据库中,但这也需要大量的连接。.因此,我对ASP.NET CORE的经验不足以及网络资源的匮乏使我陷入了困境。混乱。

有人可以分享一个通用的想法来实施一种安全的解决方案,以防止多用户登录吗?

4 个答案:

答案 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/

如果这对于您的用例而言太复杂了,我将把您描述的“共享内存”方法与身份函数相结合,类似于法比奥在另一个答案中指出的那样。