ASP.NET Core 奇技淫巧之介面代理轉發

GuZhenYin發表於2020-08-11
前言

先講講本文的開發背景吧..

在如今前後端分離的大背景下,我們的客戶又有要求啦~

要前後端分離~ 然因為種種原因..沒辦法用用純前端的框架(其實是學習成本高,又沒錢請前端開發人員)...

所以最終決定了一種方案..

那就是採用MVC(只處理前端檢視層,單純是為了託管在.net core上)+Webapi的方式來實現前後端分離(講真,很奇葩)..

那麼問題就隨之而來了.

現在主流的前端框架都是託管在nodejs上,是通過axios來訪問後端API,可以通過配置axios的代理配置(proxyTable)來實現跨域訪問.

那麼我們的JS執行在MVC上,託管在.net core上..那咋辦呢?..沒有現成的轉發輪子..我們只有自己造了..

所以這就是本篇的背景 - -.~

 

正文

幸運的是ASP.NET Core 給我們提供了強大的中介軟體模式.

我們完全可以通過定義一個轉發中介軟體的形式來實現代理介面轉發,流程如圖:

廢話不多說,我們來建立我們的中介軟體:

 

一.建立檢測約定URL的介面與實現

首先定義一個介面IUrlRewriter 用來檢測我們的URL是否有對應字首,如果有,則產生新的URL地址:

這裡我們定義介面是為了方便以後更好的更換注入類來實現快速更換檢測字首的規則.

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

實現這個介面,如下(解釋都在註釋裡了):

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

 

二.建立代理轉發需要的ProxyHttpClient

建立獨立的ProxyHttpClient,主要是為了區分代理轉發的httpClient,方便後期新增日誌或做別的處理.程式碼如下:

    public class ProxyHttpClient
    {
        public HttpClient Client { get; private set; }

        public ProxyHttpClient(HttpClient httpClient)
        {
            Client = httpClient;
        }
    }

 

三.建立代理轉發的中介軟體

程式碼如下,中介軟體嘛,主要就是Invoke方法了,說明可以看註釋.

    public class ProxyMiddleware
    {
        private ProxyHttpClient _proxyHttpClient;
        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,
               ProxyHttpClient proxyHttpClient
               )
        {
            _next = next;
            _logger = logger;
            _proxyHttpClient = proxyHttpClient;
        }

        /// <summary>
        /// 通過中介軟體,攔截訪問,檢測字首,並轉發
        /// </summary>
        /// <param name="context"></param>
        /// <param name="urlRewriter"></param>
        /// <returns></returns>
        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 _proxyHttpClient.Client.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);
        }
    }

 

四.注入和啟用我們的中介軟體和ProxyHttpClient

我們在Startup的ConfigureServices中新增如下程式碼,注入我們的HttpClient與IUrlRewriter,如下:

 services.AddHttpClient<ProxyHttpClient>()
            .ConfigurePrimaryHttpMessageHandler(x => new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                MaxConnectionsPerServer = int.MaxValue,
                UseCookies = false,
            }); //注入我們定義的HttpClient
 services.AddSingleton<IUrlRewriter>(new PrefixRewriter("/webapp", "http://localhost:63445"));//這裡填寫字首與需要轉發的地址

然後在Startup的Configure中,啟動我們的中介軟體,如下:

  app.UseMiddleware<ProxyMiddleware>();

 

五.測試中介軟體效果

我們編寫前端程式碼如下:

            created: function () {
                this.mockTableData1();
                axios.get("/webapp/api/values/get", "123").then(res => { alert(res.data[0]) });
                axios.post("/webapp/api/values/post",{value: 'david'}).then(res => { alert(res.data.message) });

            }

在另外的WebApi專案,編寫介面如下:

        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", accstring.ToString() };
        }

        [HttpPost]
        public AjaxResult Post(dynamic value)
        {
            string aaa = JsonConvert.SerializeObject(value);
            return Success("OK");
        } 

效果如下,可以看到我們的檢視正確的獲取到了返回值:

 

 

寫在最後

這裡我們通過中介軟體的形式實現了介面的代理轉發,在具體的使用過程中肯定還會有一些小問題,而且這裡我們只實現了Http的轉發.ws的則沒有.

如果要使用的話,其實國外有一個開源的專案:https://github.com/ProxyKit, 已經有900多個star了.應該還不錯.

相關文章