Предотвращение нескольких логинов

Я пытаюсь заблокировать несколько логинов с одним и тем же пользователем в своем приложении.
Моя идея - обновить штамп безопасности при входе пользователя и добавить его в качестве претензии, а затем в каждом отдельном запросе, сравнивая штамп с куки файлом с таковым в базе данных. Вот как я реализовал это:

        public virtual async Task<ActionResult> Login([Bind(Include = "Email,Password,RememberMe")] LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        SignInStatus result =
            await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);
        switch (result)
        {
            case SignInStatus.Success:
                var user = UserManager.FindByEmail(model.Email);
                var id = user.Id;
                UserManager.UpdateSecurityStamp(user.Id);
                var securityStamp = UserManager.FindByEmail(model.Email).SecurityStamp;
                UserManager.AddClaim(id, new Claim("SecurityStamp", securityStamp));

Затем в конфигурации аутентификации я добавил

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                OnValidateIdentity = ctx =>
                {
                    var ret = Task.Run(() =>
                    {
                        Claim claim = ctx.Identity.FindFirst("SecurityStamp");
                        if (claim != null)
                        {
                            var userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
                            var user = userManager.FindById(ctx.Identity.GetUserId());

                            // invalidate session, if SecurityStamp has changed
                            if (user != null && user.SecurityStamp != null && user.SecurityStamp != claim.Value)
                            {
                                ctx.RejectIdentity();
                            }
                        }
                    });
                    return ret;
                }
            }

        });

Как показано на рисунке, я попытался сравнить заявку из файла cookie с той, что находится в базе данных, и отклонить идентификатор, если они не совпадают.
Теперь, каждый раз, когда пользователь подписывается в марке безопасности, обновляется, но значение отличается от cookie пользователя, которое я не могу понять, почему? Я подозрительно, может быть, новый обновленный штамп безопасности не сохраняется в пользовательском cookie?

Ответы

Ответ 1

Решение несколько проще, чем вы начали внедрять. Но идея такая же: каждый раз, когда пользователь входит в систему, измените свою марку безопасности. И это приведет к аннулированию всех других сеансов входа в систему. Таким образом, мы научим пользователей не делиться своим паролем.

Я только что создал новое приложение MVC5 из стандартного шаблона VS2013 и успешно выполнил то, что вы хотите сделать.

Метод входа. Вам нужно изменить штамп безопасности перед созданием файла cookie, поскольку после того, как файл cookie установлен, вы не можете легко обновить значения:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }


    // check if username/password pair match.
    var loggedinUser = await UserManager.FindAsync(model.Email, model.Password);
    if (loggedinUser != null)
    {
        // change the security stamp only on correct username/password
        await UserManager.UpdateSecurityStampAsync(loggedinUser.Id);
    }

     // do sign-in
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

Таким образом, каждый логин будет делать обновление в записи пользователя с новой маркой безопасности. Обновление штампа безопасности - это всего лишь вопрос await UserManager.UpdateSecurityStampAsync(user.Id); - гораздо проще, чем вы предполагали.

Следующий шаг - проверить печать безопасности по каждому запросу. Вы уже нашли лучший крючок в Startup.Auth.cs, но вы снова слишком сложны. Рамка уже делает то, что вам нужно сделать, вам нужно немного ее настроить:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    // other stuff
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(0), // <-- Note the timer is set for zero
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
});            

Временной интервал установлен на ноль - означает, что фреймворк в каждом запросе будет сравнивать печать безопасности пользователя с базой данных. Если штамп в cookie не совпадает с штампом в базе данных, пользовательское auth-cookie выкидывается, прося его выйти из системы.

Однако обратите внимание, что это будет иметь дополнительный запрос к вашей базе данных по каждому запросу HTTP от пользователя. На большой базе пользователей это может быть дорогостоящим, и вы можете несколько увеличить интервал проверки до пары минут - это даст вам меньше запросов к вашей базе данных, но все равно будет нести ваше сообщение о том, что вы не будете делиться регистрационными данными.


Полный источник в github


Дополнительная информация в блоге

Ответ 2

В прошлом я использовал IAuthorizationFilter и статическую входящую в систему коллекцию пользователей для достижения этой цели:

public static class WebAppData
{
     public static ConcurrentDictionary<string, AppUser> Users = new ConcurrentDictionary<string, AppUser>();
}

public class AuthorisationAttribute : FilterAttribute, IAuthorizationFilter {

    public void OnAuthorization(AuthorizationContext filterContext){

            ...
            Handle claims authentication
            ...

            AppUser id = WebAppData.Users.Where(u=>u.Key ==userName).Select(u=>u.Value).FirstOrDefault();
            if (id == null){
                id = new AppUser {...} ;
                id.SessionId = filterContext.HttpContext.Session.SessionID;
                WebAppData.Users.TryAdd(userName, id);
            }
            else
            {
                if (id.SessionId != filterContext.HttpContext.Session.SessionID)
                {
                        FormsAuthentication.SignOut();
                        ...
                        return appropriate error response depending is it ajax request or not
                        ...


                }
            } 
     }
}

При выходе из системы:

WebAppData.Users.TryRemove(userName, out user)