45.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--修改通用中间件

前面我们通过三篇文章在网关中集成了认证功能,这样我们编写的通用中间件ApplicationMiddleware的功能就和网关的认证功能就重复了,因此我们就需要修改它的功能。那么,我们开始吧。

一、修改通用中间件的功能

目前ApplicationMiddleware的功能是首先从请求头中获取 Authorization,如果是 Bearer 格式则解析出 JWT Token;然后提取其中的 UserIdUserName 等声明(Claims)。若 HttpContext.User 尚无身份信息,则创建新的 ClaimsPrincipal,否则将缺失的用户声明合并到现有身份中。若 Token 不存在或解析失败,则抛出 UnauthorizedException 表示未登录。最后调用 _next(context) 将请求传递给下一个中间件。

我们在重构ApplicationMiddleware后的功能如下:

  1. 验证网关签名(X-Gateway-Signature),确保请求来自网关且在5分钟内有效
  2. 支持匿名访问(X-Anonymous)
  3. 从请求头获取用户信息(X-User-Id等)并创建身份认证
  4. 将请求传递给下一个中间件

这样既保证了系统的安全性和可靠性,又简化了身份认证流程,同时提供了更灵活的访问控制机制,使整个系统更加健壮和易于维护。具体实现代码如下:

csharp 复制代码
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SP.Common.ExceptionHandling.Exceptions;

namespace SP.Common.Middleware;

/// <summary>
/// 应用程序中间件,所有微服务都要引入
/// </summary>
public class ApplicationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ApplicationMiddleware> _logger;
    private readonly IConfiguration _configuration;

    /// <summary>
    /// 应用程序中间件构造函数
    /// </summary>
    /// <param name="next">下一个中间件</param>
    /// <param name="logger">日志记录器</param>
    /// <param name="configuration">配置</param>
    public ApplicationMiddleware(RequestDelegate next, ILogger<ApplicationMiddleware> logger, IConfiguration configuration)
    {
        _next = next;
        _logger = logger;
        _configuration = configuration;
    }

    /// <summary>
    /// 中间件处理请求
    /// </summary>
    /// <param name="context">HTTP上下文</param>
    /// <returns>异步任务</returns>
    public async Task InvokeAsync(HttpContext context)
    {
        // 验证网关签名
        if (!ValidateGatewaySignature(context))
        {
            _logger.LogWarning("检测到未授权的直接访问,IP: {IP}", context.Connection.RemoteIpAddress);
            throw new UnauthorizedException("未授权的访问");
        }

        if (context.Request.Headers.ContainsKey("X-Anonymous"))
        {
            await _next(context);
            return;
        }
        // 从header中获取用户信息
        var userId = context.Request.Headers["X-User-Id"].FirstOrDefault();
        var username= context.Request.Headers["X-User-Name"].FirstOrDefault();
        var email = context.Request.Headers["X-User-Email"].FirstOrDefault();
        if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(username) || string.IsNullOrEmpty(email))
        {
            _logger.LogError("请求头中缺少用户信息");
            throw new UnauthorizedException("未登录");
        }

        // 创建 ClaimsIdentity 并添加 claims
        var claims = new List<Claim>();
        if (!string.IsNullOrEmpty(userId))
            claims.Add(new Claim("UserId", userId));
        if (!string.IsNullOrEmpty(username))
            claims.Add(new Claim("UserName", username));
        if (!string.IsNullOrEmpty(email))
            claims.Add(new Claim("Email", email));

        var identity = new ClaimsIdentity(claims, "header");
        context.User = new ClaimsPrincipal(identity);

        await _next(context);
    }

    private bool ValidateGatewaySignature(HttpContext context)
    {
        try
        {
            var signature = context.Request.Headers["X-Gateway-Signature"].FirstOrDefault();
            if (string.IsNullOrEmpty(signature))
            {
                return false;
            }

            var signatureBytes = Convert.FromBase64String(signature);
            var signatureText = System.Text.Encoding.UTF8.GetString(signatureBytes);
            var parts = signatureText.Split('.');
            
            if (parts.Length != 2)
            {
                return false;
            }

            if (!long.TryParse(parts[0], out var timestamp))
            {
                return false;
            }

            var secret = _configuration["GatewaySecret"] ?? "SP_Gateway_Secret_2024";
            if (parts[1] != secret)
            {
                return false;
            }

            // 验证时间戳(5分钟内的请求有效)
            var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            var timeDiff = Math.Abs(currentTimestamp - timestamp);
            if (timeDiff > 300) // 5分钟
            {
                return false;
            }

            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "验证网关签名时发生错误");
            return false;
        }
    }
}

在修改后的中间件中,我们引入了一个至关重要的安全验证机制,即网关签名验证。这一机制是专门为微服务架构设计的安全保护措施,其核心目标是确保所有请求必须经过网关进行转发,禁止任何尝试绕过网关直接访问微服务的行为。这种设计不仅能有效防止非法请求,还能抵御重放攻击和中间人攻击,从而显著提升整个系统的安全性与稳定性。在实现上,网关签名验证依赖于一个特殊的请求头字段 X-Gateway-Signature,该字段的值不是简单的字符串,而是经过 Base64 编码的复杂格式数据。签名字符串内部由两部分组成,用英文句点(.)分隔,第一部分是 Unix 时间戳,第二部分是网关密钥。时间戳采用十位格式,表示自 1970 年 1 月 1 日 UTC 零时起至当前时刻的秒数,这样的设计可以防止重放攻击,即攻击者即使截获了请求,也无法在未来重新发送以欺骗系统,因为系统会对时间戳进行严格验证,通常限制在五分钟内有效。网关密钥是一个预先配置的字符串值,可以通过配置文件中的 GatewaySecret 项进行指定,如果用户没有配置,则系统会使用默认值 SP_Gateway_Secret_2024。网关密钥的存在确保了只有网关和微服务端知道这个密钥,任何试图伪造请求的行为都会因为密钥不匹配而被拒绝,从而有效保护了微服务的接口安全。

当请求到达微服务时,中间件会首先检查请求是否包含 X-Gateway-Signature 请求头,如果缺少该字段,则说明请求并非经过网关转发,立即被判定为非法请求并返回错误。接着,中间件将签名字符串进行 Base64 解码,以获取原始的时间戳和网关密钥信息,如果解码失败则说明签名格式异常,请求同样会被拒绝。解码后的字符串会按照句点进行拆分,从而分别获取时间戳和网关密钥。时间戳会被转换为数值类型,然后与系统当前时间进行对比,如果时间差超过五分钟,则说明请求已过期。网关密钥则会与配置文件中指定的密钥进行比对,如果不匹配则说明请求不是来自合法网关,也会被拒绝。只有当时间戳在有效范围内且网关密钥正确时,请求才会被允许继续传递给下一个中间件或微服务处理逻辑。通过这种双重验证机制,网关签名验证能够有效防止请求被篡改或重放,同时保证请求来源的合法性。

在具体实现中,这一过程的代码逻辑相对简洁但功能完整。中间件首先通过 RequestDelegate 获取请求上下文,然后尝试读取请求头 X-Gateway-Signature 的值,如果不存在则直接返回 401 状态码。接着对签名进行 Base64 解码,并按照句点拆分出时间戳和密钥部分。如果拆分后的长度不符合预期,说明签名格式错误,中间件会立即返回错误。随后将时间戳与当前 UTC 时间进行比较,如果请求时间超过五分钟,说明请求已过期,会被拒绝。最后将解码得到的网关密钥与配置文件中的密钥进行比对,如果不一致,也会返回错误。整个验证流程设计得既严密又高效,即使在高并发场景下,也不会对系统性能造成明显影响。通过这一机制,微服务可以完全信任来自网关的请求,避免任何未经授权的访问。

网关签名验证机制不仅增强了系统安全性,还为微服务架构提供了可靠的安全保障。它可以有效阻止攻击者绕过网关直接访问服务端点,保证所有请求都必须经过统一的入口。由于签名中包含时间戳信息,系统能够在请求到达时识别过期请求,从而防止重放攻击。这种设计对于保护敏感业务数据和核心接口尤其重要,例如在金融、支付和医疗等高安全需求的场景中,任何非法访问或请求篡改都可能带来严重风险。网关密钥的配置灵活性也使得运维团队可以根据需要随时更换密钥,进一步增强系统安全性,同时默认值保证了系统开箱即用的便捷性。

除了自身的安全优势,网关签名验证机制通常还会与其他安全手段结合使用。在实际生产环境中,它可以与用户身份认证机制如 JWT 一起工作,网关签名确保请求来源合法,而 JWT 则用于验证用户身份,双重验证能够提供更高的安全保障。同时,通过在 HTTPS 传输层加密请求,可以防止请求内容在传输过程中被窃听或篡改,使签名验证与传输安全形成互补。此外,在一些高安全要求的场景中,还可以配合 IP 白名单机制,进一步限制只有网关所在服务器的 IP 能够访问微服务接口。通过这种多层安全设计,系统不仅能够抵御常规的攻击手段,还能有效防范高级持续威胁(APT),确保微服务在复杂网络环境中的稳定性和可靠性。

网关签名验证是微服务安全体系中非常重要的一环。它通过在请求中增加包含时间戳和网关密钥的签名,实现对请求来源和时效的严格校验,从而防止请求被篡改、重放或伪造,同时确保所有请求必须经过网关转发。该机制实现简单、性能开销低,却能够提供显著的安全保护,对于任何依赖网关的微服务架构来说,都是一种高效、可靠且易于推广的安全方案。并且通过这种设计,开发者和运维团队可以在保持系统灵活性和可扩展性的同时,获得稳固的安全保障,从根本上提升系统的整体安全水平。

二、总结

本文主要讲解了通用中间件ApplicationMiddleware的重构过程。为避免与网关认证功能重复,新版中间件实现了网关签名验证机制,通过X-Gateway-Signature请求头进行验证,包含时间戳和网关密钥两部分,有效防止请求篡改和重放攻击。中间件还支持匿名访问,并能从请求头提取用户信息创建身份认证。这些改进既保证了系统安全性,又简化了认证流程,为微服务架构提供了可靠的安全保障。