How to securely send a message to a specific user

匆匆过客 提交于 2019-12-05 05:33:06

Updated on

Note: I am working on an e-commerce website where users need to be able to receive messages even if they are offline (the messages would be stored in DB and they can read once the are online).

Do the followings to send message using randomly generated connectionId in secured way:

First add the following classes to track user's connectionIds to understand whether the user is online or offline

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; }
}

Configure the ChatHub as follows

To make the ChatHub secured add [Authorize] attribute on the ChatHub class.

[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);
    }
}

Now use the following model class to store the message to the database. You can also customize the message class according to your exact need.

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; }
}

Then in the Message Controller:

This is just an example code. You can customize the code according to your exact need.

[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);
}

Now question is what if the message was sent whenever the receiver was offline?

Okay! In this case you can handle the push notification in two ways! calling a ajax method or SignalR Hub method as soon as the receiver is online to bind the notifications. Another method is using partial view for notification area in layout page. I personally prefer using partial view for notification area.

Hope this will help you!

MS Documentation does not mention anything about security considerations when explaining IUserID provider, which, in my opinion makes the matter confusing...

I posted the same question on ASP.NET SignalR Forum, and they confirmed that using a fixed ClientId as connectionId is a less secure solution. If security is a concern, then the Permanent, external storage is your best bet, because connectionId is randomly generated and hard to guess.

In the case of my application, I continued using the IUserID provider approach (the less secure option). Though I did add some validation on the server side, before sending the message:

  1. Obviously using: [Authorize]
  2. I added a blocking mechanism, and will validate the Sender is not blocked by the Receiver before sending a message.
  3. I have also added a mechanism, that a Sender can send max 10 un-replied messages to the Receiver.
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!