一、为什么需要自定义中间件
在开发实时应用时,我们经常需要对WebSocket连接进行额外处理。比如记录日志、验证权限、拦截可疑消息等。虽然SignalR本身提供了一些基础功能,但很多时候我们需要更灵活的控制。
想象一下这样的场景:你的聊天应用中,需要阻止某些敏感词汇的传播,或者需要记录每个用户的连接行为以便后续分析。这时候,自定义中间件就能派上用场了。
二、中间件基础概念
中间件就像是管道中的一个个过滤器,每个请求都会依次通过这些过滤器。在ASP.NET Core中,中间件可以处理传入的HTTP请求,也可以处理SignalR的Hub连接。
一个典型的中间件结构是这样的:
// 技术栈:ASP.NET Core 6.0
public class CustomHubMiddleware
{
private readonly RequestDelegate _next;
public CustomHubMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 在这里处理请求前的逻辑
await _next(context); // 调用下一个中间件
// 在这里处理请求后的逻辑
}
}
这个简单的中间件模板展示了最基本的处理流程。_next代表管道中的下一个中间件,调用它意味着将请求继续传递下去。
三、实现消息拦截功能
让我们来实现一个实际可用的消息拦截中间件。假设我们需要过滤聊天消息中的敏感词:
// 技术栈:ASP.NET Core 6.0
public class MessageFilterMiddleware
{
private readonly RequestDelegate _next;
private readonly List<string> _forbiddenWords = new() { "敏感词1", "敏感词2" };
public MessageFilterMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest ||
context.Request.Headers["Upgrade"] == "websocket")
{
// 保存原始请求体以便后续读取
var originalBody = context.Request.Body;
try
{
using var memStream = new MemoryStream();
context.Request.Body = memStream;
await _next(context);
// 在这里可以处理响应消息
}
finally
{
context.Request.Body = originalBody;
}
}
else
{
await _next(context);
}
}
private bool ContainsForbiddenWords(string message)
{
return _forbiddenWords.Any(word =>
message.Contains(word, StringComparison.OrdinalIgnoreCase));
}
}
这个中间件会检查所有WebSocket请求,虽然它还不能直接拦截SignalR消息,但已经展示了基本的拦截思路。
四、完整的日志记录实现
日志记录是中间件的常见用途。下面我们实现一个完整的SignalR日志记录中间件:
// 技术栈:ASP.NET Core 6.0
public class SignalRLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SignalRLoggingMiddleware> _logger;
public SignalRLoggingMiddleware(
RequestDelegate next,
ILogger<SignalRLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var connectionId = context.Connection.Id;
var userId = context.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation(
"SignalR连接建立: {ConnectionId}, 用户: {UserId}, 时间: {Time}",
connectionId, userId, DateTime.UtcNow);
try
{
await _next(context);
_logger.LogInformation(
"SignalR连接正常关闭: {ConnectionId}, 用户: {UserId}",
connectionId, userId);
}
catch (Exception ex)
{
_logger.LogError(ex,
"SignalR连接异常: {ConnectionId}, 用户: {UserId}, 错误: {ErrorMessage}",
connectionId, userId, ex.Message);
throw;
}
}
}
这个中间件会记录每个SignalR连接的建立、关闭和异常情况,对于排查线上问题非常有帮助。
五、权限校验中间件开发
权限校验是另一个重要功能。下面我们实现一个基于JWT的权限校验中间件:
// 技术栈:ASP.NET Core 6.0
public class SignalRAuthMiddleware
{
private readonly RequestDelegate _next;
private readonly JwtSecurityTokenHandler _tokenHandler;
public SignalRAuthMiddleware(RequestDelegate next)
{
_next = next;
_tokenHandler = new JwtSecurityTokenHandler();
}
public async Task InvokeAsync(HttpContext context)
{
// 检查是否是SignalR请求
if (context.WebSockets.IsWebSocketRequest ||
context.Request.Path.StartsWithSegments("/hub"))
{
var token = context.Request.Query["access_token"];
if (string.IsNullOrEmpty(token))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("未授权的访问");
return;
}
try
{
var principal = ValidateToken(token);
context.User = principal;
}
catch
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("无效的访问令牌");
return;
}
}
await _next(context);
}
private ClaimsPrincipal ValidateToken(string token)
{
// 这里应该是实际的JWT验证逻辑
// 简化示例,实际使用时需要配置正确的验证参数
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secret-key")),
ValidateIssuer = false,
ValidateAudience = false
};
return _tokenHandler.ValidateToken(token, validationParameters, out _);
}
}
这个中间件会检查SignalR连接请求中是否包含有效的JWT令牌,如果没有或者无效,会直接拒绝连接。
六、中间件的注册与配置
实现好中间件后,我们需要在Startup中注册它们。这里有一个推荐的注册顺序:
// 技术栈:ASP.NET Core 6.0
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// 异常处理应该在最外层
app.UseMiddleware<ExceptionHandlingMiddleware>();
// 然后是认证中间件
app.UseMiddleware<SignalRAuthMiddleware>();
// 日志记录中间件
app.UseMiddleware<SignalRLoggingMiddleware>();
// 消息过滤中间件
app.UseMiddleware<MessageFilterMiddleware>();
// SignalR本身的配置
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub");
});
// 其他中间件...
}
}
注册顺序很重要,因为中间件是按照注册顺序依次执行的。一般来说,异常处理和认证应该放在最前面。
七、实际应用中的注意事项
在实际项目中使用自定义中间件时,有几个重要注意事项:
性能影响:每个中间件都会增加一点处理时间,特别是在处理请求体时。要确保中间件的逻辑尽可能高效。
异常处理:中间件中的异常如果没有妥善处理,可能会导致整个应用崩溃。建议在最外层添加一个异常处理中间件。
依赖注入:中间件支持依赖注入,但要小心循环依赖问题。
测试难度:自定义中间件会增加测试复杂度,建议为每个中间件编写单元测试。
SignalR特殊性:SignalR使用WebSocket时,有些HTTP中间件可能不会按预期工作,需要特别注意。
八、技术方案优缺点分析
这种自定义中间件的方案有几个明显优势:
优点:
- 灵活性高:可以完全自定义处理逻辑
- 可复用:一个中间件可以在多个项目中复用
- 非侵入式:不需要修改Hub本身的代码
- 集中管理:所有相关逻辑都在一个地方
缺点:
- 学习曲线:需要理解ASP.NET Core中间件机制
- 调试困难:中间件执行流程有时难以跟踪
- 性能开销:每个中间件都会增加一点延迟
- SignalR限制:不是所有SignalR功能都能通过中间件控制
九、总结与最佳实践
通过自定义中间件,我们可以优雅地扩展SignalR的功能。在实际项目中,建议:
- 保持中间件单一职责:一个中间件只做一件事
- 编写详细的日志:方便排查问题
- 进行性能测试:确保中间件不会成为性能瓶颈
- 提供配置选项:让中间件行为可配置
- 编写文档:说明中间件的用途和使用方式
记住,中间件是强大的工具,但也要谨慎使用。只有在真正需要时才添加自定义中间件,避免过度设计。
评论