如何安全地向特定用户发送消息

时间:2018-09-06 00:32:49

标签: asp.net-mvc signalr asp.net-identity signalr-2

我正在使用ASP.NET MVC 5和SignalR。我想向特定用户发送消息。我遵循了this tutorial(也由this answer建议)中说明的方法。

我已覆盖IUserIdProvider,将UserId用作 connectionId

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

现在,我的客户可以将目标 ClientID 传递给中心,中心将仅将邮件转发给所请求的客户,这就是我的中心:

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

这一切正常。我的问题是关于安全性,这是否是安全网站的正确方法?

根据Introduction to SignalR Security,随机生成的连接ID是SignalR安全性的一部分:

  

服务器不会处理来自连接ID的任何请求   与用户名不匹配。恶意用户不太可能猜到   有效请求,因为恶意用户必须知道该用户   名称和当前随机生成的连接ID。

以上方法是用固定的UserId替换随机选择的 connectionId ...以上代码是否存在安全问题?


注意:我正在一个电子商务网站上,即使用户离线,用户也需要能够接收消息(消息将存储在数据库中并且他们可以在线阅读)。

3 个答案:

答案 0 :(得分:1)

更新于

  

注意:我正在一个电子商务网站上,即使用户处于脱机状态,用户也需要能够接收消息(消息将存储在DB中,并且一旦联机就可以阅读)。

执行以下操作,以安全的方式使用随机生成的connectionId发送消息:

首先添加以下类来跟踪用户的connectionId,以了解用户是在线还是离线

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

现在的问题是,如果接收方离线时是否发送了消息,该怎么办?

好吧!在这种情况下,您可以通过两种方式处理推送通知!接收者在线后立即调用ajax方法或SignalR Hub方法来绑定通知。另一种方法是将局部视图用于布局页面中的通知区域。我个人更喜欢将局部视图用于通知区域。

希望这对您有帮助!

答案 1 :(得分:1)

您在通往正确解决方案的途中进展顺利。唯一的技巧是,应设置安全性,以免欺骗他人的UserId。

例如,我们在SignalR上具有完全相同的场景,但是我们使用JWT令牌中的UserId声明来告诉您您是谁。因此,如果您想接收该人的消息,则需要知道该人的登录凭据。您不能只更改声明中的UserId,因为这样JWT签名将无效,并且您将不再得到身份验证或授权。

所以TL; DR:使用JWT身份验证或带有单向签名的东西,以防止篡改UserId。

答案 2 :(得分:0)

MS文档在解释IUserID provider时没有提及任何安全方面的考虑,我认为这使事情变得混乱...

我在ASP.NET SignalR Forum上发布了相同的问题,他们确认使用固定的ClientId作为connectionId是不太安全的解决方案。如果需要考虑安全性,那么Permanent, external storage是您最好的选择,因为connectionId是随机生成的并且很难猜测。

对于我的应用程序,我继续使用IUserID provider方法(不太安全的选项)。尽管我确实在发送消息之前在服务器端添加了一些验证:

  1. 显然使用:[Authorize]
  2. 我添加了阻止机制,并将在发送消息之前验证发件人是否被接收者阻止。
  3. 我还添加了一种机制,发件人最多可以向 Receiver 发送10条未回复的消息。