1. 程式人生 > >ASP.NET Core 反向代理部署知多少

ASP.NET Core 反向代理部署知多少

引言

最近在折騰統一認證中心,看到開源專案IdentityServer4.Admin集成了IdentityServer4和管理面板,就直接拿過來用了。在嘗試Nginx部署時遇到了諸如虛擬目錄對映,請求頭超長、基礎路徑對映有誤等問題,簡單記錄,以供後人參考。

Nginx 配置路由轉發

首先來看下IdentityServer4.Admin的專案結構:

IdentityServer4.Admin /                         
├── Id4.Admin.Api                  # 用於提供訪問Id4資源的WebApi專案
├── Id4.Admin                      # 用於提供管理Id4資源的Web管理面板 
├── Id4.STS.Identity               # 用於提供 STS 服務的Web專案

作為三個獨立的專案,分開部署很簡單,但為了統一入口管理,我傾向於將Id4.AdminId4.STS.Identity 部署在一個域名之下,Id4.Admin.API專案部署到閘道器中去。也就是通過http://auth.xxx.com訪問Id4.STS.Identity,通過http://auth.xxx.com/admin訪問Id4.Admin

這也就是遇到的第一個問題如何藉助Nginx實現單域名多站點部署!

Kestrel作為一個邊緣web伺服器部署時,其將獨佔一個IP和埠。在沒有反向代理伺服器的情況下,用作邊緣伺服器的Kestrel不支援在多個程序之間共享相同的IP和埠。當將Kestrel配置為在埠上偵聽時,Kestrel將處理該埠的所有網路通訊,並且忽略請求頭中指定的Host

請求頭,也就意味著Kestrel 不會負責請求轉發。

因此為了進行埠共享,我們需藉助反向代理將唯一的IP和埠上將請求轉發給Kestrel。也就是下面這張圖。

根據Nginx 官方配置文件,通過配置Location就可以實現指定路徑路由轉發。

server {
    listen 80;
    listen [::]:80;
    server_name mysite;
    
    location / {
        proxy_pass http://id4.sts.identity:80;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location /admin/ {
        proxy_pass http://id4.admin:80/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

我們 比較下兩個proxy_pass的配置:

  1. location / { proxy_pass http://id4.sts.identity:80; }
  2. location /admin/ { proxy_pass http://id4.admin:80/; }

主要的不同點是 location /admin/ 節點下proxy_pass http://id4.admin:80/結尾包含一個左斜槓 /。(如果沒有這個左斜槓,所有的請求都會被路由到根節點。)比如有個請求http://auth.xxx.com/admin/dashboard,那麼nginx根據以上配置會將請求路由到http://id4.admin:80/dashboard。也就是最後一個左斜槓會將替換掉 location 指定的路由規則,也就是這裡的/admin

但這樣就OK了嗎?Absolutely no!執行nginx -s reload 你將會得到一個大大的404

啟用 UsePathBase 中介軟體

這時就要用到UsePathBase中介軟體了,其作用就是設定站點請求基礎路徑。在Web專案中新增UsePathBase 中介軟體很簡單,首先在appsettings.json中新增一個配置項PATHBASE,然後Startup的Config中啟用就好。

appsettings.json
{
    "PATHBASE":"/admin"
}
-----

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    private IConfiguration Configuration { get; }
    // ...
    public void Configure(...)
    {
        // ...
        app.UsePathBase(Configuration.GetValue<string>("PATHBASE"));

啟用 UseForwardedHeaders 中介軟體

使用反向代理還有一個問題要注意,那就是反向代理會模糊一些請求資訊:

  1. 通過HTTP代理HTTPS請求時,原始傳輸協議(HTTPS)丟失,必須在請求頭中轉發。
  2. 由於應用程式是從代理伺服器收到請求的,而不是真正的請求來源,因此原始客戶端IP地址也必須在請求頭中轉發。

這也就是為什麼上面的Nginx 配置,會預設包含以下兩項配置的原因。

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Nginx已經預設配置轉發了以上資訊,那麼自然要顯式告知ASP.NET Core Web 應用要從請求頭中取回真實的請求資訊。配置很簡單,需要安`Microsoft.AspNetCore.HttpOverrides NuGet包,然後在Startup的Config中啟用中介軟體。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    private IConfiguration Configuration { get; }
    // ...
    public void Configure(...)
    {
        // ...        
        app.UseForwardedHeaders(new ForwardedHeadersOptions{
          ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto });

        app.UsePathBase(Configuration.GetValue<string>("PATHBASE"));

有一點必須注意,依賴於傳輸協議的任何元件,例如身份驗證,連結生成,重定向和地理位置,都必須在請求頭轉發中介軟體之後啟用。通常,除了診斷和錯誤處理中介軟體外,請求頭轉發中介軟體應先於其他中介軟體執行。

配置完成後,重新部署,對於一般的專案,應該可以正常運行了。但也可能遭遇:

解除 Nginx 請求頭轉發大小限制

針對這種錯誤當然要查Nginx錯誤日誌了,如果Nginx伺服器部署在Linux伺服器,那麼預設日誌檔案在/var/log/nginx/error.log,日誌如下:17677925 upstream sent too big header while reading response header from upstream。簡單翻譯就是請求頭資料過大。那我們就來看看轉發的請求頭到底會有多大,從下圖來看請求頭中攜帶的Cookie最大的有3K多。

nginx新增下面的配置即可:

proxy_buffer_size          128k;
proxy_buffers              4 256k;
proxy_busy_buffers_size    256k;

重新載入Nginx 配置,訪問成功。

Is All Set? No!

修復基礎路徑錯誤

當我嘗試點選Admin管理面板的連結時,得到無情的404,因為連結地址為:http://auth.xxx.com/configruaion/clients,正確的連結地址應該是http://auth.xxx.com/admin/configruaion/clients。也就是Razor TagHelper 渲染的<a asp-controller="Configruaion" asp-action="Clients">Manage Client</a>,並沒有幫按照UsePathBase指定的路徑生成a標籤連結。咱們只能看看原始碼一探究竟了Microsoft.AspNetCore.Mvc.TagHelpers/AnchorTagHelper.cs,最終在拼接Herf屬性時使用的是var pathBase = ActionContext.HttpContext.Request.PathBase;來拼接基礎路徑。也就是說說TagHelper根據Http請求上下文中獲取基礎路徑。因此如果採用location /admin/ { proxy_pass http://id4.admin:80/;這種路由對映,最終會丟失原始路由的基礎路徑,也就是/admin/ 路由部分。所以,我們還是乖乖把基礎路徑補充上,也就是proxy_pass http://id4.admin:80/admin/;
至此完成反向代理的單域名多站點部署。

最後

一波三折,但最終不負期望。最後完整Nginx配置放出,以供參考:

server {
    listen 80;
    listen [::]:80;
    server_name mysite;
    
    location / {
        proxy_pass http://id4.sts.identity:80;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location /admin/ {
        proxy_pass http://id4.admin:80/admin/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffer_size          128k;
        proxy_buffers              4 256k;
        proxy_busy_buffers_size    256k;
    }
}

參考資料:

  1. Configure ASP.NET Core to work with proxy servers and load balancers
  2. GitHub Issue: Deploy to subdirectory #15464
  3. ASP.Net Core 3 App Running under a Subdirectory on Nginx