asp.net core实现一个反向代理

本文将向你展示如何在C#和ASP.NET Core中实现一个反向代理功能。

使用反向代理

在网络基础设施的各种元素(如DNS服务器、防火墙、代理和类似的)中,反向代理受到各类IT人士,尤其是开发人员和运维人员的喜爱。这主要是由于近年来微服务架构的普及,以及在各类应用之间的高级集成需求。

什么是反向代理

一个标准的代理服务器在客户和服务器之间充当中介,以便进行处理,如缓存、流量监控、资源访问控制等。相关的事情是,客户需要连接到一个特定的服务器,代理服务器在评估如何满足这一要求后,代表客户联系目标服务器。

反向代理是一种特殊类型的代理服务器,它向客户隐藏目标服务器。客户端向代理服务器请求一个资源,代理服务器从另一个服务器上获取该资源并提供给客户端。在这种情况下,客户不知道该资源来自另一个服务器,与之相对的则是正向代理。

为什么使用反向代理

反向代理可以用于多种情况。

  • 负载平衡。

负载均衡应该是反向代理最广泛的用途之一。它可以根据一些特定的算法(轮询,加权轮询等),把HTTP请求分配到一组相同的服务器上,提高并发量,增加系统的可扩展性和可用性。

  • URL重写。

从SEO的角度来看,拥有有意义的URL是至关重要的。如果你不能改变你的网站的URLs,你可以把它们隐藏在反向代理后面,向用户和网络爬虫展示一个更有吸引力的版本。 静态内容代理。

许多反向代理服务器可以被配置为网络服务器。这允许你使用它们来提供静态内容,如HTML页面、JavaScript脚本、图像和其他文件,同时将动态内容的请求转发给专用服务器。这是一种基于内容类型的负载平衡。

  • API网关。

在一个微服务架构的系统中,通常会由很多个服务组成,每个服务都会提供WEBAPI入口。你可以使用一个反向代理,把微服务中的服务组合暴露一个单一的入口点。

  • 多个网站结合。

这与API网关的情况相当类似。在这种情况下,你可以为多个网站提供一个入口点,可能还有一个集中的主页。

为什么要使用自定义反向代理

最流行的反向代理之一是NGINX。当然,你也可以使用其他工具,如Pound或Squid,或者你也可以配置Apache Web服务器作为一个反向代理。这些工具提供了很多配置选项,允许你在大多数常见的情况下设置你的系统。其中一些还提供了用脚本语言扩展其功能的能力,例如,NGINX的Lua脚本语言。这种能力允许你满足一些简单服务器配置所不能提供的处理需求。HTTP头操作、有条件的请求转发、简单的内容转换等。 然而,你可能会发现一些场景,由于场景本身的复杂性,或者由于脚本变得难以维护,即使是集成的脚本语言也不能满足你的需求。例如,考虑这样一个场景:你需要在当前域内公开一个远程Web应用程序,并需要通过从数据库注入数据来准备HTTP请求,并操作响应以将其集成到当前环境中。或者你需要应用复杂的自定义规则来分析HTTP流量的情况。 如果你处于这种情况,你可能需要建立自己的自定义反向代理。

在C#中实现一个反向代理

实现你自己的反向代理的核心并不像它听起来那么难。当然,你不可能用NGINX或其他类似工具提供的所有选项来创建一个反向代理。然而,你可以专注于你的具体目标,以便在你最好的情况下解决它,而不诉诸于复杂的配置和脚本。 在本文的其余部分,你将在C#中建立一个简单的反向代理,它将允许你在你的网站中集成一个Google表单。这个表单是公开的,不需要认证,允许用户注册以获得一件T恤。当通过反向代理集成到你的Web应用程序时,它将被预先填入当前用户的一些个人数据。当然,网络应用程序的实现将保持简单,以便专注于与反向代理有关的挑战。让我们开始编码吧!

创建项目

建议使用vs直接创建ASP.NET Core项目,也可以使用控制台输入命令创建

$ dotnet new web

添加反向代理中间件

在反向代理的定义中,代理服务需要接受客户端的HTTP请求,然后将他转发到另一台服务器。我们可以使用中间件来实现这一操作。(中间件是ASP.NET Core 中处理HTTP请求和响应的组件)

在项目中添加一个ReverseProxyMiddleware中间件类。

public class ReverseProxyMiddleware
{
    private readonly RequestDelegate _nextMiddleware;

    public ReverseProxyMiddleware(RequestDelegate nextMiddleware)
    {
        _nextMiddleware = nextMiddleware;
    }

    public async Task Invoke(HttpContext context)
    {
        await _nextMiddleware(context);
    }
}

首先考虑拦截原始请求,定义一个BuildTargetUri()方法中来处理。示例中我们将/proxy的请求转发到本文链接。BuildTargetUri()方法中回匹配以/proxy开头的字符串,成功找到后会将/proxy替换成 https://scung.cn/p ,然后生成新的URI对象。如果没有找到对应的前缀,就会返回空对象。

public async Task Invoke(HttpContext context)
{
    var targetUri = BuildTargetUri(context.Request);

    await _nextMiddleware(context);
}

public Uri BuildTargetUri(HttpRequest request)
{
    Uri targetUri = null;

    if (request.Path.StartsWithSegments("/proxy", out var remainingPath))
    {
        targetUri = new Uri("https://scung.cn/p" + remainingPath);
    }

    return targetUri;
}

该方法试图从当前的HTTP上下文开始建立目标URI,即目标服务器的地址。如果BuildTargetUri()方法已返回目标URI,则意味着应将原始HTTP请求转发到目标服务器,因此必须处理原始请求。否则,当前中间件无法处理该请求,并将其传递给管道中的下一个中间件。

CreateTargetMessage()方法中会生成一个全新的HttpRequestMessage对象,并将客户端发来的消息复制一份到该对象中,然后将其返回。

private HttpRequestMessage CreateTargetMessage(HttpContext context, Uri targetUri)
{
    var requestMessage = new HttpRequestMessage();
    CopyFromOriginalRequestContentAndHeaders(context, requestMessage);

    requestMessage.RequestUri = targetUri;
    requestMessage.Headers.Host = targetUri.Host;
    requestMessage.Method = GetMethod(context.Request.Method);

    return requestMessage;
}

private void CopyFromOriginalRequestContentAndHeaders(HttpContext context, HttpRequestMessage requestMessage)
{
    var requestMethod = context.Request.Method;

    if (!HttpMethods.IsGet(requestMethod) &&
        !HttpMethods.IsHead(requestMethod) &&
        !HttpMethods.IsDelete(requestMethod) &&
        !HttpMethods.IsTrace(requestMethod))
    {
       var streamContent = new StreamContent(context.Request.Body);
       requestMessage.Content = streamContent;
    }

    foreach (var header in context.Request.Headers)
    {
       requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
    }
}

private static HttpMethod GetMethod(string method)
{
    if (HttpMethods.IsDelete(method)) return HttpMethod.Delete;
    if (HttpMethods.IsGet(method)) return HttpMethod.Get;
    if (HttpMethods.IsHead(method)) return HttpMethod.Head;
    if (HttpMethods.IsOptions(method)) return HttpMethod.Options;
    if (HttpMethods.IsPost(method)) return HttpMethod.Post;
    if (HttpMethods.IsPut(method)) return HttpMethod.Put;
    if (HttpMethods.IsTrace(method)) return HttpMethod.Trace;
    return new HttpMethod(method);
}

在Invoke()方法中,当处理请求时,先调用BuildTargetUri()方法构建出真实的URI,然后通过CreateTargetMessage()方法为目标服务器建立一个消息,并通过使用_httpClient私有属性来发送消息。然后,从目标服务器收到的响应被完全复制到要提供给客户端的响应中。

public async Task Invoke(HttpContext context)
{
    var targetUri = BuildTargetUri(context.Request);

    if (targetUri != null)
    {
        var targetRequestMessage = CreateTargetMessage(context, targetUri);

        using (var responseMessage = await _httpClient.SendAsync(targetRequestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted))
        {
          context.Response.StatusCode = (int)responseMessage.StatusCode;
          CopyFromTargetResponseHeaders(context, responseMessage);
          await responseMessage.Content.CopyToAsync(context.Response.Body);
        }
        return;
    }

    await _nextMiddleware(context);
}

使用反向代理中间件

把刚才写好的中间件加入到我们的程序中,找到Program.cs文件,在中间件的靠前部分加上。

app.UseMiddleware<ReverseProxyMiddleware>();

现在,我们的反向代理中间件已经添加近HTTP处理管道中了,直接启动应用,当我们程序成功运行后,在浏览器中访问 http://localhost:5000/proxy 。就可以看到如下内容。该应该已经成功的把我们的网站给代理过来了。

发布时间:2023-03-15