本地搭建linux服务器做网站,网站前后端分离怎么做,石家庄云图网站建设,山东做网站三五这篇文章也可以在我的博客中查看
前言
哥们最近都在埋头苦干#xff0c;沉默是金#xff0c;有一段时间没更新博客了。然而今儿SignalR集成测试实属是给我整破防了。虽说SignalR是.NET官方维护的实时通信库#xff0c;已经开发了有十几年#xff0c;甚至已经编入至了core…这篇文章也可以在我的博客中查看
前言
哥们最近都在埋头苦干沉默是金有一段时间没更新博客了。然而今儿SignalR集成测试实属是给我整破防了。虽说SignalR是.NET官方维护的实时通信库已经开发了有十几年甚至已经编入至了core dll然而更新迭代异常迅速导致文档不全出了事不知所措。这不最近在集成测试SignalR这点上就踩了大坑。
今天就给大伙分享一下如何配置SignalR并重点讲解如何在 .NET 8 中使用xUnit与Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory对最新版ASP.NET CoreSignalR进行集成测试希望后来者可以少走弯路。
痛点
SignalR测试为何困难原因有下
WebApplicationFactory或者说其背后的TestServer并不提供真的服务器环境所有默认配置下的网络客户端当然包括HttpClient都无法连接至该模拟的服务器。 然而SignalR客户端所有连接都是在默认网络环境下的需要替换成TestServer环境下的客户端 HttpClient并不提供WebSocket连接支持。 然而SignalR实时通讯首选的是WebSocket所以我们还要配个TestServer环境下的WebSocket客户端 Hub受身份验证保护。 替换成TestServer客户端的时候还需要考虑身份验证 汗流浃背了家人们 关于本文
本文按这三个问题为思路逐步进行最合理的解决方案会在文末给出。 如果你觉得TL;DR、不想关注过程、或者认为看代码比看文章舒服可以跳转到文章最后获取项目源码 本文只介绍SignalR配置与集成测试阅读本文前建议做以下准备工作本文可能不会介绍以下内容
SignalR的使用只提及部分配置[Authorize]身份认证只一笔带过配置.NET集成测试框架如 xUnit配置WebApplicationFactory
本文操作环境
.NET 8xUnit 测试框架
无身份验证SignalR
在引入复杂性之前应先处理最核心的配置因此先不配置身份验证。
基本配置
配置Hub
在 .NET 8 中SignalR已经集成至ASP.NET Core中因此不需要下载任何Nuget包就能够使用。
配置也十分简洁首先需要创建一个Hub。Hub相当于是SignalR中的控制器。 创建Hub非常简单只需要继承Hub即可。以下例子展示了一个最基本的收发消息ChatHubSendMessage向所有连接广播一条消息
using Microsoft.AspNetCore.SignalR;namespace SignalR.IntegrationTests;public class ChatHub : Hub
{public async Task SendMessage(string message){await Clients.All.SendAsync(ReceiveMessage, message);}
}SendMessage是客户端向服务端发送消息的入口 该方法可以有返回值返回值会传回调用者 ReceiveMessage是服务端向客户端发送消息的入口 message是参数参数不一定只有一个也不一定为string A向B发送一条聊天信息其实需要经历两次交互 A向服务器发送消息服务器向B发送消息
配置Program.cs
在Program.cs中注册SignalR组件最简单的配置如下
const string HubsPrefix /hubs; // -- Grouped by prefix /hubsvar builder WebApplication.CreateBuilder(args);builder.Services.AddSignalR(); // -- Add SignalRvar app builder.Build();app.MapGroup(HubsPrefix).MapHubChatHub(/chat); // -- Map your ChatHub to /hubs/chatapp.Run();强类型Hub
上面的例子中服务端消息方法ReceiveMessage是字符串众所周知字符串意味着弱类型无编译时提示稍不留神可能就会写错。 .NET提供了一个做法强类型化这些方法。
首先定义一个接口
public interface IChatClientProxy
{public Task ReceiveMessage(string message);
}由于客户端还是需要以字符串订阅消息因此函数应以客户端的角度进行命名
是Receive而不是Send虽然是异步方法但不加Async后缀
然后将ChatHub修改如下
public class ChatHub : HubIChatClientProxy
{public async Task SendMessage(string message){await Clients.All.ReceiveMessage(message);}
}SignalR会自动实现IChatClientProxy接口当调用这个接口的方法时对应名称的消息就会被发出。
在Hub外向客户端发送消息
更多时候我们会在Hub之外发送消息就需要借助IHubContext获取Hub上下文。这个接口也支持强类型化。 以下实现了一个简单的服务先做一系列检测和记录再使用IHubContext实现实时发送消息
using Microsoft.AspNetCore.SignalR;namespace SignalR.IntegrationTests;public class ChatService(IHubContextChatHub, IChatClientProxy _hubContext)
{public async Task SendMessageToAllAsync(string message){// Chek for permissions...// Record to database...// ...await _hubContext.Clients.All.ReceiveMessage(message);}
}为了保持程序中的一致性通常情况下也会希望在Hub中引用自己的服务而不是直接发送消息
public class ChatHub(ChatService _chatService) : HubIChatClientProxy
{public async Task SendMessage(string message){await _chatService.SendMessageToAllAsync(message);}
}别忘了在Program.cs中为自己的服务注册依赖注入
builder.Services.AddScopedChatService();在客户端中接收SignalR消息
呃严格意义上你无法在服务端中接收SignalR消息你需要一个客户端接收服务端发出的信息。
以下代码是客户端代码它可能位于另一个项目可以是另一种语言实现甚至可以处于另一个平台e.g. Android 但是它也可以碰巧是同一个平台又碰巧是C#实现甚至碰巧在同一个项目
总之如果要在C#中接收SignalR消息你需要安装客户端Nuget包Microsoft.AspNetCore.SignalR.Client 下面的例子展示了如何向服务端的ChatHub收发消息
using Microsoft.AspNetCore.SignalR.Client;
using System.Diagnostics;var connection new HubConnectionBuilder().WithUrl(http://localhost/hubs/chat).Build();
// Add receive message handler.
connection.Onstring(ReceiveMessage, (message) Debug.WriteLine(message));await connection.StartAsync();// Send message.
await connection.InvokeAsync(SendMessage, Hello World);await connection.StopAsync();On方法用于接收消息。注意泛型参数一定要与服务端的类型兼容否则可能收不到对应消息InvokeAsync方法用于发送消息。第一个参数是远程方法名第二个起是远程方法对应的参数 该方法可以有泛型参数TResult以接受对应类型的返回值 HubConnectionBuilder还可以配置断线重连、身份验证等功能具体请查阅官方文档
集成测试
准备工作
进行下一步之前需要先
新建一个 xUnit 项目添加主项目为依赖项在测试项目中安装并配置WebApplicationFactory在测试项目中安装Microsoft.AspNetCore.SignalR.ClientNuget包
测试用例
根据含义我们会尝试使用SignalR客户端发送一条消息然后断言能够收到消息
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.SignalR.Client;public class WebAppFactory : WebApplicationFactoryProgram { }public class HubIntegrationTests(WebAppFactory _factory) : IClassFixtureWebAppFactory
{private HubConnection SetupHubConnection(string path){var uri new Uri(_factory.Server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri).Build();}[Fact]public async Task MessageTest(){// -- Arrangevar connection SetupHubConnection(/hubs/chat);string? received null;connection.Onstring(ReceiveMessage, (m) received m);await connection.StartAsync();string message Hello World;// -- Actawait connection.InvokeAsync(SendMessage, message);// Wait for messages to be received. You may need to increase the delay if youre running in a slow environment.await Task.Delay(1);// -- AssertAssert.Equal(message, received);}
}SetupHubConnection函数用作连接SignalR服务器。其中等待了1毫秒以确保有足够的时间接收消息 如果你的测试环境是老爷机可能需要增加等待时间 然而这个用例会失败错误如下
System.Net.Http.HttpRequestException : No connection could be made because the target machine actively refused it. (localhost:80)原因是TestServer并不是真的服务器它只模拟ASP.NET应用服务器的行为而不会在宿主机环境中启动真的服务器。因此我们使用常规的方式进行连接是无法访问的。但没有关系……
非WebSocket传输模式的测试
TestServer提供了一个用于连接至测试服务器的HttpMessageHandler对象也就是任何支持HttpMessageHandler进行Http数据交换的库都可以通过使用该对象访问TestServer。 经常接触.NET测试的伙伴此时已经要素察觉了HttpClient对HttpMessageHandler就是原生支持的
然后还有两个好消息
非WebSocket模式下的SignalR发起的连接使用的就是HttpClient 没错只是非WebSocket但总比连接失败要好 SignalR提供了一个配置项可以替换内部HttpClient使用的HttpMessageHandler
所以解决方案很简单只需要将上述SetupHubConnection函数修改成以下形式
private HubConnection SetupHubConnection(string path)
{var server _factory.Server;var uri new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o {o.HttpMessageHandlerFactory _ server.CreateHandler();}).Build();
}TestServer.CreateHandler()生成了一个HttpMessageHandler将它赋值给HttpMessageHandlerFactory可以改变其内部HttpClient的连接行为使其得以与TestServer进行交互。
问题
虽然测试是能通过了但是注意到测试时间长达4秒这对于本地服务器来讲显然是不正常的 Starting test run
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.3.16b60a9e56a (64-bit .NET 8.0.4)
[xUnit.net 00:00:00.04] Starting: SignalR.IntegrationTests.Tests
[xUnit.net 00:00:04.37] Finished: SignalR.IntegrationTests.TestsTest run finished: 1 Tests (1 Passed, 0 Failed, 0 Skipped) run in 4.4 sec 原因是因为产生了等待。事实上这个用例并没有建立WebSocket连接而是在等待WebSocket连接超时后转为了使用LongPolling模式连接。 如果我们强制限制SignalR客户端使用WebSocket连接
private HubConnection SetupHubConnection(string path)
{var server _factory.Server;var uri new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o {o.Transports Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets; // WebSockets only.o.HttpMessageHandlerFactory _ server.CreateHandler();}).Build();
}这个用例会在4秒后超时失败
System.AggregateException : Unable to connect to the server with any of the available transports. (WebSockets failed: Unable to connect to the remote server) (ServerSentEvents failed: The transport is disabled by the client.) (LongPolling failed: The transport is disabled by the client.)轮询并不是一般情况下的连接方式而且我们也不希望每个连接都等待4秒所以有没有办法能够进行Socket连接
WebSocket传输模式的测试
WebSocket连接失败的原因是WebSocketClient独立于HttpClient虽然我们构建了SignalR内部HttpClient与TestServer之间的连接但是并没有改变WebSocketClient它仍然是向真正的宿主机环境建立连接所以必然会失败。
但是没有关系这个问题早在几年前就被SignalR团队注意到并提供了替换WebSocketClient的配置项
private HubConnection SetupHubConnection(string path)
{var server _factory.Server;var uri new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o {o.Transports HttpTransportType.WebSockets;o.HttpMessageHandlerFactory _ server.CreateHandler();// Support WebSocket transports.o.WebSocketFactory async (context, cancellationToken) {var wsClient server.CreateWebSocketClient();return await wsClient.ConnectAsync(context.Uri, cancellationToken);};o.SkipNegotiation true;}).Build();
}通过配置WebSocketFactory可以将默认的WebSocketClient换成TestServer提供的客户端。从而能够对其进行WebSocket访问。 在WebSocket模式下顺便设置了SkipNegotiation可以减少协商时间而不会影响结果。 这里其实可以省略HttpMessageHandlerFactory的配置因为使用WebSocket时不会用到HttpClient。但如果使用LongPolling则很重要因此还是保留以供选择。 修改了WebSoketClient配置后重新运行测试用例这次可以快速以WebSocket模式通过测试 Starting test run
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.3.16b60a9e56a (64-bit .NET 8.0.4)
[xUnit.net 00:00:00.03] Starting: SignalR.IntegrationTests.Tests
[xUnit.net 00:00:00.21] Finished: SignalR.IntegrationTests.TestsTest run finished: 1 Tests (1 Passed, 0 Failed, 0 Skipped) run in 216 ms 带身份验证SignalR
身份配置
SignalR的身份验证方式
SignalR可以使用Cookie与Token令牌两种方式进行身份认证。
Cookie是浏览器环境下的首选方式可以自动传递凭证而Token则是非浏览器客户端下最简便的做法。 由于Cookie开箱即用不需要做额外配置因此本文只重点介绍Token做法。
SignalR Token令牌传递方式
根据SignalR文档在不同情况下有不同的传达方式
在非浏览器环境中以Authorization请求头的方式传递在浏览器环境的WebSocket, Server Side Event模式下无法使用自定义请求头需要以查询字符串的方式传递 该查询字符串需要在身份验证服务器自行读取接收
服务端配置接收access_token
所有无法自定义连接请求头的情况下都约定使用一个写死的微软你也干这事啊查询字符串access_token作为身份认证的参数。
你写死不要紧要紧的是我们使用SignalR是需要手动处理这个查询字符串的否则这种情况下永远无法触发身份验证。
虽然官网有说明但是总有像我一样的愣头青不喜欢看官方文档然后捣鼓了一整天才发现涅麻麻的要手动配置这个查询字符串。
所以为了减少愣头青请你务必 按照以下操作配置查询字符串 按照以下操作配置查询字符串 按照以下操作配置查询字符串
接收查询字符串
需要在SignalR服务端中主动接收这个查询字符串。 使用不同的身份验证库需要以不同的方式进行接收
你使用了内置的JWT库或者Identity Server可以参照官方文档进行配置你使用了Identity内置的BearerToken可以在Bearer Token中间件进行配置见下文你使用了其它的身份验证库基本也是相同的套路需要在验证请求事件中手动将该查询字符串赋值为用户凭证
Identity 内置Bearer Token身份验证
我这里使用了 .NET 8 Identity的内置BearerToken所以能够实现目标的最小配置Program.cs是这样的
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using SignalR.IntegrationTests;const string HubsPrefix /hubs;var builder WebApplication.CreateBuilder(args);builder.Services.AddAuthorization();
builder.Services.AddAuthentication(IdentityConstants.BearerScheme).AddCookie(IdentityConstants.ApplicationScheme).AddBearerToken(IdentityConstants.BearerScheme, o {o.Events new(){OnMessageReceived context {var accessToken context.Request.Query[access_token];var path context.HttpContext.Request.Path;// If the request is for our hub...if (!string.IsNullOrEmpty(accessToken) path.StartsWithSegments(HubsPrefix)){// Read the token out of the query stringcontext.Token accessToken;}return Task.CompletedTask;}};});
builder.Services.AddIdentityCoreIdentityUser().AddApiEndpoints().AddEntityFrameworkStoresIdentityDbContext();
builder.Services.AddDbContextIdentityDbContext(x x.UseInMemoryDatabase(db));builder.Services.AddSignalR();var app builder.Build();app.MapIdentityApiIdentityUser();app.MapGroup(HubsPrefix).MapHubChatHub(/chat);app.Run();使用身份验证保护Hub
与Controller一样通过使用Authorize、AllowAnonymous特性控制对Hub的访问
[Authorize]
public class ChatHub(ChatService _chatService) : HubIChatClientProxy
{// ......
}
为Hub连接提供身份验证
Cookie验证
浏览器环境中正常使用Cookie登录凭证会在请求时自动携带非浏览器环境中可以通过手动设置Cookie请求头实现Cookie验证 但这种做法不如使用Token更加正规
Token令牌验证
Token可以在客户端发起连接前使用AccessTokenProvider提供。
var connection new HubConnectionBuilder().WithUrl(http://localhost/hubs/chat, options {options.AccessTokenProvider () Task.FromResult(token);}).Build();考虑到重连与Token过期问题AccessTokenProvider接受的是一个工厂函数你可以选择动态获取新Token而不是写死一个值 集成测试
由于我们替换了默认的WebSocketClient我们需要手动携带Token令牌以支持WebSocket模式下的身份验证非WebSocket的身份验证仍然使用AccessTokenProvider配置项无需修改。因此修改SetupHubConnection方法
配置AccessTokenProvider参数使非WebSocket连接方式能够携带令牌将token添加至WebSocketClient中使WebSocket连接方式能够携带令牌。由于是非浏览器环境有两种方案可以选择 添加名为access_token的查询字符串添加Authorization请求头 小孩子才做选择我全都要。 private HubConnection SetupHubConnection(string path, string? token null)
{var server _factory.Server;var uri new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o {o.Transports HttpTransportType.WebSockets;o.HttpMessageHandlerFactory _ server.CreateHandler();o.WebSocketFactory async (context, cancellationToken) {var wsClient server.CreateWebSocketClient();if (token ! null){// Authentication for socket transports. (Chooses one of these.)// Option1: Use request headers.wsClient.ConfigureRequest request request.Headers.Authorization new($Bearer {token});// Option2: Add access token to query string.uri new Uri(QueryHelpers.AddQueryString(context.Uri.ToString(), access_token, token));// I like both ;)}else{uri context.Uri;}return await wsClient.ConnectAsync(uri, cancellationToken);};o.SkipNegotiation true;// Authentication for non-socket transports. (Can be omitted here.)o.AccessTokenProvider () Task.FromResult(token);}).Build();
}最后在用例中指定token参数即可成功通过测试。
Q: 我应该如何生成token令牌
如何生成令牌取决于你身份验证的实现方式。
在使用WebApplicationFactory的集成测试中你可以比较容易地使用真实的用户与正常的登录方式获取令牌如果身份认证本身并不是集成测试的关键你可以设法使用测试替身替换掉原有的身份验证程序。但一般情况下这只会更麻烦 如果你想了解如何以正常登录方式获取令牌可以查看我的源码 至此所有问题解决现在我们可以用SignalR WebSocket模式对带身份认证的Hub进行集成测试了
项目源码
Github
参考资料
Authentication and authorization in ASP.NET Core SignalR[SignalR] Better integration with TestServerSignalR Hub auth?