sud*_*and 1 c# asp.net-mvc signalr asp.net-core blazor
我使用 blazor 服务器和 signalR 以及数据库来存储用户名和消息实现了一个简单的公共聊天室系统。在系统中,用户只需输入姓名即可加入聊天,然后出现聊天界面。
现在,我想要的是添加另一个功能,可以将消息发送给公共聊天室中存在的特定用户。
任何帮助都会很棒,谢谢。
下面是我的公共聊天室代码。
下面是我的中心
public class Chat:Hub
{
public async Task SendMessage(Message message)
{
await Clients.All.SendAsync("Receiver", message);
}
}
Run Code Online (Sandbox Code Playgroud)
下面是在数据库中保存用户名和消息的代码
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();
}
Run Code Online (Sandbox Code Playgroud)
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));
}
}
Run Code Online (Sandbox Code Playgroud)
@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>
}
Run Code Online (Sandbox Code Playgroud)
Isa*_*aac 10
这是如何做到这一点的完整解决方案。您可以从Github下载代码的工作和更新示例:
注意:此答案的目的不是如何创建 SignlR 应用程序以及如何管理用户。这在文档和许多其他教程中都有展示。但缺少一件事,那就是如何保护 SignalR hub 的端点,以及如何使用户的声明在 Blazor Server App 的 hub 中可用。我没有找到任何使用 Blazor Server App 进行操作的示例。我向 Blazor 团队寻求了一些提示,但无济于事......
注意:在 Blazor 服务器应用程序的前端对用户进行身份验证不会使您获得访问 Hub 上受保护端点的授权。您应该像对待 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 应用程序。现在 cookie 在 Blazor 应用程序中可用,您可以从聊天页面访问它并将其传递到中心。请注意,与使用 SignalR 的 WebAssembly 应用程序示例不同,所有 ClaimPrincipal 对象都在 Hub 中可用,并且您可以访问其所有声明,例如:
var user = Context.User.Identity.Name
var userid = Context.UserIdentifier
Run Code Online (Sandbox Code Playgroud)
@{
// 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: /sf/answers/4167682361/
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>
Run Code Online (Sandbox Code Playgroud)
@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();
}
}
Run Code Online (Sandbox Code Playgroud)
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; }
}
}
Run Code Online (Sandbox Code Playgroud)
services.AddScoped<CookiesProvider>();
services.AddSignalR();
Run Code Online (Sandbox Code Playgroud)
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapHub<ChatHub>("/chatHub");
endpoints.MapFallbackToPage("/_Host");
});
Run Code Online (Sandbox Code Playgroud)
请注意,NavMenu 包含一个 AuthorizeView 组件,其目的是防止用户访问 Chat 组件,除非她已经过身份验证。另请注意,聊天页面受授权属性保护。
<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>
Run Code Online (Sandbox Code Playgroud)
@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();
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,为了传递私人消息,您需要拥有 UserIdentifier,但您还需要将要发布私人消息的用户与 UserIdentifier 相关联。您可以简单地在聊天中存储用户标识符列表,并传递需要的用户标识符。这当然会带来一些安全风险,应该避免。请参阅我的代码我如何处理这个问题。用户只能查看用户名列表(是的,这些是已连接用户的电子邮件。回想一下,在数据库中,UserName 列包含用户的电子邮件)。您当然可以将其更改为更易于显示的值;您的显示名称可以是名字+姓氏等。这由您决定。请记住,您需要为此添加新的声明。如何做到这一点值得一个新问题......
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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
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; }
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
5865 次 |
| 最近记录: |