更新时间:2023-02-14 20:11:12
这是如何做到这一点的完整解决方案。您可以从Github:
下载正在运行且已更新的代码示例 注意:此答案的目的不是如何创建SignlR应用程序,以及如何管理用户。文档和许多其他教程中都显示了这一点。但有一点是欠缺的,那就是如何保护SignalR集线器的端点,以及如何使用户的声明在Blazor Server App的集线器中可用。我没有找到一个使用Blazor服务器应用的例子。我向Blazor团队寻求一些提示,但无济于事…使用带有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
@{
// 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://***.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>
@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();
}
}
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; }
}
}
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属性保护。<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>
@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包含用户的电子邮件)。当然,您可以将其更改为更具显示性的值;您的显示名称可以是名字+姓氏,等等,这由您决定。只需记住,您需要为此添加一个新的索赔。如何做到这一点值得我们提出新的问题……
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);
}
}
}
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; }
}
}