Asp-Net-Core開發筆記:給SwaggerUI加上登入保護功能

程序设计实验室發表於2024-05-21

前言

在 SwaggerUI 中加入登入驗證,是我很早前就做過的,不過之前的做法總感覺有點硬編碼,最近 .Net8 增加了一個新特性:呼叫 MapSwagger().RequireAuthorization 來保護 Swagger UI ,但官方的這個功能又像半成品一樣,只能使用 postman curl 之類的工具帶上 Authorization header 來請求,在瀏覽器裡開啟就直接401了 ……

剛好有個專案需要用到這個功能,於是我把之前做過的 SwaggerUI 登入認證中介軟體拿出來重構了一下。

這次我依然使用 Basic Auth 的方式來登入,寫了一個自定義的 SwaggerAuthenticationHandler,透過 Microsoft.AspNetCore.Authentication 提供的擴充套件方法來實現登入。

PS:本文以我最近在開發的單點認證專案(IdentityServerLite)為例

配置Swagger

這次我試著不按照寫程式碼的順序,而是站在使用者的角度來介紹,也許會更直觀一些。

編輯 src/IdsLite.Api/Extensions/CfgSwagger.cs 檔案 (顧名思義,這是用來配置Swagger的相關擴充套件方法)

public static class CfgSwagger {
  public static IServiceCollection AddSwagger(this IServiceCollection services) {
    services.AddSwaggerGen();
    return services;
  }

  public static IApplicationBuilder UseSwaggerWithAuthorize(this IApplicationBuilder app) {
    app.UseMiddleware<SwaggerBasicAuthMiddleware>();
    app.UseSwagger();
    app.UseSwaggerUI();

    return app;
  }
}

其他的都是常規的配置,重點在於 app.UseMiddleware<SwaggerBasicAuthMiddleware>(); 新增了一箇中介軟體

SwaggerBasicAuth 中介軟體

來編寫這個中介軟體,程式碼路徑 src/IdsLite.Api/Middlewares/SwaggerBasicAuthMiddleware.cs

public class SwaggerBasicAuthMiddleware {
  private readonly RequestDelegate _next;

  public SwaggerBasicAuthMiddleware(RequestDelegate next) {
    _next = next;
  }

  public async Task InvokeAsync(HttpContext context) {
    if (context.Request.Path.StartsWithSegments("/swagger")) {
      var result = await context.AuthenticateAsync(AuthSchemes.Swagger);
      if (!result.Succeeded) {
        context.Response.Headers["WWW-Authenticate"] = "Basic";
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return;
      }
    }

    await _next(context);
  }
}

主要邏輯在 InvokeAsync 方法裡

判斷當前地址以 /swagger 開頭的話,就進入身份認證流程,如果配置了其他 SwaggerUI 地址,記得同步修改這個中介軟體的配置,或者做成通用的配置,避免硬編碼。

這裡使用了 Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions 提供的擴充套件方法 context.AuthenticateAsync("Scheme Name") 來驗證身份 (具體的 scheme 我們後面會實現)

如果驗證失敗的話,返回 401 ,同時新增響應頭 WWW-Authenticate:Basic ,這樣就能在瀏覽器裡彈出輸入使用者名稱和密碼的提示框了。

AuthenticationScheme

在註冊 Authentication 服務的時候,可以新增一些其他的 scheme

PS: AspNetCore 的這套 Identity 確實有點複雜,用了這麼久感覺還是沒有系統的認識這個 Identity 框架

註冊服務

註冊服務的程式碼大概是這樣

services
  .AddAuthentication(options => {
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
  })
  .AddJwtBearer(...)
  .AddScheme<AuthenticationSchemeOptions, SwaggerAuthenticationHandler>(AuthSchemes.Swagger, null);

AddScheme 方法可以新增各種型別的認證方案,這裡新增了一個自定義的認證方案 SwaggerAuthenticationHandler,後面的引數是方案的名稱和選項。

為了避免硬編碼,我寫了個靜態類

public static class AuthSchemes {
  public const string Swagger = "SwaggerAuthentication";
}

SwaggerAuthenticationHandler

接下來實現這個自定義的認證方案

其實就是把 Basic Authenticate 和固定使用者名稱和密碼結合在一起

不過為了不在程式碼裡硬編碼,我把使用者名稱和密碼放在配置裡了,透過注入 IOption<T> 的方式獲取。也可以放在資料庫裡,透過 EFCore 之類的去讀取。

public class SwaggerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
  public SwaggerAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) {}

  public SwaggerAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) {}

  protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
    if (!Request.Headers.TryGetValue("Authorization", out var value)) {
      return AuthenticateResult.Fail("Missing Authorization Header");
    }

    var config = Context.RequestServices.GetRequiredService<IOptions<IdsLiteConfig>>().Value;

    try {
      var authHeader = AuthenticationHeaderValue.Parse(value);
      var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
      var credentials = Encoding.UTF8.GetString(credentialBytes).Split(":", 2);
      var username = credentials[0];
      var password = credentials[1];

      if (username != config.Swagger.UserName || password != config.Swagger.Password) {
        return AuthenticateResult.Fail("Invalid Username or Password");
      }

      var claims = new[] { new Claim(ClaimTypes.Name, username) };
      var identity = new ClaimsIdentity(claims, Scheme.Name);
      var principal = new ClaimsPrincipal(identity);
      var ticket = new AuthenticationTicket(principal, Scheme.Name);

      return AuthenticateResult.Success(ticket);
    }
    catch {
      return AuthenticateResult.Fail("Invalid Authorization Header");
    }
  }
}

try 裡面的程式碼,就是從 request header 裡讀取 basic auth 的使用者名稱和密碼(通常是 Base64 編碼過的),解碼之後判斷是否正確,然後返回認證結果。

擴充套件

還可以整合 OpenIDConnect 和 OAuth ,我還沒有實踐,詳情見參考資料。

小結

既要在專案釋出後訪問 SwaggerUI ,又要保證一定的安全性,本文提供的思路或許是一種比較簡單又有效的解決方案。

參考資料

  • https://medium.com/@niteshsinghal85/securing-swagger-in-production-92d0a045a5
  • https://medium.com/@niteshsinghal85/securing-swagger-ui-in-production-in-asp-net-core-part-2-dc2ae0f03c73

相關文章