<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Asp-Net-Core開發(fā)筆記:給SwaggerUI加上登錄保護(hù)功能

          共 7161字,需瀏覽 15分鐘

           ·

          2024-05-21 16:35

          前言

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

          剛好有個(gè)項(xiàng)目需要用到這個(gè)功能,于是我把之前做過的 SwaggerUI 登錄認(rèn)證中間件拿出來重構(gòu)了一下。

          這次我依然使用 Basic Auth 的方式來登錄,寫了一個(gè)自定義的 SwaggerAuthenticationHandler,通過 Microsoft.AspNetCore.Authentication 提供的擴(kuò)展方法來實(shí)現(xiàn)登錄。

          ?

          PS:本文以我最近在開發(fā)的單點(diǎn)認(rèn)證項(xiàng)目(IdentityServerLite)為例

          配置Swagger

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

          編輯 src/IdsLite.Api/Extensions/CfgSwagger.cs 文件 (顧名思義,這是用來配置Swagger的相關(guān)擴(kuò)展方法)

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

          其他的都是常規(guī)的配置,重點(diǎn)在于 app.UseMiddleware<SwaggerBasicAuthMiddleware>(); 添加了一個(gè)中間件

          SwaggerBasicAuth 中間件

          來編寫這個(gè)中間件,代碼路徑 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 方法里

          判斷當(dāng)前地址以 /swagger 開頭的話,就進(jìn)入身份認(rèn)證流程,如果配置了其他 SwaggerUI 地址,記得同步修改這個(gè)中間件的配置,或者做成通用的配置,避免硬編碼。

          這里使用了 Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions 提供的擴(kuò)展方法 context.AuthenticateAsync("Scheme Name") 來驗(yàn)證身份 (具體的 scheme 我們后面會(huì)實(shí)現(xiàn))

          如果驗(yàn)證失敗的話,返回 401 ,同時(shí)添加響應(yīng)頭 WWW-Authenticate:Basic ,這樣就能在瀏覽器里彈出輸入用戶名和密碼的提示框了。

          AuthenticationScheme

          在注冊 Authentication 服務(wù)的時(shí)候,可以添加一些其他的 scheme

          ?

          PS: AspNetCore 的這套 Identity 確實(shí)有點(diǎn)復(fù)雜,用了這么久感覺還是沒有系統(tǒng)的認(rèn)識(shí)這個(gè) Identity 框架

          注冊服務(wù)

          注冊服務(wù)的代碼大概是這樣

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

          AddScheme 方法可以添加各種類型的認(rèn)證方案,這里添加了一個(gè)自定義的認(rèn)證方案 SwaggerAuthenticationHandler,后面的參數(shù)是方案的名稱和選項(xiàng)。

          為了避免硬編碼,我寫了個(gè)靜態(tài)類

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

          SwaggerAuthenticationHandler

          接下來實(shí)現(xiàn)這個(gè)自定義的認(rèn)證方案

          其實(shí)就是把 Basic Authenticate 和固定用戶名和密碼結(jié)合在一起

          不過為了不在代碼里硬編碼,我把用戶名和密碼放在配置里了,通過注入 IOption<T> 的方式獲取。也可以放在數(shù)據(jù)庫里,通過 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 編碼過的),解碼之后判斷是否正確,然后返回認(rèn)證結(jié)果。

          擴(kuò)展

          還可以集成 OpenIDConnect 和 OAuth ,我還沒有實(shí)踐,詳情見參考資料。

          小結(jié)

          既要在項(xiàng)目發(fā)布后訪問 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


          瀏覽 43
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美日比视频 | 青青草天天爽 | 理论片无码 | 亚洲狼人香蕉 | 亚州高清A∨在线观看 |