Как запретить пользователю входить в систему на нескольких устройствах и как мне завершить сеанс с предыдущего устройства?

#c# #asp.net #asp.net-core #asp.net-identity

#c# #asp.net #asp.net-core #asp.net-identity

Вопрос:

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

Я использую dotnet 5.0

Вам нужен столбец с именем currentlyloggedinUser в таблице «пользователи» в базе данных, чтобы узнать, вошли ли они в систему с помощью Boolean?

Вам также понадобится печать безопасности?

Ниже приведен метод входа в систему, который помещается в AuthController

 [AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(UserForLoginDto userForLoginDto)
{

    try 
    {
        // does the user exist in the database
        var user = await _userManager.FindByNameAsync(userForLoginDto.Username);
        // does the password match
        var result = await _signInManager.CheckPasswordSignInAsync(user, userForLoginDto.Password, true);

        //if the users are logged in, store True Value in the database 
            
            user.nonconcurrent = true; 
            var noLoggedin = await _userManager.UpdateAsync(user);

        if(!result.IsLockedOut)
        {
            if(user.IsEnabled)
            {
                if (result.Succeeded)
                {   
                    var userToReturn = _mapper.Map<UserForReturnDto>(user); 

                    return Ok(new
                    {
                        token = GenerateJwtToken(user).Result,
                        user = userToReturn,                 
                    });
                }
                return Unauthorized("Login Failed"); //if username and password are incorrect return unauthorised
            }
            return Unauthorized("This account is disabled. Please get an administrator to unlock this account.");                               
        }                
        return Unauthorized("This account is locked out. Please try again in 10 minutes or get an "   
            "administrator to unlock your account.");              
    }
    catch (ArgumentNullException) 
    {
        return Unauthorized("Login Failed");
    }
}
 

Ниже приведена часть StartUp.cs

  namespace Schedular.API
{
    public class Startup
    {
      public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // services.AddDbContext<DataContext>(x => x.UseMySql(Configuration
            //     .GetConnectionString("DefaultConnection")));

            services.AddDbContext<DataContext>(x => x.UseMySql(Configuration
                .GetConnectionString("DefaultConnection"),
                        new MySqlServerVersion(new Version(8, 0, 21)), 
                        mySqlOptions => mySqlOptions
                            .CharSetBehavior(CharSetBehavior.NeverAppend)));
 
            var lockoutOptions = new LockoutOptions()
            {
                AllowedForNewUsers = true,
                DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10),
                MaxFailedAccessAttempts = 5
            };
                
            // for role and identity authentication
            // initial creation of user in the user table in database 
            IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
            {
                // password requirements
                opt.Lockout = lockoutOptions;
                opt.Password.RequireDigit = true;
                opt.Password.RequiredLength = 8;
                opt.Password.RequireNonAlphanumeric = false;
                opt.Password.RequireUppercase = true;
            });

            builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
            builder.AddEntityFrameworkStores<DataContext>();
            builder.AddRoleValidator<RoleValidator<Role>>();
            builder.AddRoleManager<RoleManager<Role>>();
            builder.AddSignInManager<SignInManager<User>>();

            
            // allows api to use authentication 
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => 
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
                            .GetBytes(Configuration.GetSection("AppSettings:Token").Value)),
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                });  


            // authorisation policy
            services.AddAuthorization(options =>
            {
                options.AddPolicy("AdminAccess", policy => policy.RequireRole("Admin"));
                options.AddPolicy("everyone", policy => policy.RequireRole("Admin", "standard"));
            });

            services.AddControllers(options => 
            {
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                
                options.Filters.Add(new AuthorizeFilter(policy));
            })
             .AddNewtonsoftJson(opt =>
            {
                opt.SerializerSettings.ReferenceLoopHandling = 
                Newtonsoft.Json.ReferenceLoopHandling.Ignore;
            }); 
            
            services.AddScoped<ITaskScheduleRepository, TaskScheduleRepository>();
            services.AddScoped<IUserRepository, UserRepository>();
            services.AddScoped<INotesRepository, NotesRepository>();
            services.AddScoped<IAttachmentFileRepository, AttachmentFileRepository>();
            services.AddScoped<ICustomerRepository, CustomerRepository>();
            services.AddScoped<IReportRepository, ReportRepository>();
            services.AddControllers();
            services.AddCors(); 
        


            //allows use of tokens
  

            // Auto Mapper Configurations
            var mappingConfig = new MapperConfiguration(mc =>
            {
                mc.AddProfile(new AutoMapperProfiles());
            });
            IMapper mapper = mappingConfig.CreateMapper();
            services.AddSingleton(mapper);            
        }
 

Это файл модели user.cs

 using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;

namespace Schedular.API.Models
{
    public class User: IdentityUser<int>
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        // to disable account and have a time limiter on when it can be enabled again

        public bool nonconcurrent { get; set; }

        // connect User table to the userRole join table. the below configure the relationship
        public virtual ICollection<UserRole> UserRoles {get; set;}
    }
}
 

Ниже приведен userForLoginDto

 namespace Schedular.API.Dtos
{
    public class UserForLoginDto
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}
 

Комментарии:

1. Зачем вам нужно было запретить одновременный вход пользователя в систему?

Ответ №1:

Есть несколько способов, которыми вы могли бы добиться этого. Для простой отправной точки я предоставлю две возможности.

Первым может быть включение IP-адреса пользователя в ваши данные (база данных, кэш, токен и т. Д.). Затем вы можете проверить, используется ли IP-адрес первого входа в систему, и заблокировать все другие попытки входа или последующие запросы с любых других IP-адресов для данного пользователя.

Простой способ сделать это может выглядеть примерно так, как показано ниже:

 public class UserForLoginDto
{
    public string Username { get; set; }
    public string Password { get; set; }
    public string CurrentIpAddress { get; set; }
}
 

Затем вы можете извлечь IP-адрес из контекста запроса или http и сравнить для проверки при входе в систему следующим образом:

 if(HttpContext.Connection.RemoteIpAddress == userForLoginDto.CurrentIpAddress)
{
    // do things here after validation.
}
else
{
    // kick-out invalid login attempt.
}
 

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

Это было бы хорошо для проверки концепции, но не обязательно идеально для производства, поскольку вы можете столкнуться с проблемами, когда IP-адреса не всегда уникальны.


Во-вторых, вместо IP-адреса, который не всегда уникален, вы могли бы вместо этого сгенерировать GUID, UUID или какие-либо другие уникальные данные, которые передаются через токены и сохраняются данные, т.Е. в базе данных.

 public class UserForLoginDto
{
    public string Username { get; set; }
    public string Password { get; set; }
    public Guid CurrentLogin { get; set; }
}
 

Затем выполните ту же проверку, что и раньше, при входе в систему:

 if(Request.Headers.GetValues("your-token-here").FirstOrDefault() == userForLoginDto.CurrentLogin)
{
    // do things here after validation.
}
else
{
    // kick-out invalid login attempt.
}
 

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

Вы также захотите создавать новую уникальную точку данных при каждом успешном входе в систему, и если она уже существует, вы можете либо завершить предыдущий сеанс, либо аннулировать попытку входа в систему, чтобы предотвратить одновременное использование нескольких устройств.


Попробуйте изучить JWT (веб-токены Json) для подходов, основанных на токенах. Существует множество отличных примеров и руководств, таких как аутентификация JWT с помощью C # или создание и проверка токенов jwt в asp net core.

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

Комментарии:

1. Это может привести к сбою, если пользователь использует один и тот же Wi-Fi (т. Е. Маршрутизатор шлюза с одним NAT-адресом) со своего ноутбука и мобильного телефона. Что может работать лучше, так это установить некоторый случайный токен и использовать его вместо IP-адреса.

2. Это также приведет к сбою, если сайт размещен в ферме серверов (много экземпляров одного и того же сайта)

3. @DarcyThomas отличный момент, как я уже сказал, он не идеален, но может быть полезен для poc в сжатые сроки. Обновленный ответ, чтобы лучше отразить это.

Ответ №2:

Перед созданием и возвратом токена сеанса получите текущий активный токен сеанса для пользователя, сделайте его недействительным, а затем верните новый. С помощью этого мы можем убедиться, что в данный момент времени существует только один допустимый сеанс.