【问题标题】:SignalR C# MVC Mapping Anonymous User to Client IDSignalR C# MVC 将匿名用户映射到客户端 ID
【发布时间】:2016-02-02 09:00:31
【问题描述】:

问题:如何管理匿名用户,以便在 Hub 发出响应时更新单个浏览器中的多个选项卡?

场景如下:

我想将 SignalR 集成到一个项目中,以便匿名用户可以与操作员进行实时聊天。显然,已通过 iIdentity 进行身份验证的用户是通过 Client.User(username) 命令映射的。但目前说一个匿名用户正在浏览 site.com/tools 和 site.com/notTools 我无法将消息发送到只有一个 connectionID 的所有选项卡。只有一个选项卡收集响应。

我曾尝试使用 IWC 补丁,但该工具不考虑将聊天信息保存到数据库中,而且我认为通过 ajax 传递变量不是读取/写入数据库的安全方法。

我还查看了以下内容:Managing SignalR connections for Anonymous user

但是,创建这样的基础并使用会话似乎不如 Owin 安全。

我的想法是使用 client.user(),方法是在用户每次连接时创建一个虚假帐户,并在他们断开连接时将其删除。但我宁愿不要用垃圾帐户填充我的 aspnetusers db 上下文,这似乎没有必要。

是否可以使用UserHandler.ConnectedIds.Add(Context.ConnectionId); 来映射假用户名?我不知所措。

使用 iUserId 提供者有意义吗?

public interface IUserIdProvider
{
    string GetUserId(IRequest request);
}

然后创建一个将 IP 地址存储到 connectionID 和单个用户名的数据库?

数据库:

用户:使用 FK 连接 ID

|userid|username|IP       |connectionIDs|
|   1  | user1  |127.0.0.1|  1          |
|   2  | user2  |127.0.0.2|  2          |

连接ID:

|connectionID|SignalRID|connectionIDs|
|      1     | xx.x.x.x|     1       |
|      2     | xx.xxx.x|     2       |
|      3     | xx.xxxxx|     2       |
|      4     | xxxxx.xx|     2       |

那么可能围绕连接编写逻辑?

public override Task OnConnected()
    {
     if(Context.Identity.UserName != null){
      //add a connection based on the currently logged in user. 
     }
    else{
     ///save new user to database?
     } 
  }

但问题仍然存在,当在 websocket 上发送命令时,我将如何处理多个选项卡?

更新 需要明确的是,我的意图是创建一个实时聊天/支持工具,该工具始终允许在浏览器中进行匿名访问。

客户想要类似于http://www.livechatinc.com的东西

我已经创建了一个从集线器发送和接收的 javascript 插件,无论它位于哪个域中。 (我的客户有多个站点)拼图的缺失部分是最简单的,管理匿名用户以允许多标签对话。

【问题讨论】:

  • 您希望匿名用户能够从任何打开的浏览器选项卡与同一站点(即 site.com)聊天,对吗?
  • @tede24 是的,这就是我想要的。
  • 你不能用某种 SessionId (Guid) 设置一个 cookie,为那个 Id 创建一个组并订阅那里的所有 connectionids 吗?这样您就可以将消息广播到所有选项卡
  • 这到底是怎么回事?您是否尝试过解决方案或找到其他解决方案?

标签: c# asp.net-mvc-5 signalr


【解决方案1】:

我没有遵循虚假用户帐户的想法(不知道它是否有效),但会开发一个替代方案。

该目标可以通过所有浏览器选项卡共享的 ChatSessionId cookie 并创建名为此 ChatSessionId 的组来实现。

我从 asp.net/signalr 学习了基本的聊天教程,并添加了允许从多个选项卡作为同一用户聊天的功能。

1) 在“聊天”操作中分配一个聊天会话 ID 以识别用户,因为我们没有用户凭据:

    public ActionResult Chat()
    {
        ChatSessionHelper.SetChatSessionCookie();
        return View();
    }

2) 进入聊天页面时订阅聊天会话

客户端

    $.connection.hub.start()
        .then(function(){chat.server.joinChatSession();})
        .done(function() {
           ...

服务器端(集线器)

public Task JoinChatSession()
{
    //get user SessionId to allow use multiple tabs
    var sessionId = ChatSessionHelper.GetChatSessionId();
    if (string.IsNullOrEmpty(sessionId)) throw new InvalidOperationException("No chat session id");

    return Groups.Add(Context.ConnectionId, sessionId);
}

3) 向用户的聊天会话广播消息

public void Send(string message)
{
    //get user chat session id
    var sessionId = ChatSessionHelper.GetChatSessionId();
    if (string.IsNullOrEmpty(sessionId)) throw new InvalidOperationException("No chat session id");

    //now message will appear in all tabs
    Clients.Group(sessionId).addNewMessageToPage(message);
}

最后是(简单的)ChatSessionHelper 类

public static class ChatSessionHelper
{
    public static void SetChatSessionCookie()
    {
        var context = HttpContext.Current;
        HttpCookie cookie = context.Request.Cookies.Get("Session") ?? new HttpCookie("Session", GenerateChatSessionId());

        context.Response.Cookies.Add(cookie);
    }

    public static string GetChatSessionId()
    {
        return HttpContext.Current.Request.Cookies.Get("Session")?.Value;
    }

    public static string GenerateChatSessionId()
    {
        return Guid.NewGuid().ToString();
    }
}

【讨论】:

  • 虽然您的回答会起作用,但它不符合我所需要的,这是一个安全风格的聊天系统。任何人都可以伪造他们的 cookie 并访问他们不打算使用的聊天。我需要一个解决方案,该解决方案将根据无法伪造的数据管理 ids……很容易。 @tede24
  • 此方案适用于一个域,但没有考虑到两个域需要共享 cookie。
  • @chris 两个域没有在问题中说明,示例始终是 site.com/xx。无论如何,您可以在需要时通过适当的 cookie 设置共享 cookie
  • @chris 关于安全性,我不明白如何从客户端发送和 ID 并将其用作组(Dan 的回答)可能比从服务器生成 GUID 更安全..
  • 虽然 Dans 的回答要求客户分配 ID,但我可以使用他的代码来形成适合我的设计,方法是创建一个可以由我的服务器上的 WebAPI 创建的哈希字符串,更多因此您的代码特别要求来自 RAZOR 中的视图的帮助程序类返回 cookie。您的两个代码都可以使用,但需要不同的用例。他的用例是我最需要的。一旦我有一个特别适合我的代码,我就会自己提供这个问题的答案。
【解决方案2】:

一种广泛采用的解决方案是让用户在 onConnected 上使用他的某种带有连接返回的 id 进行注册。

 public override Task OnConnected()
        {
            Clients.Caller.Register();


            return base.OnConnected();
        }

然后用户返回带有某种你自己的 id 逻辑的调用

来自客户的注册方法

 public void Register(Guid userId)
    {
        s_ConnectionCache.Add(userId, Guid.Parse(Context.ConnectionId));
    }

并且您将用户 ID 保存在静态字典中(注意锁,因为您需要它是线程安全的;

static readonly IConnectionCache s_ConnectionCache = new ConnectionsCache();

这里

 public class ConnectionsCache :IConnectionCache
{
    private readonly Dictionary<Guid, UserConnections> m_UserConnections = new Dictionary<Guid, UserConnections>();
    private readonly Dictionary<Guid,Guid>  m_ConnectionsToUsersMapping = new Dictionary<Guid, Guid>();
    readonly object m_UserLock = new object();
    readonly object m_ConnectionLock = new object();
    #region Public


    public UserConnections this[Guid index] 
        => 
        m_UserConnections.ContainsKey(index)
        ?m_UserConnections[index]:new UserConnections();

    public void Add(Guid userId, Guid connectionId)
    {
        lock (m_UserLock)
        {
            if (m_UserConnections.ContainsKey(userId))
            {
                if (!m_UserConnections[userId].Contains(connectionId))
                {

                    m_UserConnections[userId].Add(connectionId);

                }

            }
            else
            {
                m_UserConnections.Add(userId, new UserConnections() {connectionId});
            }
        }


            lock (m_ConnectionLock)
            {
                if (m_ConnectionsToUsersMapping.ContainsKey(connectionId))
                {
                    m_ConnectionsToUsersMapping[connectionId] = userId;
                }
                else
                {
                        m_ConnectionsToUsersMapping.Add(connectionId, userId);
                }
            }

    }

    public void Remove(Guid connectionId)
    {
        lock (m_ConnectionLock)
        {
            if (!m_ConnectionsToUsersMapping.ContainsKey(connectionId))
            {
                return;
            }
            var userId = m_ConnectionsToUsersMapping[connectionId];
            m_ConnectionsToUsersMapping.Remove(connectionId);
            m_UserConnections[userId].Remove(connectionId);
        }



    }

从 Android 应用程序注册的示例调用

 mChatHub.invoke("Register", PrefUtils.MY_USER_ID).get();

对于 JS 来说是一样的

 chat.client.register = function () {
    chat.server.register(SOME_USER_ID);
}

【讨论】:

  • 虽然您的示例在此期间可以工作,但它并不能解决我的直接问题。该应用程序已经存在,但是我们无法处理并发选项卡。这是唯一的问题。我们不想强迫用户注册访问我们的网站。当用户连接并开始对话时,会收集姓名、电子邮件、查询描述等数据。连接到“已注册”对话时,其他未“注册”的选项卡不会立即获得聊天对话。
  • 用户有一个昵称 - 这是你的“分组”令牌 - 目标不是让用户注册,目标是将他的连接组合在一起,就像你回复 Clients.All( )。您可以使用动态客户端轻松获取相关连接 = Clients.Clients(userConnections.Select(x => x.ToString()).ToList());而不是仅将数据推送到这些连接。我在android客户端上使用的是相同的,我没有任何注册
  • 我将您的回复标记为答案。它在过渡期间有效,我会在以后发展它。
  • 也很高兴看到你以后有没有更好的解决方案
猜你喜欢
  • 1970-01-01
  • 2017-04-20
  • 1970-01-01
  • 2010-11-27
  • 2010-10-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多