通过ASP.NET Core开发反向代理服务器与API网关功能插图

通过ASP.NET Core开发反向代理服务器与API网关功能:从零构建你的流量调度中心

你好,我是源码库的技术博主。在微服务架构大行其道的今天,反向代理和API网关已成为系统不可或缺的“交通枢纽”。我曾经历过项目初期直接硬编码服务调用地址,后期服务拆分和部署变更时带来的“牵一发而动全身”的噩梦。今天,我将带你一起,使用我们熟悉的ASP.NET Core,从零开始构建一个兼具反向代理和基础API网关功能的应用。这个过程不仅有助于理解其底层原理,更能让你在需要定制特殊路由、鉴权或流量控制逻辑时,拥有完全的自主权。

一、项目初始化与核心库YARP的引入

首先,我们创建一个新的ASP.NET Core Web项目。这里我选择空模板,以便更清晰地展示核心结构。

dotnet new web -n MyCustomGateway
cd MyCustomGateway

接下来,引入本次实战的核心——YARP (Yet Another Reverse Proxy)。这是微软官方推出的一个高性能、可扩展的反向代理库,它完美地集成在ASP.NET Core的管道中,是我们构建自定义网关的基石。

dotnet add package Yarp.ReverseProxy

安装完成后,打开 `Program.cs` 文件。我们需要在这里完成服务和中间件的配置。

二、配置反向代理:让请求流动起来

反向代理的核心是配置:定义一组“路由”(Routes)和它们对应的“集群”(Clusters)。路由负责匹配传入的请求(比如根据路径),集群则定义了该请求将被转发到的一个或多个目标地址。

我们先在 `appsettings.json` 中定义一个简单的配置,将访问 `/service1/` 的请求转发到本地另一个运行在5001端口的服务。

{
  "ReverseProxy": {
    "Routes": {
      "route1": {
        "ClusterId": "cluster1",
        "Match": {
          "Path": "/service1/{**catch-all}"
        },
        "Transforms": [
          { "PathPattern": "{**catch-all}" }
        ]
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "destination1": {
            "Address": "http://localhost:5001/"
          }
        }
      }
    }
  }
}

这里有个踩坑提示:注意 `Path` 匹配和 `PathPattern` 转换。`{**catch-all}` 是一个通配参数,它匹配路径的剩余部分。在 `Match` 中我们捕获了整个路径,在 `Transforms` 中我们又将其原样设置为转发后的路径,这确保了 `/service1/api/users` 会被正确地转发到 `http://localhost:5001/api/users`,而不是错误地带上 `/service1` 前缀。

现在,在 `Program.cs` 中加载配置并启用YARP。

var builder = WebApplication.CreateBuilder(args);

// 1. 添加反向代理服务
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

// 2. 启用反向代理中间件
app.MapReverseProxy();

app.Run();

启动你的网关项目(假设在5000端口)和一个模拟的后端服务(在5001端口返回简单信息)。访问 `http://localhost:5000/service1/hello`,你应该能看到来自5001端口的响应。恭喜,一个基础的反向代理已经跑通了!

三、进阶为API网关:注入自定义逻辑

如果只是静态配置转发,那和Nginx差别不大。API网关的强大之处在于能在请求转发链路上轻松插入各种业务逻辑。YARP通过“管道”(Pipeline)的概念支持这一点。让我们实现两个常见功能:请求头验证和简单的响应缓存。

1. 自定义请求验证中间件

假设我们要求所有转到 `cluster1` 的请求必须携带一个特定的API密钥头。我们创建一个自定义的中间件。

// CustomAuthMiddleware.cs
public class CustomAuthMiddleware
{
    private readonly RequestDelegate _next;
    public CustomAuthMiddleware(RequestDelegate next) => _next = next;

    public async Task Invoke(HttpContext context)
    {
        // 检查请求是否指向我们关心的集群(可通过路由元数据或特定路径判断)
        // 这里简单检查路径前缀
        if (context.Request.Path.StartsWithSegments("/service1"))
        {
            if (!context.Request.Headers.TryGetValue("X-API-Key", out var key) || key != "MySecretKey")
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsync("Unauthorized: Missing or invalid API Key");
                return; // 中断管道,不再转发
            }
        }
        // 验证通过,继续执行代理管道
        await _next(context);
    }
}

在 `Program.cs` 中,我们需要将这个中间件插入到代理管道之前。

var app = builder.Build();

// 使用自定义认证中间件
app.UseMiddleware();

app.MapReverseProxy();
app.Run();

现在,不带 `X-API-Key: MySecretKey` 头请求 `/service1` 的请求将被拦截并返回401。

2. 实现简单的响应缓存

YARP允许我们通过实现 `IForwarderHttpClientFactory` 或使用 `ITransformProvider` 来深度定制行为。这里我们用一个更直接的方式演示思路:在代理执行前后通过事件钩子进行操作。我们可以创建一个自定义的 `IForwarderTransform` 来实现响应缓存逻辑,但为了清晰,我们先在自定义中间件里做一个概念演示。

// 在CustomAuthMiddleware的Invoke方法中,验证之后可以加入缓存逻辑
// 注意:这是一个简化演示,生产环境应使用分布式缓存(如Redis)并考虑缓存策略。
public async Task Invoke(HttpContext context, IMemoryCache cache)
{
    if (context.Request.Path.StartsWithSegments("/service1/api/products") && context.Request.Method == "GET")
    {
        var cacheKey = context.Request.Path + context.Request.QueryString;
        if (cache.TryGetValue(cacheKey, out string cachedResponse))
        {
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(cachedResponse);
            return; // 直接返回缓存,不再转发
        }
        // 缓存未命中,继续执行代理,但我们需要“窃听”响应
        var originalBodyStream = context.Response.Body;
        using var responseBody = new MemoryStream();
        context.Response.Body = responseBody;

        await _next(context); // 执行代理,请求被转发到后端

        // 如果响应成功,则缓存
        if (context.Response.StatusCode == 200)
        {
            responseBody.Seek(0, SeekOrigin.Begin);
            var responseText = await new StreamReader(responseBody).ReadToEndAsync();
            cache.Set(cacheKey, responseText, TimeSpan.FromSeconds(30)); // 缓存30秒

            responseBody.Seek(0, SeekOrigin.Begin);
            await responseBody.CopyToAsync(originalBodyStream);
        }
        context.Response.Body = originalBodyStream;
    }
    else
    {
        await _next(context);
    }
}

别忘了在 `Program.cs` 的 `builder.Services` 中添加 `IMemoryCache` 服务:`builder.Services.AddMemoryCache();`。

这个示例演示了在网关层拦截请求和响应的能力。在实际项目中,更复杂的限流、熔断、服务发现等功能,都可以通过类似的方式,在YARP提供的强大抽象之上进行构建。

四、动态配置与生产环境考量

硬编码在 `appsettings.json` 中的配置在服务动态上下线时不够灵活。YARP支持从任何来源(数据库、Consul、Apollo等)动态加载配置。你需要实现 `IProxyConfigProvider` 和 `IProxyConfig` 接口,并在服务注册时通过 `.LoadFromCustomProvider` 方法注入。

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
    .LoadFromCustomProvider(); // 添加自定义配置源

在你的 `MyCustomConfigProvider` 中,可以定期从数据库拉取最新的路由和集群信息,并调用 `IProxyConfig` 的 `SignalChange()` 方法来通知YARP热更新配置,无需重启网关服务。

最后的重要提示:自己构建网关给了你极大的灵活性,但也意味着你需要承担更多的责任,比如高性能转发、高可用部署、全面的日志监控和异常处理。对于大多数标准场景,直接使用成熟的开源网关(如Ocelot, 它本身也基于ASP.NET Core)或云厂商的托管网关可能是更高效的选择。但这次手把手的实践,无疑会让你在面对任何网关相关问题时,都能洞悉其本质,从容应对。

希望这篇教程能帮助你打开自定义API网关的大门。在源码库,我们始终相信,理解底层原理是成为高级开发者的必经之路。如果有任何问题或更深入的探讨,欢迎在评论区交流。 Happy coding!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。