簡體   English   中英

如何使用 SignalR 在 Blazor 服務器中向特定用戶發送消息?

[英]How To Send Message To Specific User In Blazor Server Using SignalR?

我已經使用 blazor 服務器和 signalR 實現了一個簡單的公共聊天室系統,並帶有數據庫來存儲用戶名和消息。 IN系統用戶只需輸入其名稱即可加入聊天,並出現聊天界面。

現在,我想要添加另一個功能,可以將消息發送給該公共聊天室中的特定用戶。
任何幫助都會很棒,謝謝。

以下是我的公共聊天室代碼。
下面是我的集線器

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

下面是加載數據的代碼
@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>
}

下面是我的用戶界面
@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 服務器應用程序中的集線器中提供用戶的聲明。 我沒有找到一個使用 Blazor 服務器應用程序 dong it 的示例。 我從 Blazor 團隊尋求一些提示,但無濟於事......

一些澄清

注意:在 Blazor 服務器應用程序的前端對您的用戶進行身份驗證不會使您有權訪問集線器上的受保護端點。 您應該像對待 Web Api 端點一樣對待集線器,這要求您在執行 HTTP 調用時傳遞訪問令牌。 例如,如果您想從 Web Api 中的 WeatherForecastController 檢索數據到使用 HttpClient 服務的 FetchData 頁面,您需要在授權 Z099FB995346F31C749EDB 中傳遞訪問令牌

當您使用帶有 API 身份驗證的 WebAssembly 應用程序時,您可以在創建集線器連接時將訪問令牌傳遞給集線器。 這很簡單,文檔中有一個示例代碼演示了這一點,實際上您無需做太多事情來保護 Hub 和訪問...... ...,即使這樣,也有一些問題需要處理,因為在 Hub 中只能訪問 UserIdentifier,而不是所有用戶的聲明。

但是,這里的答案是關於 Blazor Server App,解決方法是將安全 cookie(“.AspNetCore.Identity.Application”)傳遞給 hub。 因此,解決該問題的第一步是在呈現 Blazor SPA 之前從 HttpContext 捕獲 cookie,並將 cookie 傳遞給 Blazor App 作為參數發送到 App 組件。 現在 cookie 在 Blazor 應用程序中可用,您可以從聊天頁面訪問它並將其傳遞給 Hub。 請注意,與帶有 SignalR 的 WebAssembly 應用程序示例不同,所有 ClaimPrincipal object 在 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; }
    }
}

啟動.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 組件,其 object 用於防止用戶訪問聊天組件,除非她已通過身份驗證。 另請注意,聊天頁面受 Authorize 屬性的保護。

導航菜單.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 相關聯。 您可以簡單地在 Chat 中存儲 UserIdentifiers 列表,然后通過 reired 一個。 這當然會帶來一些安全風險,應該避免。 請參閱我的代碼如何處理此問題。 用戶只能查看用戶名列表(是的,這些是已連接用戶的電子郵件。回想一下,在數據庫中,UserName 列包含用戶的電子郵件)。 您當然可以將其更改為更可顯示的值; 您的顯示名稱可以是名字 + 姓氏等。這取決於您。 請記住,您需要為此添加新的聲明。 如何做到這一點值得一個新的問題......

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

我知道有兩種方法。

  1. 您需要將用戶 ConnectionId 和 map 記錄到服務器上的用戶。
Clients.Client(ConnectionId).SendAsync(...);
  1. 當用戶連接到集線器時,將他們放入一個僅供他們使用的組中。 例如,使用他們的 userId 作為組名。 然后您可以向該組廣播,並且只有該用戶會收到該消息。
 Clients.Group($"userId").SendAsync(...);

覆蓋集線器上的OnConnectedAsync()以設置任一方法。 不要忘記覆蓋OnDisconnectedAsync()以整理任何 state 變量或斷開連接時的存儲。 如果做得正確,這還具有跟蹤誰連接的好處。

使用集線器上的[Authorize]屬性。 請參閱此處了解如何使用 WASM。

以下代碼也可能有所幫助。 (中心)

var connectionId = Context.ConnectionId;

var userId = Context.UserIdentifier;
var applicationUser = await userManager.FindByIdAsync(userId);

恕我直言:出於安全原因,除了顯示名稱之外,我不會將其他用戶的詳細信息發送給客戶端。 這就是我使用[Authorize]屬性的原因。 這兩種技術都會混淆其他用戶的詳細信息和與集線器的連接。

您可以簡單地在您的Message class 中添加另一個屬性,以識別消息的預期收件人以及是否將其空白發送給所有人。 然后在您的集線器中使用此屬性在廣播方法之間進行選擇。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM