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

How to securely send a message to a specific user

我正在使用 ASP.NET MVC 5 和 SignalR。我想向特定用户发送消息。我已经按照 this tutorial (also suggested by 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 安全的一部分:

The server does not process any request from a connection id that does not match the user name. It is unlikely a malicious user could guess a valid request because the malicious user would have to know the user name and the current randomly-generated connection id.

上面的方法是用固定的UserId替换随机选择的connectionId...上面的代码有什么安全问题吗?


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

更新于

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).

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

首先添加以下classes来跟踪用户的connectionIds以了解用户是在线还是离线

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 安全,请在 ChatHub class 上添加 [Authorize] 属性。

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

现在使用下面的模型class将消息存储到数据库中。您还可以根据自己的具体需要自定义消息 class。

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 方法来绑定通知。另一种方法是在布局页面中对通知区域使用局部视图。我个人更喜欢在通知区域使用部分视图。

希望对您有所帮助!

MS 文档在解释 IUserID provider 时没有提及任何有关安全注意事项的内容,在我看来,这使问题变得混乱...

我在 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 上发布了同样的问题是你最好的选择,因为 connectionId 是随机生成的并且很难猜测。

在我的应用程序中,我继续使用 IUserID provider 方法(安全性较低的选项)。虽然我确实在服务器端添加了一些验证,但在发送消息之前:

  1. 显然使用:[Authorize]
  2. 我添加了拦截机制,在发送消息前会验证Sender没有被Receiver拦截。
  3. 我还添加了一种机制,即 Sender 最多可以向 Receiver 发送 10 条未回复的消息。

您已经找到了正确的解决方案。唯一的技巧是您的安全设置应确保您不能欺骗其他人的 UserId。

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

所以 TL;DR:使用 JWT 身份验证或带有 one-way 签名的东西来防止篡改 UserId。