使用 Asp.net 核心创建另一个网站 api 的代理

Creating a proxy to another web api with Asp.net core

我正在开发一个 ASP.Net 核心 Web 应用程序,我需要在其中创建一种 "authentication proxy" 到另一个(外部)Web 服务。

我所说的身份验证代理是指我将通过我的 Web 应用程序的特定路径接收请求,并且必须检查这些请求的 headers 我将发出的身份验证令牌更早,然后将具有相同请求字符串/内容的所有请求重定向到外部 Web API,我的应用程序将通过 HTTP Basic 身份验证对其进行身份验证。

这是pseudo-code

中的整个过程

这是我目前拥有的。它似乎工作正常,但我想知道这是否真的应该这样做,或者是否没有更优雅或更好的解决方案?该解决方案是否会在 运行 扩展应用程序时产生问题?

[HttpGet]
public async Task GetStatement()
{
    //TODO check for token presence and reject if issue

    var queryString = Request.QueryString;
    var response = await _httpClient.GetAsync(queryString.Value);
    var content = await response.Content.ReadAsStringAsync();

    Response.StatusCode = (int)response.StatusCode;
    Response.ContentType = response.Content.Headers.ContentType.ToString();
    Response.ContentLength = response.Content.Headers.ContentLength;

    await Response.WriteAsync(content);
}

[HttpPost]
public async Task PostStatement()
{
    using (var streamContent = new StreamContent(Request.Body))
    {
        //TODO check for token presence and reject if issue

        var response = await _httpClient.PostAsync(string.Empty, streamContent);
        var content = await response.Content.ReadAsStringAsync();

        Response.StatusCode = (int)response.StatusCode;

        Response.ContentType = response.Content.Headers.ContentType?.ToString();
        Response.ContentLength = response.Content.Headers.ContentLength;

        await Response.WriteAsync(content);
    }
}

_httpClient 是在其他地方实例化的 HttpClient class 并且是单例并且具有 BaseAddresshttp://someexternalapp.com/api/

此外,是否有比手动操作更简单的令牌创建/令牌检查方法?

我最终实现了受 a project in Asp.Net's GitHub 启发的代理中间件。

它基本上实现了一个中间件,读取收到的请求,从中创建一个副本并将其发送回配置的服务,从服务读取响应并将其发送回调用者。

下面是 Proxy library for ASP.NET Core 的基本实现:

这没有实现授权,但对于寻找具有 ASP.NET 核心的简单反向代理的人可能有用。我们仅将其用于开发阶段。

using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;

namespace Sample.Proxy
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLogging(options =>
            {
                options.AddDebug();
                options.AddConsole(console =>
                {
                    console.IncludeScopes = true;
                });
            });

            services.AddProxy(options =>
            {
                options.MessageHandler = new HttpClientHandler
                {
                    AllowAutoRedirect = false,
                    UseCookies = true 
                };

                options.PrepareRequest = (originalRequest, message) =>
                {
                    var host = GetHeaderValue(originalRequest, "X-Forwarded-Host") ?? originalRequest.Host.Host;
                    var port = GetHeaderValue(originalRequest, "X-Forwarded-Port") ?? originalRequest.Host.Port.Value.ToString(CultureInfo.InvariantCulture);
                    var prefix = GetHeaderValue(originalRequest, "X-Forwarded-Prefix") ?? originalRequest.PathBase;

                    message.Headers.Add("X-Forwarded-Host", host);
                    if (!string.IsNullOrWhiteSpace(port)) message.Headers.Add("X-Forwarded-Port", port);
                    if (!string.IsNullOrWhiteSpace(prefix)) message.Headers.Add("X-Forwarded-Prefix", prefix);

                    return Task.FromResult(0);
                };
            });
        }

        private static string GetHeaderValue(HttpRequest request, string headerName)
        {
            return request.Headers.TryGetValue(headerName, out StringValues list) ? list.FirstOrDefault() : null;
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseWebSockets()
                .Map("/api", api => api.RunProxy(new Uri("http://localhost:8833")))
                .Map("/image", api => api.RunProxy(new Uri("http://localhost:8844")))
                .Map("/admin", api => api.RunProxy(new Uri("http://localhost:8822")))
                .RunProxy(new Uri("http://localhost:8811"));
        }

        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }
    }
}

如果有人感兴趣,我拿了 Microsoft.AspNetCore.Proxy 代码并用中间件让它变得更好。

在这里查看:https://github.com/twitchax/AspNetCore.Proxy. NuGet here: https://www.nuget.org/packages/AspNetCore.Proxy/。 Microsoft 存档了此 post 中提到的另一个,我计划回应有关此项目的任何问题。

基本上,它允许您在使用 args 路由并计算代理地址的方法上使用属性,从而使反向代理另一个 Web 服务器变得容易得多。

[ProxyRoute("api/searchgoogle/{query}")]
public static Task<string> SearchGoogleProxy(string query)
{
    // Get the proxied address.
    return Task.FromResult($"https://www.google.com/search?q={query}");
}

我很幸运使用 twitchax's AspNetCore.Proxy NuGet package, but could not get it to work using the ProxyRoute method shown in 。 (在我这边很可能是个错误。)

相反,我在 Statup.cs Configure() 方法中定义映射,类似于下面的代码。

app.UseProxy("api/someexternalapp-proxy/{arg1}", async (args) =>
{
    string url = "https://someexternalapp.com/" + args["arg1"];
    return await Task.FromResult<string>(url);
});

借助 James Lawruk 的回答 让 twitchax 代理属性起作用,我也遇到了 404 错误,直到我在 ProxyRoute 属性中指定了完整的路由。我的静态路由在一个单独的控制器中,控制器路由的相对路径不起作用。

这有效:

public class ProxyController : Controller
{
    [ProxyRoute("api/Proxy/{name}")]
    public static Task<string> Get(string name)
    {
        return Task.FromResult($"http://www.google.com/");
    }
}

这不是:

[Route("api/[controller]")]
public class ProxyController : Controller
{
    [ProxyRoute("{name}")]
    public static Task<string> Get(string name)
    {
        return Task.FromResult($"http://www.google.com/");
    }
}

希望这对某人有所帮助!

一个很好的反向代理中间件实现也可以在这里找到:https://auth0.com/blog/building-a-reverse-proxy-in-dot-net-core/

注意我在这里替换了这一行

requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());

requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToString());

原始 headers(例如,像带有不记名令牌的授权 header)在我的情况下如果没有我的修改将不会被添加。

这篇 post 讨论了用 C# 或 ASP.NET Core 编写简单的 HTTP 代理逻辑。并允许您的项目将请求代理给任何其他 URL。这与为您的 ASP.NET 核心项目部署代理服务器无关。

在项目的任意位置添加以下代码。

        public static HttpRequestMessage CreateProxyHttpRequest(this HttpContext context, Uri uri)
        {
            var request = context.Request;

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

            // Copy the request headers
            foreach (var header in request.Headers)
            {
                if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
                {
                    requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
                }
            }

            requestMessage.Headers.Host = uri.Authority;
            requestMessage.RequestUri = uri;
            requestMessage.Method = new HttpMethod(request.Method);

            return requestMessage;
        }

此方法隐蔽用户将 HttpContext.Request 发送到可重复使用的 HttpRequestMessage。这样就可以把这个消息发送到目标服务器了。

目标服务器响应后,您需要将响应的 HttpResponseMessage 复制到 HttpContext.Response,以便用户的浏览器直接获取它。

        public static async Task CopyProxyHttpResponse(this HttpContext context, HttpResponseMessage responseMessage)
        {
            if (responseMessage == null)
            {
                throw new ArgumentNullException(nameof(responseMessage));
            }

            var response = context.Response;

            response.StatusCode = (int)responseMessage.StatusCode;
            foreach (var header in responseMessage.Headers)
            {
                response.Headers[header.Key] = header.Value.ToArray();
            }

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

            // SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response.
            response.Headers.Remove("transfer-encoding");

            using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
            {
                await responseStream.CopyToAsync(response.Body, _streamCopyBufferSize, context.RequestAborted);
            }
        }

现在准备工作已经完成。回到我们的控制器:

    private readonly HttpClient _client;

    public YourController()
    {
        _client = new HttpClient(new HttpClientHandler()
        {
            AllowAutoRedirect = false
        });
    }

        public async Task<IActionResult> Rewrite()
        {
            var request = HttpContext.CreateProxyHttpRequest(new Uri("https://www.google.com"));
            var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
            await HttpContext.CopyProxyHttpResponse(response);
            return Ok();
        }

并尝试访问它。它将被代理到 google.com

Twitchax 的回答似乎是目前最好的解决方案。在对此进行研究时,我发现 Microsoft 正在开发更强大的解决方案,以准确解决 OP 试图解决的问题。

回购:https://github.com/microsoft/reverse-proxy

预览 1 的文章(他们实际上刚刚发布了 prev 2):https://devblogs.microsoft.com/dotnet/introducing-yarp-preview-1/

来自文章...

YARP is a project to create a reverse proxy server. It started when we noticed a pattern of questions from internal teams at Microsoft who were either building a reverse proxy for their service or had been asking about APIs and technology for building one, so we decided to get them all together to work on a common solution, which has become YARP.

YARP is a reverse proxy toolkit for building fast proxy servers in .NET using the infrastructure from ASP.NET and .NET. The key differentiator for YARP is that it is being designed to be easily customized and tweaked to match the specific needs of each deployment scenario. YARP plugs into the ASP.NET pipeline for handling incoming requests, and then has its own sub-pipeline for performing the steps to proxy the requests to backend servers. Customers can add additional modules, or replace stock modules as needed.
...
YARP works with either .NET Core 3.1 or .NET 5 preview 4 (or later). Download the preview 4 (or greater) of .NET 5 SDK from https://dotnet.microsoft.com/download/dotnet/5.0

更具体地说,他们的一个示例应用程序实现了身份验证(至于 OP 的初衷) https://github.com/microsoft/reverse-proxy/blob/master/samples/ReverseProxy.Auth.Sample/Startup.cs