ASP.NET core 2使用重写中间件充当反向代理

2024-04-05

我正在努力让我的 asp.net core 2 应用程序使用 URL 重写规则充当反向代理。

我的startup.cs中有以下内容:

var rewriteRules = new RewriteOptions()
                .AddRedirectToHttps();
                .AddRewrite(@"^POC/(.*)", "http://192.168.7.73:3001/$1", true);
app.UseRewriter(rewriteRules);

重写规则与我的 IIS 设置中的完全相同(我试图用此方法替换它),效果很好。

我假设它可能与转发标头有关?或者也许我只是不明白重写中间件应该如何工作,如果您希望转发请求而不是仅相对于当前主机名重写。


反向代理可以在中间件中模拟/实现:

首先是启动类,我们在其中添加 IUrlRewriter 服务和 ProxyMiddleware。

public class Startup
{
    private readonly IConfiguration _configuration;

    public Startup(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IUrlRewriter>(new SingleRegexRewriter(@"^/POC/(.*)", "http://192.168.7.73:3001/$1"));
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseRewriter(new RewriteOptions().AddRedirectToHttps());
        app.UseMiddleware<ProxyMiddleware>();
    }
}

接下来我们将创建 IUrlRewriter 的基本实现。 RewriteUri 方法必须将 HttpContext 转换为绝对 Uri。如果 url 不应在中间件中重定向,则返回 null。

public interface IUrlRewriter
{
    Task<Uri> RewriteUri(HttpContext context);
}

public class SingleRegexRewriter : IUrlRewriter
{
    private readonly string _pattern;
    private readonly string _replacement;
    private readonly RegexOptions _options;

    public SingleRegexRewriter(string pattern, string replacement)
        : this(pattern, replacement, RegexOptions.None) { }

    public SingleRegexRewriter(string pattern, string replacement, RegexOptions options)
    {
        _pattern = pattern ?? throw new ArgumentNullException(nameof(pattern));
        _replacement = replacement ?? throw new ArgumentNullException(nameof(pattern));
        _options = options;
    }

    public Task<Uri> RewriteUri(HttpContext context)
    {
        string url = context.Request.Path + context.Request.QueryString;
        var newUri = Regex.Replace(url, _pattern, _replacement);

        if (Uri.TryCreate(newUri, UriKind.Absolute, out var targetUri))
        {
            return Task.FromResult(targetUri);
        }

        return Task.FromResult((Uri)null);
    }
}

然后是中间件(从旧版本的 asp net proxy 中窃取)repo https://github.com/aspnet/Proxy/)并定制。它获取 IUrlRewrite 服务作为参数Invoke method.

管道是:

  • 尝试重写网址
  • 创建 HttpRequest 消息
  • 复制请求头和内容
  • 发送请求
  • 复制响应头
  • 复制回复内容
  • done

Et voila

public class ProxyMiddleware
{
    private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler()
    {
        AllowAutoRedirect = false,
        MaxConnectionsPerServer = int.MaxValue,
        UseCookies = false,
    });

    private const string CDN_HEADER_NAME = "Cache-Control";
    private static readonly string[] NotForwardedHttpHeaders = new[] { "Connection", "Host" };

    private readonly RequestDelegate _next;
    private readonly ILogger<ProxyMiddleware> _logger;

    public ProxyMiddleware(
           RequestDelegate next,
           ILogger<ProxyMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context, IUrlRewriter urlRewriter)
    {
        var targetUri = await urlRewriter.RewriteUri(context);

        if (targetUri != null)
        {
            var requestMessage = GenerateProxifiedRequest(context, targetUri);
            await SendAsync(context, requestMessage);

            return;
        }

        await _next(context);
    }

    private async Task SendAsync(HttpContext context, HttpRequestMessage requestMessage)
    {
        using (var responseMessage = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted))
        {
            context.Response.StatusCode = (int)responseMessage.StatusCode;

            foreach (var header in responseMessage.Headers)
            {
                context.Response.Headers[header.Key] = header.Value.ToArray();
            }

            foreach (var header in responseMessage.Content.Headers)
            {
                context.Response.Headers[header.Key] = header.Value.ToArray();
            }

            context.Response.Headers.Remove("transfer-encoding");

            if (!context.Response.Headers.ContainsKey(CDN_HEADER_NAME))
            {
                context.Response.Headers.Add(CDN_HEADER_NAME, "no-cache, no-store");
            }

            await responseMessage.Content.CopyToAsync(context.Response.Body);
        }
    }

    private static HttpRequestMessage GenerateProxifiedRequest(HttpContext context, Uri targetUri)
    {
        var requestMessage = new HttpRequestMessage();
        CopyRequestContentAndHeaders(context, requestMessage);

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


        return requestMessage;
    }

    private static void CopyRequestContentAndHeaders(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)
        {
            if (!NotForwardedHttpHeaders.Contains(header.Key))
            {
                if (header.Key != "User-Agent")
                {
                    if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
                    {
                        requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
                    }
                }
                else
                {
                    string userAgent = header.Value.Count > 0 ? (header.Value[0] + " " + context.TraceIdentifier) : string.Empty;

                    if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, userAgent) && requestMessage.Content != null)
                    {
                        requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, userAgent);
                    }
                }

            }
        }
    }

    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);
    }
}

奖励:其他一些重写器

public class PrefixRewriter : IUrlRewriter
{
    private readonly PathString _prefix;
    private readonly string _newHost;

    public PrefixRewriter(PathString prefix, string newHost)
    {
        _prefix = prefix;
        _newHost = newHost;
    }

    public Task<Uri> RewriteUri(HttpContext context)
    {
        if (context.Request.Path.StartsWithSegments(_prefix))
        {
            var newUri = context.Request.Path.Value.Remove(0, _prefix.Value.Length) + context.Request.QueryString;
            var targetUri = new Uri(_newHost + newUri);
            return Task.FromResult(targetUri);
        }

        return Task.FromResult((Uri)null);
    }
}

public class MergeRewriter : IUrlRewriter
{
    private readonly List<IUrlRewriter> _rewriters = new List<IUrlRewriter>();
    public MergeRewriter()
    {
    }
    public MergeRewriter(IEnumerable<IUrlRewriter> rewriters)
    {
        if (rewriters == null) throw new ArgumentNullException(nameof(rewriters));

        _rewriters.AddRange(rewriters);
    }

    public MergeRewriter Add(IUrlRewriter rewriter)
    {
        if (rewriter == null) throw new ArgumentNullException(nameof(rewriter));

        _rewriters.Add(rewriter);

        return this;
    }

    public async Task<Uri> RewriteUri(HttpContext context)
    {
        foreach (var rewriter in _rewriters)
        {
            var targetUri = await rewriter.RewriteUri(context);
            if(targetUri != null)
            {
                return targetUri;
            }
        }

        return null;
    }
}

// In Statup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IUrlRewriter>(new MergeRewriter()
        .Add(new PrefixRewriter("/POC/API", "http://localhost:1234"))
        .Add(new SingleRegexRewriter(@"^/POC/(.*)", "http://192.168.7.73:3001/$1")));
}

Edit

我发现一个项目可以做同样的事情,但有更多其他功能https://github.com/damianh/ProxyKit https://github.com/damianh/ProxyKit作为 nuget 包

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ASP.NET core 2使用重写中间件充当反向代理 的相关文章

随机推荐