【问题标题】:Use the [Authorize]-attribute on Swagger UI with ASP.NET Core Swashbuckle在带有 ASP.NET Core Swashbuckle 的 Swagger UI 上使用 [Authorize] 属性
【发布时间】:2019-10-08 09:15:31
【问题描述】:

在 ASP.NET Core 中,使用 Swashbuckle.AspNetCore,如何保护对 Swagger UI 的访问,就像使用 [Authorize]-attribute 装饰它一样?

当有人试图访问我的网络应用程序上的/swagger-URL 时,我希望执行(等效于)[Authorize]-attribute,就像通常装饰的控制器/动作一样,这样我的自定义AuthenticationHandler<T> 被执行

【问题讨论】:

    标签: authentication asp.net-core swagger-ui swashbuckle


    【解决方案1】:

    Swagger 中间件完全独立于 MVC 管道,因此不可能开箱即用。但是,通过一些逆向工程,我找到了一种解决方法。它涉及在自定义控制器中重新实现大部分中间件,因此有点复杂,显然它可能会随着未来的更新而中断。

    首先,我们需要停止调用IApplicationBuilder.UseSwaggerIApplicationBuilder.UseSwaggerUI,以免与我们的控制器冲突。

    然后,我们必须通过修改 Startup.cs 来添加这些方法添加的所有内容:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("documentName", new Info { Title = "My API", Version = "v1" });
        });
    
        // RouteTemplate is no longer used (route will be set via the controller)
        services.Configure<SwaggerOptions>(c =>
        {
        });
    
        // RoutePrefix is no longer used (route will be set via the controller)
        services.Configure<SwaggerUIOptions>(c =>
        {
            // matches our controller route
            c.SwaggerEndpoint("/swagger/documentName/swagger.json", "My API V1");
        });
    }
    
    
    public void Configure(IApplicationBuilder app)
    {
        // we need a custom static files provider for the Swagger CSS etc..
        const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist";
        app.UseStaticFiles(new StaticFileOptions
        {
            RequestPath = "/swagger", // must match the swagger controller name
            FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace),
        });
    }
    

    最后要重新实现两件事:swagger.json文件的生成,以及swagger UI的生成。我们使用自定义控制器来做到这一点:

    [Authorize]
    [Route("[controller]")]
    public class SwaggerController : ControllerBase
    {
        [HttpGet("{documentName}/swagger.json")]
        public ActionResult<string> GetSwaggerJson([FromServices] ISwaggerProvider swaggerProvider, 
            [FromServices] IOptions<SwaggerOptions> swaggerOptions, [FromServices] IOptions<MvcJsonOptions> jsonOptions,
            [FromRoute] string documentName)
        {
            // documentName is the name provided via the AddSwaggerGen(c => { c.SwaggerDoc("documentName") })
            var swaggerDoc = swaggerProvider.GetSwagger(documentName);
    
            // One last opportunity to modify the Swagger Document - this time with request context
            var options = swaggerOptions.Value;
            foreach (var filter in options.PreSerializeFilters)
            {
                filter(swaggerDoc, HttpContext.Request);
            }
    
            var swaggerSerializer = SwaggerSerializerFactory.Create(jsonOptions);
            var jsonBuilder = new StringBuilder();
            using (var writer = new StringWriter(jsonBuilder))
            {
                swaggerSerializer.Serialize(writer, swaggerDoc);
                return Content(jsonBuilder.ToString(), "application/json");
            }
        }
    
        [HttpGet]
        [HttpGet("index.html")]
        public ActionResult<string> GetSwagger([FromServices] ISwaggerProvider swaggerProvider, [FromServices] IOptions<SwaggerUIOptions> swaggerUiOptions)
        {
            var options = swaggerUiOptions.Value;
            var serializer = CreateJsonSerializer();
    
            var indexArguments = new Dictionary<string, string>()
            {
                { "%(DocumentTitle)", options.DocumentTitle },
                { "%(HeadContent)", options.HeadContent },
                { "%(ConfigObject)", SerializeToJson(serializer, options.ConfigObject) },
                { "%(OAuthConfigObject)", SerializeToJson(serializer, options.OAuthConfigObject) }
            };
    
            using (var stream = options.IndexStream())
            {
                // Inject arguments before writing to response
                var htmlBuilder = new StringBuilder(new StreamReader(stream).ReadToEnd());
                foreach (var entry in indexArguments)
                {
                    htmlBuilder.Replace(entry.Key, entry.Value);
                }
    
                return Content(htmlBuilder.ToString(), "text/html;charset=utf-8");
            }
        }
    
        private JsonSerializer CreateJsonSerializer()
        {
            return JsonSerializer.Create(new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
                Converters = new[] { new StringEnumConverter(true) },
                NullValueHandling = NullValueHandling.Ignore,
                Formatting = Formatting.None,
                StringEscapeHandling = StringEscapeHandling.EscapeHtml
            });
        }
    
        private string SerializeToJson(JsonSerializer jsonSerializer, object obj)
        {
            var writer = new StringWriter();
            jsonSerializer.Serialize(writer, obj);
            return writer.ToString();
        }
    }
    

    【讨论】:

      【解决方案2】:

      您可以通过一个简单的中间件解决方案来实现这一目标

      中间件

      public class SwaggerAuthenticationMiddleware : IMiddleware
      {
          //CHANGE THIS TO SOMETHING STRONGER SO BRUTE FORCE ATTEMPTS CAN BE AVOIDED
          private const string UserName = "TestUser1";
          private const string Password = "TestPassword1";
      
          public async Task InvokeAsync(HttpContext context, RequestDelegate next)
          {
              //If we hit the swagger locally (in development) then don't worry about doing auth
              if (context.Request.Path.StartsWithSegments("/swagger") && !IsLocalRequest(context))
              {
                  string authHeader = context.Request.Headers["Authorization"];
                  if (authHeader != null && authHeader.StartsWith("Basic "))
                  {
                      // Get the encoded username and password
                      var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();
      
                      // Decode from Base64 to string
                      var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));
      
                      // Split username and password
                      var username = decodedUsernamePassword.Split(':', 2)[0];
                      var password = decodedUsernamePassword.Split(':', 2)[1];
      
                      // Check if login is correct
                      if (IsAuthorized(username, password))
                      {
                          await next.Invoke(context);
                          return;
                      }
                  }
      
                  // Return authentication type (causes browser to show login dialog)
                  context.Response.Headers["WWW-Authenticate"] = "Basic";
      
                  // Return unauthorized
                  context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
              }
              else
              {
                  await next.Invoke(context);
              }
          }
      
          private bool IsAuthorized(string username, string password) => UserName == username && Password == password;
      
          private bool IsLocalRequest(HttpContext context)
          {
              if(context.Request.Host.Value.StartsWith("localhost:"))
                  return true;
      
              //Handle running using the Microsoft.AspNetCore.TestHost and the site being run entirely locally in memory without an actual TCP/IP connection
              if (context.Connection.RemoteIpAddress == null && context.Connection.LocalIpAddress == null)
                  return true;
      
              if (context.Connection.RemoteIpAddress != null && context.Connection.RemoteIpAddress.Equals(context.Connection.LocalIpAddress))
                  return true;
      
              return IPAddress.IsLoopback(context.Connection.RemoteIpAddress);
          }
      }
      

      在启动时 -> 配置(确保在身份验证和授权后添加招摇的东西)

              app.UseAuthentication();
              app.UseAuthorization();
      
              //Enable Swagger and SwaggerUI
              app.UseMiddleware<SwaggerAuthenticationMiddleware>(); //can turn this into an extension if you ish
              app.UseSwagger();
              app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "my test api"));
      

      在 Startup -> ConfigureServices 注册中间件

      services.AddTransient<SwaggerAuthenticationMiddleware>();
      

      【讨论】:

      • 有趣的解决方案!您的基本身份验证可以与现有的基于用户的身份验证结合使用吗?巧合的是,我有一个非常相似的问题,可能想知道您的解决方案是否也能正常工作:stackoverflow.com/questions/62727471
      • 不满足以下要求:“我想要执行 ... [Authorize]-attribute”。手动身份验证不是未来安全的解决方案。
      • 同意目前没有办法将 Swagger UI 与 [Authorize] 属性集成而无需大量工作,即使这不会成为 Swashbuckle 等下一个版本的未来证明。这提供了一个简单的阻止对 Swagger UI 的访问的方法,特别是如果您有公共托管的 API 并且整个 Swagger UI 都开放供查看
      • 太棒了。 UserName.ToLower() == 用户名将失败。请改成 UserName.ToLower() == username.ToLower()
      • 好点,完全没必要做lower,我现在已经去掉了。 @user2021262 如果答案对你有帮助,别忘了点赞。
      【解决方案3】:

      嗯,我找到了解决问题的简单方法。 您需要执行以下操作:

      • 实现中间件。如果你有一个现有的,你可以使用它。
      • app.UseSwagger() 应该在 app.UseAuthentication() 之后调用。
      • 在中间件Invoke方法中,只需检查swagger的路径并将用户重定向到主页/布局下的其他页面等已启用身份验证或仅写“未授权”之类的消息并返回。
      • 这比属性要好得多,因为您可以在用户到达控制器之前阻止用户,并且可以轻松地将其扩展到所有控制器。

      希望这会有所帮助。

      【讨论】:

      • 这可能是更好的选择,但请提供更多详细信息,以便我决定是否可行。谢谢。
      • 我为 ASP.NET Web API 实现了类似于 Owin 中间件的东西。 Owin/Katana 中间件基本上是 ASP.NET Core 中间件的前身,所以也许有人会觉得它很有用:stackoverflow.com/a/61602929/350384
      【解决方案4】:

      我能够增强 @Ricky G 的答案以支持 asp.net 核心身份验证机制。

      在 SwaggerAuthenticationMiddleware 中,

      public async Task InvokeAsync(HttpContext context)
      {
          //Make sure we are hitting the swagger path, and not doing it locally as it just gets annoying :-)
          if (context.Request.Path.StartsWithSegments("/swagger"))
          {
              if (!context.User.Identity.IsAuthenticated)
              {
                  string authHeader = context.Request.Headers["Authorization"];
                  if (authHeader != null && authHeader.StartsWith("Basic "))
                  {
                      // Get the encoded username and password
                      var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();
      
                      // Decode from Base64 to string
                      var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));
      
                      // Split username and password
                      var username = decodedUsernamePassword.Split(':', 2)[0];
                      var password = decodedUsernamePassword.Split(':', 2)[1];
      
                      var signInManager = _httpContextAccessor.HttpContext.RequestServices.GetService<SignInManager<IdpUser>>();
                      var result = await signInManager.PasswordSignInAsync(username, password, false, lockoutOnFailure: false);
                      if (result.Succeeded)
                      {
                          await next.Invoke(context);
                          return;
                      }
                  }
      
                  // Return authentication type (causes browser to show login dialog)
                  context.Response.Headers["WWW-Authenticate"] = "Basic";
      
                  // Return unauthorized
                  context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
              }
              else
              {
                  await next.Invoke(context);
                  return;
              }
          }
          else
          {
              await next.Invoke(context);
          }
      }
      

      在 Startup.cs 中你必须像下面这样注册 HttpContextAccessor。

      services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-06-21
        • 1970-01-01
        • 2017-03-31
        • 1970-01-01
        • 2018-02-01
        • 2021-03-01
        相关资源
        最近更新 更多