一、为什么需要自定义中间件

在开发实时应用时,我们经常需要对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");
        });
        
        // 其他中间件...
    }
}

注册顺序很重要,因为中间件是按照注册顺序依次执行的。一般来说,异常处理和认证应该放在最前面。

七、实际应用中的注意事项

在实际项目中使用自定义中间件时,有几个重要注意事项:

  1. 性能影响:每个中间件都会增加一点处理时间,特别是在处理请求体时。要确保中间件的逻辑尽可能高效。

  2. 异常处理:中间件中的异常如果没有妥善处理,可能会导致整个应用崩溃。建议在最外层添加一个异常处理中间件。

  3. 依赖注入:中间件支持依赖注入,但要小心循环依赖问题。

  4. 测试难度:自定义中间件会增加测试复杂度,建议为每个中间件编写单元测试。

  5. SignalR特殊性:SignalR使用WebSocket时,有些HTTP中间件可能不会按预期工作,需要特别注意。

八、技术方案优缺点分析

这种自定义中间件的方案有几个明显优势:

优点:

  • 灵活性高:可以完全自定义处理逻辑
  • 可复用:一个中间件可以在多个项目中复用
  • 非侵入式:不需要修改Hub本身的代码
  • 集中管理:所有相关逻辑都在一个地方

缺点:

  • 学习曲线:需要理解ASP.NET Core中间件机制
  • 调试困难:中间件执行流程有时难以跟踪
  • 性能开销:每个中间件都会增加一点延迟
  • SignalR限制:不是所有SignalR功能都能通过中间件控制

九、总结与最佳实践

通过自定义中间件,我们可以优雅地扩展SignalR的功能。在实际项目中,建议:

  1. 保持中间件单一职责:一个中间件只做一件事
  2. 编写详细的日志:方便排查问题
  3. 进行性能测试:确保中间件不会成为性能瓶颈
  4. 提供配置选项:让中间件行为可配置
  5. 编写文档:说明中间件的用途和使用方式

记住,中间件是强大的工具,但也要谨慎使用。只有在真正需要时才添加自定义中间件,避免过度设计。