4
votes

Utilisez l'attribut [Authorize] sur l'interface utilisateur Swagger avec ASP.NET Core Swashbuckle

Dans ASP.NET Core, à l'aide de Swashbuckle.AspNetCore , comment protéger l'accès à mon interface utilisateur Swagger de la même manière que le décorer avec l'attribut [Authorize] ?

Je veux que l'attribut (équivalent de) [Authorize] s'exécute, comme pour un contrôleur / action normalement décoré, quand quelqu'un essaie d'accéder à /swagger -URL sur mon application Web, de sorte que mon AuthenticationHandler<T> soit exécuté .


0 commentaires

3 Réponses :


3
votes

L'intergiciel Swagger est totalement indépendant du pipeline MVC, il n'est donc pas possible dès le départ. Cependant, avec un peu de rétro-ingénierie, j'ai trouvé une solution de contournement. Cela implique de réimplémenter la plupart des intergiciels dans un contrôleur personnalisé, donc c'est un peu compliqué, et évidemment cela peut rompre avec une future mise à jour.

Tout d'abord, nous devons arrêter d'appeler IApplicationBuilder.UseSwagger et IApplicationBuilder.UseSwaggerUI , afin qu'il ne soit pas en conflit avec notre contrôleur.

Ensuite, nous devons ajouter tout ce qui a été ajouté par ces méthodes en modifiant notre Startup.cs :

[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();
    }
}

Enfin, il y a deux choses à swagger.json : la génération du fichier swagger.json et la génération de l'interface utilisateur swagger. Nous faisons cela avec un contrôleur personnalisé:

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


0 commentaires

1
votes

Eh bien, j'ai trouvé une solution simple au problème. Vous devez effectuer les opérations suivantes:

  • Implémentez un middleware. Si vous en avez un, vous pouvez l'utiliser.
  • app.UseSwagger () doit être appelé après app.UseAuthentication ().
  • Dans la méthode Invoke du middleware, vérifiez simplement le chemin pour swagger et redirigez l'utilisateur vers la page d'accueil / d'autres pages sous mise en page, etc. qui ont activé l'authentification ou écrivez simplement un message comme "Non autorisé" et retournez-le.
  • C'est bien meilleur qu'un attribut car vous pouvez arrêter l'utilisateur avant d'atteindre le contrôleur et il peut facilement être mis à l'échelle pour tous les contrôleurs.

J'espère que cela t'aides.


2 commentaires

Cela pourrait être la meilleure option, mais veuillez fournir plus de détails, afin que je puisse décider si c'est faisable. Merci.


J'ai implémenté quelque chose de similaire au middleware Owin pour l'API Web ASP.NET. Le middleware Owin / Katana est fondamentalement un précurseur du middleware ASP.NET Core, alors peut-être que quelqu'un le trouverait utile: stackoverflow.com/a/61602929/350384



0
votes

Vous pouvez y parvenir avec une solution middleware simple

Intergiciel

services.AddTransient<SwaggerAuthenticationMiddleware>();

Au démarrage -> Configurer (assurez-vous d'ajouter des éléments swagger après l'authentification et l'autorisation)

        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"));

Dans Startup -> ConfigureServices, enregistrez le middleware

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


5 commentaires

Solution intéressante! Votre authentification de base peut-elle être combinée avec une authentification utilisateur existante? Par coïncidence, j'ai une question très similaire et je me demande peut-être si votre solution fonctionnerait également: stackoverflow.com/questions/62727471


Ne remplit pas l'exigence de: "Je veux que l'attribut ... [Autoriser] s'exécute". L'authentification manuelle n'est pas une solution sûre pour l'avenir.


Il est actuellement convenu qu'il n'y a aucun moyen d'intégrer l'interface utilisateur de Swagger avec l'attribut [Authorize] sans travail significatif, même cela ne serait pas une preuve pour l'avenir avec la prochaine version de Swashbuckle etc. Cela offre un moyen simple de bloquer l'accès à l'interface utilisateur de Swagger, spécifiquement si vous avez une API hébergée publique et que toute l'interface utilisateur de Swagger est ouverte à la visualisation


Impressionnant. UserName.ToLower () == nom d'utilisateur échouera. Veuillez changer pour UserName.ToLower () == username.ToLower ()


bon point, il n'y a aucun besoin de faire plus bas du tout, je l'ai enlevé maintenant. @ user2021262 si la réponse vous a aidé, n'oubliez pas de la voter.