使用Asp.net core创建一个代理到另一个Web API。

88
我正在开发一个ASP.Net Core Web应用程序,需要创建一种“认证代理”,以连接到另一个(外部)Web服务。
所谓的认证代理是指,我将通过Web应用程序的特定路径接收请求,并检查这些请求的头部是否包含我之前发布的身份验证令牌,然后将所有相同请求字符串/内容的请求重定向到一个外部Web API,我的应用程序将通过HTTP基本身份验证进行身份验证。
以下是伪代码中整个过程:
1.客户端通过在我前面发送的唯一URL上进行POST来请求令牌 2.我的应用程序对此POST作出响应并向其发送一个唯一的令牌。 3.客户端向我的应用程序的特定URL发出GET请求,并在HTTP标题中添加auth-token 4.我的应用程序获取请求,检查auth-token是否存在且有效 5.我的应用程序执行相同的请求到外部Web API,并使用BASIC身份验证对请求进行身份验证。 6.我的应用程序接收请求结果并将其发送回客户端。
以上是目前的实现方式,看起来运行正常,但我想知道是否有更优雅的解决方案?那种解决方案能否在长期运行中为扩展应用程序带来问题?
[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 类,具有 BaseAddresshttp://someexternalapp.com/api/

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


IIS反向代理 - Callum Linington
1
我真的看不出来那会如何实现。那么,我该如何从反向代理中检查认证令牌呢? - Gimly
说实话,我并没有看到你的代码有什么问题,我只是建议将其抽象化,并确保明确复制任何标头或查询字符串值,以保护自己免受攻击。 - Callum Linington
有用提示:使用ApiController创建代理 - J.Hpour
微软解决方案 Yarp.ReverseProxy - https://microsoft.github.io/reverse-proxy/articles/getting_started.html - surya
显示剩余3条评论
8个回答

44
如果有人感兴趣,我拿了Microsoft.AspNetCore.Proxy的代码,并用中间件使其更好。
在此处查看: https://github.com/twitchax/AspNetCore.Proxy。 NuGet在这里: https://www.nuget.org/packages/AspNetCore.Proxy/。 Microsoft存档了本帖子中提到的其他内容,我计划回应该项目上的任何问题。
基本上,它通过允许您在使用带参数路由的方法上使用属性并计算代理地址,使反向代理另一个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}");
}

谢谢。无法使ProxyRoute属性起作用,得到了404错误。可能是我做错了什么。使用UseProxy()方法成功了,再次感谢。 - James Lawruk
请查看下面的解决方案。中间件尚未考虑类路由。欢迎提出问题! :) - twitchax

26
这篇文章讲述了如何在C#或ASP.NET Core中编写一个简单的HTTP代理逻辑,并允许你的项目将请求代理到任何其他URL。它不是关于为你的ASP.NET Core项目部署代理服务器。
在你的项目的任何地方添加以下代码。
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 new EmptyResult();
}

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

![](/uploads/img-f2dd7ca2-79e4-4846-a7d0-6685f9b33ff4.png)


1
问题在于,具有相对路径的图像和脚本将无法正确加载。 - Renato Sanhueza
3
谢谢你提供的代码。这里有足够的内容可以引导我走向想要去的方向。 - Michael

24

我最终实现了一个由Asp.Net的GitHub项目灵感启发的代理中间件。

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


2
你能分享一下你的中间件实现吗?如果可以的话。它是基于 .Net Core 的吗?谢谢。 - Dmitriy
@Dmitriy,很抱歉我不能分享实现方式,因为它是封闭源代码程序的一部分。但基本上它与问题中的代码相同,只是作为中间件实现。请查看https://github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Proxy/ProxyMiddleware.cs文件,以了解如何启动中间件。 - Gimly

10

在这里还可以找到一个不错的反向代理中间件实现: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());

在我的情况下,原始头文件(例如带有承载令牌的授权头)不会在没有我的修改的情况下添加。


2
该代理还不会将查询字符串添加到它代理的请求中。我在BuildTargetUri方法中使用string query = request.QueryString.ToString();来添加查询字符串。 - Eddie

6
Twitchax的回答似乎是目前最好的解决方案。在研究中,我发现微软正在开发一种更强大的解决方案,以解决OP试图解决的确切问题。
仓库链接: https://github.com/microsoft/reverse-proxy 预览1文章(实际上他们刚刚发布了预览2): https://devblogs.microsoft.com/dotnet/introducing-yarp-preview-1/ 从文章中可以看到...
YARP是一个创建反向代理服务器的项目。它起源于微软内部团队的一系列问题,这些团队要么正在为其服务构建反向代理,要么正在询问有关构建反向代理的API和技术,因此我们决定将它们聚集在一起共同解决问题,这就是YARP。

YARP是一个反向代理工具包,用于使用ASP.NET和.NET基础架构在.NET中构建快速代理服务器。 YARP的关键不同之处在于它被设计为易于定制和调整,以匹配每个部署方案的特定需求。 YARP插入ASP.NET管道以处理传入请求,然后具有自己的子管道以执行将请求代理到后端服务器的步骤。客户可以添加其他模块或根据需要替换库存模块。
...
YARP与.NET Core 3.1或.NET 5预览4(或更高版本)配合使用。从https://dotnet.microsoft.com/download/dotnet/5.0下载.NET 5 SDK的预览4(或更高版本)。
更具体地说,他们的样例应用程序之一实现了身份验证(针对原始意图)。https://github.com/microsoft/reverse-proxy/blob/master/samples/ReverseProxy.Auth.Sample/Startup.cs

哦,@spencer741,这很有趣,看起来设置也太容易了。 - genuinefafa
1
@genuinefafa 是的确如此...它目前处于早期阶段。从安全或性能角度来看,现在还不能信任它。 - spencer741
看起来现在非常有前途。 - spencer741
一个对于后端服务进行请求身份验证的好例子是:https://github.com/microsoft/reverse-proxy/blob/main/samples/ReverseProxy.Transforms.Sample/Startup.cs - Miha Pirnat

6
我使用了twitchax的AspNetCore.Proxy NuGet包,运气不错,但无法使用twitchax的答案中显示的ProxyRoute方法使其正常工作。(可能是我的错误。)相反,我在Statup.cs Configure()方法中定义了类似下面代码的映射。
app.UseProxy("api/someexternalapp-proxy/{arg1}", async (args) =>
{
    string url = "https://someexternalapp.com/" + args["arg1"];
    return await Task.FromResult<string>(url);
});

这是您所做的唯一配置吗? - BrunoMartinsPro
没事了,我把 "api/someexternalapp-proxy/{arg1}" 改成了 "api/someexternalapp-proxy/{**catchall}",并在 ConfigureServices 中添加了 "services.AddProxies();",现在它可以正常工作了! - BrunoMartinsPro

5
借鉴James Lawruk的回答https://dev59.com/SVgQ5IYBdhLWcg3w7oZA#54149906,以使twitchax代理属性起作用。在ProxyRoute属性中指定完整的路由信息之前,我也遇到了404错误。我的静态路由在一个单独的控制器中,并从控制器的路由中提取相对路径未能生效。

这样做是有效的:

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

希望这能帮助到某些人!

啊,好的。随意在那上报一个 bug。应该很容易修复。 - twitchax

2
这里是一个基本实现ASP.NET Core的代理库
这并没有实现授权,但对于寻求用ASP.NET Core实现简单反向代理的人可能会很有用。我们仅在开发阶段使用它。
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();
        }
    }
}

2
你能否更新NuGet包?这段代码与发布的0.2.0版本不兼容。 - Kugel
2
不确定我在这段代码中是否遗漏了什么,但我无法解决 services.AddProxy(...)。我正在使用 Microsoft.AspNetCore.Proxy v0.2.0。此外,RunProxy 方法不接受 Uri 作为参数。这个示例使用的是哪个版本? - Allan
1
我使用了来自预览源的v0.2 NuGet:https://dotnet.myget.org/feed/aspnetcore-release/package/nuget/Microsoft.AspNetCore.Proxy - Kerem Demirer
2
当我编译源代码时,似乎SDK不支持asp net core 2.1。 - Bangyou
同样的问题。无法解决 Microsoft.AspNetCore.Proxy v0.2.0 中的 services.AddProxy() 函数... 自 2018 年 3 月以来,有人能给我们提供解决方案吗? - amin89
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接