如何使用SignalR向Blazor Server中的特定用户发送消息? [英] How To Send Message To Specific User In Blazor Server Using SignalR?

查看:39
本文介绍了如何使用SignalR向Blazor Server中的特定用户发送消息?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经实现了一个简单的公共聊天室系统,使用Blazor服务器和带数据库的SignalR来存储用户名和消息。在系统中,用户只需输入聊天名称即可加入聊天,出现聊天界面。

现在,我想要添加另一个功能,可以将消息发送给该公共聊天室中的特定用户。
任何帮助都会很好,谢谢你。

以下是我的公共聊天室代码。
下面是我的集线器

    public class Chat:Hub
    {
        public async Task SendMessage(Message message)
        {
            await Clients.All.SendAsync("Receiver", message);
        }
    }

下面是在数据库中保存用户名和消息的代码

    public string MsgBody { get; set; }
    public string UserName { get; set; }
    public Message chatmsg { get; set; } = new Message();
    public bool isChatting = false;
    public string errorMsg;
    public List<Message> messages = new List<Message>();
    public List<Message> messagesList = new List<Message>();
    public HubConnection hubConnection;
    [Inject]
    public NavigationManager NavigationManager { get; set; }
    [Inject]
    public MainService mainService { get; set; }
    public async Task SendAsync()
    {
        chatmsg.UsrName = UserName;
        chatmsg.MessageBody = MsgBody;
        mainService.SaveMessage(chatmsg);
        await hubConnection.SendAsync("SendMessage", chatmsg);
        MsgBody = string.Empty;
        chatmsg = new Message();
    }

以下是加载数据的代码
    public async Task Chat()
    {
        if (string.IsNullOrWhiteSpace(UserName))
        {
            errorMsg = "Enter your name";
        }
        try
        {
            isChatting = true;
            messagesList.Clear();
            hubConnection = new HubConnectionBuilder().WithUrl(NavigationManager.ToAbsoluteUri("/chat")).Build();
            hubConnection.ServerTimeout = TimeSpan.FromMinutes(60);
            hubConnection.On<Message>("Receiver", BroadcastMessage);
            await hubConnection.StartAsync();
            LoadMessage();
            await ChatJoinLeftMessage($"[Notice] {UserName} joined chat room.");                
        }
        catch (Exception e)
        {

            errorMsg = $"ERROR: Failed to start chat client: {e.Message}";
            isChatting = false;
        }
    }
    private void BroadcastMessage(Message message)
    {
        bool isMine = message.UsrName.Equals(UserName, StringComparison.OrdinalIgnoreCase);
        messagesList.Add(new Message(message.UsrName, message.MessageBody, isMine));    
        StateHasChanged();
    }

    private void LoadMessage()
    {
        messages = mainService.GetAllMessages();
        foreach (var item in messages)
        {
            bool isMine = item.UsrName.Equals(UserName, StringComparison.OrdinalIgnoreCase);
            messagesList.Add(new Message(item.UsrName, item.MessageBody, isMine));
        }
    }

下面是我的UI
@if (!isChatting)
{
<div class="col-lg-5">
    <p>Enter your name to start chatting:</p>

    <div class="input-group  my-3">
        <input @bind="UserName" type="text" class="form-control my-input">
        <div class="input-group-append">
            <button class="btn btn-outline-secondary" type="button" @onclick="@Chat"><span class="oi oi-chat" aria-hidden="true"></span> Chat!</button>
        </div>
    </div>
</div>
   if (errorMsg != null)
   {
    <div class="col-lg-5">
        <small id="emailHelp" class="form-text text-danger">@errorMsg</small>
    </div>
   }
}
else
{
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-person mr-2" aria-hidden="true"></span>
    <span>you are connected as <b>@UserName</b></span>
    <button class="btn btn-sm btn-warning ml-md-auto" @onclick="@DisconnectAsync">disconnect</button>
</div>
<div id="scrollbox">
    @foreach (var item in messagesList)
    {
        @if (item.IsNotice)
        {
            <div class="alert alert-info">@item.MessageBody</div>
        }
        else
        {
            <div class="@item.CSS">
                <div class="user">@item.UsrName</div>
                <div class="msg">@item.MessageBody</div>
            </div>
        }
    }
    <hr />
    <textarea class="input-lg" placeholder="enter your comment" @bind="MsgBody"></textarea>
    <button class="btn btn-default" @onclick="()=>SendAsync()">Send</button>
</div>
}

推荐答案

这是如何做到这一点的完整解决方案。您可以从Github

下载正在运行且已更新的代码示例 注意:此答案的目的不是如何创建SignlR应用程序,以及如何管理用户。文档和许多其他教程中都显示了这一点。但有一点是欠缺的,那就是如何保护SignalR集线器的端点,以及如何使用户的声明在Blazor Server App的集线器中可用。我没有找到一个使用Blazor服务器应用的例子。我向Blazor团队寻求一些提示,但无济于事…

一些说明

注意:在Blazor Server App的前端对您的用户进行身份验证不会使您有权访问集线器上受保护的端点。您应该像对待Web Api端点一样对待Hub,这要求您在执行对它的HTTP调用时传递访问令牌。例如,如果要使用HttpClient服务将数据从Web Api中的WeatherForecastController检索到FetchData页面,则需要在Authorization标头中传递访问令牌

使用带有API身份验证的WebAssembly应用程序时,可以在创建集线器连接时将访问令牌传递给集线器。这很简单,文档中有一个示例代码演示了这一点,实际上您不需要做太多事情来保护集线器和访问.,即使这样也有一些问题需要处理,因为在集线器中只有UserIdentifier可以访问,而不是所有用户的声明。

但是,这里的答案是关于Blazor Server App,解决方案是将安全Cookie(";.AspNetCore.Identity.Application";)传递给集线器。因此,解决该问题的第一步是在呈现Blazor SPA之前从HttpContext捕获Cookie,并将Cookie作为发送到App组件的参数传递给Blazor App。既然在Blazor App中提供了Cookie,您就可以从聊天页面访问它并将其传递给Hub。请注意,与带有SignalR的WebAssembly App示例不同,所有ClaimRule对象在Hub中都可用,并且您可以访问其所有声明,例如:

var user = Context.User.Identity.Name
var userid = Context.UserIdentifier

_Host.cshtml

    @{
        // Capture the security cookie when the the initial call
        // to the Blazor is being executed. Initial call is when 
        // the user type the url of your Blazor App in the address
        // bar and press enter, or when the user is being redirected 
        // from the Login form to your Blazor SPA
        // See more here: https://stackoverflow.com/a/59538319/6152891 
         var cookie = 
     HttpContext.Request.Cookies[".AspNetCore.Identity.Application"];
    
    }


  <body>
     @* Pass the captured Cookie to the App component as a paramter*@
    <component type="typeof(App)" render-mode="Server" param- 
       Cookie="cookie" />
  </body>

App.razor

@inject CookiesProvider CookiesProvider

@* code omitted here... *@

@code{

    [Parameter]
    public string Cookie { get; set; }

    protected override Task OnInitializedAsync()
    {
        // Pass the Cookie parameter to the CookiesProvider service
        // which is to be injected into the Chat component, and then 
        // passed to the Hub via the hub connection builder
        CookiesProvider.Cookie = Cookie;

        return base.OnInitializedAsync();
    }
}

CookiesProvider.cs(完整代码)

using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SignalRServerIdentityAuthentication
{
    public class CookiesProvider
    {
        public string Cookie { get; set; }
    }
}

Startup.ConfigureService

 services.AddScoped<CookiesProvider>();
 services.AddSignalR();

启动。配置

 app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapBlazorHub();
                endpoints.MapHub<ChatHub>("/chatHub");
                endpoints.MapFallbackToPage("/_Host");
            });
请注意,NavMenu包含AuthorizeView组件,其目的是阻止用户访问聊天组件,除非用户已通过身份验证。另请注意,聊天页受Authorize属性保护。

NavMenu.razor

<li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
        <AuthorizeView>
            <li class="nav-item px-3">
                <NavLink class="nav-link" href="chat">
                    <span class="oi oi-chat" aria-hidden="true"></span> Chat
                </NavLink>
            </li>
        </AuthorizeView>

Chat.razor(完整代码)

    @page "/chat"

    @attribute [Authorize]

@using Microsoft.AspNetCore.SignalR.Client
@using Microsoft.AspNetCore.SignalR

@using SignalRServerIdentityAuthentication.Hubs

@inject NavigationManager NavigationManager


@using System.Net.Http
@using System.Net.Http.Json

@using System;

@using System.Net.Http.Headers;
@using System.Threading.Tasks;
@using Microsoft.AspNetCore.Http.Connections;
@using System.Net


@implements IAsyncDisposable
<p>@messageForBoard</p>
<hr />

<div>
    <label for="user">User:</label>
    <span id="user">@userName</span>
</div>
<div class="form-group">
    <label for="messageInput">Message:</label>
    <input onfocus="this.select();" @ref="elementRef" id="messageInput" @bind="messageInput" class="form-control my-input"/>
</div>

<div>
<button @onclick="Send" disabled="@(!IsConnected)" class="btn btn-outline- 
     secondary">Send Message</button> 

@if (UserList != null)
    {
        <select id="user-list" @bind="selectedUser">
            <option value="">All.....</option>
            @foreach (var user in UserList)
            {
                <option value="@user">@user</option>
            }
        </select>
    }
  
 </div>

 <div>
    <label for="messagesList">Public Message Board:</label>
    <ul id="messagesList">
       @foreach (var message in messages)
       {
          <li>@message</li>
       }
    </ul>
</div>

<div>
    <label for="private-messages-list">Private Message Board:</label>
    <ul id="private-messages-list">
       @foreach (var message in privateMessages)
       {
          <li>@message</li>
       }
    </ul>
</div>

@code {
    HubConnection hubConnection;
    private List<string> messages = new List<string>();
    private List<string> privateMessages = new List<string>();
    private string messageForBoard;
    private string userName;
    private string messageInput;
    private string selectedUser;
    private List<string> UserList;

    private ElementReference elementRef;

    [Inject]
    public CookiesProvider CookiesProvider { get; set; }
  

    protected override async Task OnInitializedAsync()
    {
        var container = new CookieContainer();
        var cookie = new Cookie() 
         {
             Name = ".AspNetCore.Identity.Application", 
             Domain = "localhost",
             Value = CookiesProvider.Cookie
         };

         container.Add(cookie);

      hubConnection = new HubConnectionBuilder()
    .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"), options => 
    {
        // Pass the security cookie to the Hub. This is the way to do 
        // that in your case. In other cases, you may need to pass
        // an access token, but not here......
        options.Cookies = container; 
    }).Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
           InvokeAsync(() => StateHasChanged());
        });

        hubConnection.On<string>("ReceiveUserName", (name) =>
        {
            userName = name;

            InvokeAsync(() => StateHasChanged());
        });

         hubConnection.On<string>("MessageBoard", (message) =>
        {
            messageForBoard = message;

           InvokeAsync(() => StateHasChanged());
        });

        hubConnection.On<string, string>("ReceivePrivateMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            privateMessages.Add(encodedMsg);

            InvokeAsync(() => StateHasChanged());
        });

        hubConnection.On<List<string>>("ReceiveInitializeUserList", ( list) =>
        {
            UserList = list ;

            InvokeAsync(() => StateHasChanged());
        });


        await hubConnection.StartAsync();
        await hubConnection.InvokeAsync("InitializeUserList");
      
    }
    protected override void OnAfterRender(bool firstRender)
    {
         elementRef.FocusAsync();
    }
       
   async Task Send() => await hubConnection.SendAsync("SendMessage", 
                                       selectedUser, messageInput);
    public bool IsConnected => hubConnection.State == 
                                      HubConnectionState.Connected;

    public void Dispose()
    {
       hubConnection.DisposeAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await hubConnection.DisposeAsync();
    }
}
请注意,为了传递私人消息,您需要具有UserIdentifier,但您还需要将您想要发布私人消息的用户与UserIdentifier相关联。您可以简单地将UserIdentifier列表存储在聊天中,并传递所需的UserIdentifier列表。这当然会带来一些安全风险,应该避免。请看我的代码,我是如何处理这个问题的。用户只能查看用户名列表(是的,这些是已连接用户的电子邮件。回想一下,在数据库中,用户名Colum包含用户的电子邮件)。当然,您可以将其更改为更具显示性的值;您的显示名称可以是名字+姓氏,等等,这由您决定。只需记住,您需要为此添加一个新的索赔。如何做到这一点值得我们提出新的问题……

hubs/ChatHub.cs(完整代码需要清除一些不必要的Using语句)

    using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.IdentityModel.Tokens;

using Microsoft.IdentityModel;

using System.Net.Http;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Authentication;
using System.Net.Http.Json;

namespace SignalRServerIdentityAuthentication.Hubs
{
    [Authorize()]
    public class ChatHub : Hub
    {
        private static List<ConnectedUser> connectedUsers = new List<ConnectedUser>();
        public async Task InitializeUserList() 
        {
            var list = (from user in connectedUsers
                       select user.Name ).ToList();

            await Clients.All.SendAsync("ReceiveInitializeUserList", list);
        }
        public async Task SendMessage(string userID, string message)
        {
            if (string.IsNullOrEmpty(userID)) // If All selected
            {
                await Clients.All.SendAsync("ReceiveMessage", Context.User.Identity.Name ?? "anonymous", message);
            }
            else
            {
                var userIdentifier = (from _connectedUser in connectedUsers
                                      where _connectedUser.Name == userID
                                      select _connectedUser.UserIdentifier).FirstOrDefault();

                await Clients.User(userIdentifier).SendAsync("ReceivePrivateMessage",
                                       Context.User.Identity.Name ?? "anonymous", message);
            }

        }

        public override async Task OnDisconnectedAsync(Exception exception)
        {
           
            var user = connectedUsers.Where(cu => cu.UserIdentifier == Context.UserIdentifier).FirstOrDefault();

            var connection = user.Connections.Where(c => c.ConnectionID == Context.ConnectionId).FirstOrDefault();
            var count = user.Connections.Count;

            if(count == 1) // A single connection: remove user
            {
                connectedUsers.Remove(user);

            }
            if (count > 1) // Multiple connection: Remove current connection
            {
                user.Connections.Remove(connection);
            }

            var list = (from _user in connectedUsers
                        select new { _user.Name }).ToList();

           await Clients.All.SendAsync("ReceiveInitializeUserList", list);

           await   Clients.All.SendAsync("MessageBoard", 
                      $"{Context.User.Identity.Name}  has left");

            // await Task.CompletedTask;
         
        }

       
        public override async Task OnConnectedAsync()
        {
            var user = connectedUsers.Where(cu => cu.UserIdentifier == Context.UserIdentifier).FirstOrDefault();

            if (user == null) // User does not exist
            {
                ConnectedUser connectedUser = new ConnectedUser
                {
                    UserIdentifier = Context.UserIdentifier,
                    Name = Context.User.Identity.Name,
                    Connections = new List<Connection> { new Connection { ConnectionID = Context.ConnectionId } }
                };

                connectedUsers.Add(connectedUser);
            }
            else
            {
                user.Connections.Add(new Connection { ConnectionID = Context.ConnectionId });
            }

            // connectedUsers.Add(new )

            await Clients.All.SendAsync("MessageBoard", $"{Context.User.Identity.Name}  has joined");

            await  Clients.Client(Context.ConnectionId).SendAsync("ReceiveUserName", Context.User.Identity.Name);
       
        
        }
  }
}

集线器/ConnectedUser.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SignalRServerIdentityAuthentication.Hubs
{
    public class ConnectedUser
    {
        public string Name { get; set; }
        public string UserIdentifier { get; set; }

        public List<Connection> Connections { get; set; } 
    }
    public class Connection
    {
         public string ConnectionID { get; set; }
              
    }
}

这篇关于如何使用SignalR向Blazor Server中的特定用户发送消息?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆