Как безопасно отправить сообщение конкретному пользователю

Я использую ASP.NET MVC 5 и SignalR. Я хочу отправить сообщение конкретному пользователю. Я следовал за методом, который объяснен в этом уроке (также предложенный этим ответом).

Я переопределил IUserIdProvider, использовать UserId как идентификатор соединения.

public class SignalRUserIdProvider : IUserIdProvider
{
    public string GetUserId(IRequest request)
    {
        // use: UserId as connectionId
        return Convert.ToString(request.User.Identity.GetUserId<int>());
    }
}

И я внес изменения в свое приложение Startup использовать вышеуказанный пользовательский провайдер:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var idProvider = new SignalRUserIdProvider();
        GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
        ConfigureAuth(app);
        app.MapSignalR();
    }
}

Теперь мой клиент может передать целевой идентификатор клиента в концентратор, и концентратор будет пересылать сообщение только запрашиваемому клиенту, это мой концентратор:

[Authorize] 
public class ChatHub : Hub
{
    public void Send(string message, string destClientId)
    {
        Clients.User(destClientId).messageReceived(Context.User.Identity.Name + " says: " + message);
    }
}

Это работает отлично. Мой вопрос касается безопасности, и если это правильный подход для безопасного веб-сайта?

Согласно разделу Введение в безопасность SignalR, случайно сгенерированный идентификатор соединения является частью безопасности SignalR:

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

Вышеупомянутый подход заменяет случайно выбранный идентификатор соединения с фиксированным UserId... Есть ли проблемы безопасности с приведенным выше кодом?


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

2 ответа

Обновление

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

Выполните следующие действия для отправки сообщения с использованием случайно сгенерированного идентификатора соединения в защищенном виде:

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

public static class ChatHubUserHandler
{
    public static ConcurrentDictionary<string, ChatHubConnectionViewModel> ConnectedIds =
        new ConcurrentDictionary<string, ChatHubConnectionViewModel>(StringComparer.InvariantCultureIgnoreCase);
}

public class ChatHubConnectionViewModel
{
    public string UserName { get; set; }
    public HashSet<string> UserConnectionIds { get; set; }
}

Настройте ChatHub следующее

Чтобы сделать ChatHub обеспеченный добавить [Authorize] атрибут на ChatHub учебный класс.

[Authorize]
public class ChatHub : Hub
{
    private string UserName => Context.User.Identity.Name;
    private string ConnectionId => Context.ConnectionId;

    // Whenever a user will be online randomly generated connectionId for
    // him be stored here.Here I am storing this in Memory, if you want you
    // can store it on database too.
    public override Task OnConnected()
    {

        var user = ChatHubUserHandler.ConnectedIds.GetOrAdd(UserName, _ => new ChatHubConnectionViewModel
        {
            UserName = UserName,
            UserConnectionIds = new HashSet<string>()
        });

        lock (user.UserConnectionIds)
        {
            user.UserConnectionIds.Add(ConnectionId);
        }

        return base.OnConnected();
    }


    // Whenever a user will be offline his connectionId id will be
    // removed from the collection of loggedIn users.

    public override Task OnDisconnected(bool stopCalled)
    {
        ChatHubConnectionViewModel user;
        ChatHubUserHandler.ConnectedIds.TryGetValue(UserName, out user);

        if (user != null)
        {
            lock (user.UserConnectionIds)
            {
                user.UserConnectionIds.RemoveWhere(cid => cid.Equals(ConnectionId));
                if (!user.UserConnectionIds.Any())
                {
                    ChatHubUserHandler.ConnectedIds.TryRemove(UserName, out user);
                }
            }
        }

        return base.OnDisconnected(stopCalled);
    }
}

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

public class Message
{

    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long MessageId { get; set; }

    [ForeignKey("Sender")]
    public string SenderId { get; set; }

    [ForeignKey("Receiver")]
    public string ReceiverId { get; set; }

    [Required]
    [DataType(DataType.MultilineText)]
    public string MessageBody { get; set; }
    public DateTime MessageSentAt { get; set; }
    public bool IsRead { get; set; }


    public User Sender { get; set; }
    public User Receiver { get; set; }
}

Затем в контроллере сообщений:

Это всего лишь пример кода. Вы можете настроить код в соответствии с вашими потребностями.

[HttpPost]
public async Task<ActionResult> SendMessage(string messageBody, string receiverAspNetUserId)
{
      string loggedInUserId = User.Identity.GetUserId();
      Message message = new Message()
      {
            SenderId = loggedInUserId,
            ReceiverId = receiverAspNetUserId,
            MessageBody = messageBody,
            MessageSentAt = DateTime.UtcNow
      };

      _dbContext.Messages.Add(message);
      _dbContext.SaveChangesAsync();


      // Check here if the receiver is currently logged in. If logged in,
      // send push notification. Send your desired content as parameter
      // to sendPushNotification method.

      if(ChatHubUserHandler.ConnectedUsers.TryGetValue(receiverAspNetUserId, out ChatHubConnectionViewModel connectedUser))
      {
            List<string> userConnectionIds = connectedUser.UserConnectionIds.ToList();
            if (userConnectionIds.Count > 0)
            {
                var chatHubContext = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
                chatHubContext.Clients.Clients(userConnectionIds).sendPushNotification();
            }
      }

      return Json(true);
}

Теперь вопрос: что, если сообщение было отправлено, когда получатель был в автономном режиме?

Хорошо! В этом случае вы можете обработать push-уведомление двумя способами! вызов метода ajax или метода SignalR Hub, как только получатель подключится к сети, чтобы связать уведомления. Другой метод - частичное представление области уведомлений на странице макета. Я лично предпочитаю использовать частичное представление для области уведомлений.

Надеюсь, что это поможет вам!

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

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

Итак, TL;DR: используйте аутентификацию JWT или что-то с односторонней подписью, которое предотвращает подделку UserId.

В документации MS ничего не говорится о соображениях безопасности при объяснении поставщика IUserID, что, на мой взгляд, делает этот вопрос запутанным...

Я разместил тот же вопрос на форуме ASP.NET SignalR, и они подтвердили, что использование фиксированного ClientId в качестве connectionId является менее безопасным решением. Если безопасность является проблемой, тогда лучше всего использовать постоянное внешнее хранилище, так как идентификатор соединения генерируется случайным образом и его трудно угадать.

В случае моего приложения я продолжал использовать подход поставщика IUserID (менее безопасный вариант). Хотя я добавил некоторую проверку на стороне сервера, перед отправкой сообщения:

  1. Очевидно, используя: [Authorize]
  2. Я добавил механизм блокировки, и проверю, что Отправитель не заблокирован Получателем перед отправкой сообщения.
  3. Я также добавил механизм, при котором Отправитель может отправлять максимум 5 неотвеченных сообщений Получателю.
Другие вопросы по тегам