• Asp-Net-Core开发笔记:给SwaggerUI加上登录保护功能


    前言#

    在 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();
        app.UseSwagger();
        app.UseSwaggerUI();
    
        return app;
      }
    }
    

    其他的都是常规的配置,重点在于 app.UseMiddleware(); 添加了一个中间件

    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(AuthSchemes.Swagger, null);
    

    AddScheme 方法可以添加各种类型的认证方案,这里添加了一个自定义的认证方案 SwaggerAuthenticationHandler,后面的参数是方案的名称和选项。

    为了避免硬编码,我写了个静态类

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

    SwaggerAuthenticationHandler#

    接下来实现这个自定义的认证方案

    其实就是把 Basic Authenticate 和固定用户名和密码结合在一起

    不过为了不在代码里硬编码,我把用户名和密码放在配置里了,通过注入 IOption 的方式获取。也可以放在数据库里,通过 EFCore 之类的去读取。

    public class SwaggerAuthenticationHandler : AuthenticationHandler {
      public SwaggerAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) {}
    
      public SwaggerAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) {}
    
      protected override async Task HandleAuthenticateAsync() {
        if (!Request.Headers.TryGetValue("Authorization", out var value)) {
          return AuthenticateResult.Fail("Missing Authorization Header");
        }
    
        var config = Context.RequestServices.GetRequiredService>().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 ,又要保证一定的安全性,本文提供的思路或许是一种比较简单又有效的解决方案。

    参考资料#

  • 相关阅读:
    5年经验之谈 —— 性能测试如何定位分析性能瓶颈?
    C/C++输入输出流函数大全
    数仓建模—ID Mapping(上)
    前端面试及答案:css 选择器有哪些?权重是什么样的?
    十二、组合API(2)
    element拖拽表单拖拽排序
    五、WebGPU Vertex Buffers 顶点缓冲区
    Linux:信号
    Epuck2机器人固件更新及IP查询
    汇编:调用Win32 API
  • 原文地址:https://www.cnblogs.com/deali/p/18204365