Compartir a través de


Comunicación segura entre las integraciones de hospedaje y cliente

Este artículo es una continuación de dos artículos anteriores que muestran la creación de integraciones de hospedaje personalizadas y integraciones de cliente personalizadas.

Una de las principales ventajas de .NET.NET Aspire es cómo simplifica la configurabilidad de los recursos y de los clientes consumidores o integraciones. En este artículo se muestra cómo compartir credenciales de autenticación desde un recurso personalizado en una integración de hospedaje al cliente que consume en una integración de cliente personalizada. El recurso personalizado es un contenedor de MailDev que permite credenciales entrantes o salientes. La integración de cliente personalizada es un cliente mailKit que envía correos electrónicos.

Prerrequisitos

Dado que este artículo continúa desde el contenido anterior, ya debería haber creado la solución resultante como punto de partida para este artículo. Si aún no lo ha hecho, complete los siguientes artículos:

La solución resultante de estos artículos anteriores contiene los siguientes proyectos:

  • MailDev. hospedaje: contiene el tipo de recurso personalizado para el contenedor MailDev.
  • MailDevResource.AppHost: el host de aplicación de que usa el recurso personalizado y lo define como una dependencia para un servicio de boletín.
  • MailDevResource.NewsletterService: un proyecto de API web de ASP.NET Core que envía correos electrónicos mediante el contenedor de MailDev.
  • MailDevResource.ServiceDefaults: contiene las configuraciones de servicio predeterminadas diseñadas para compartir.
  • MailKit.Client: contiene la integración de cliente personalizada que expone el SmtpClient MailKit a través de una fábrica.

Actualiza el recurso MailDev

Para fluir las credenciales de autenticación desde el recurso de MailDev a la integración de MailKit, debe actualizar el recurso MailDev para incluir los parámetros de nombre de usuario y contraseña.

El contenedor MailDev admite la autenticación básica para el protocolo de transferencia de correo simple (SMTP) entrante y saliente. Para configurar las credenciales de entrada, debe establecer las variables de entorno MAILDEV_INCOMING_USER y MAILDEV_INCOMING_PASS. Para obtener más información, vea MailDev: Usage. Actualice el archivo MailDevResource.cs en el proyecto de MailDev.Hosting, reemplazando su contenido por el siguiente código de C#:

// For ease of discovery, resource types should be placed in
// the Aspire.Hosting.ApplicationModel namespace. If there is
// likelihood of a conflict on the resource name consider using
// an alternative namespace.
namespace Aspire.Hosting.ApplicationModel;

public sealed class MailDevResource(
    string name,
    ParameterResource? username,
    ParameterResource password)
        : ContainerResource(name), IResourceWithConnectionString
{
    // Constants used to refer to well known-endpoint names, this is specific
    // for each resource type. MailDev exposes an SMTP and HTTP endpoints.
    internal const string SmtpEndpointName = "smtp";
    internal const string HttpEndpointName = "http";

    private const string DefaultUsername = "mail-dev";

    // An EndpointReference is a core .NET Aspire type used for keeping
    // track of endpoint details in expressions. Simple literal values cannot
    // be used because endpoints are not known until containers are launched.
    private EndpointReference? _smtpReference;

    /// <summary>
    /// Gets the parameter that contains the MailDev SMTP server username.
    /// </summary>
    public ParameterResource? UsernameParameter { get; } = username;

    internal ReferenceExpression UserNameReference =>
        UsernameParameter is not null ?
        ReferenceExpression.Create($"{UsernameParameter}") :
        ReferenceExpression.Create($"{DefaultUsername}");

    /// <summary>
    /// Gets the parameter that contains the MailDev SMTP server password.
    /// </summary>
    public ParameterResource PasswordParameter { get; } = password;

    public EndpointReference SmtpEndpoint =>
        _smtpReference ??= new(this, SmtpEndpointName);

    // Required property on IResourceWithConnectionString. Represents a connection
    // string that applications can use to access the MailDev server. In this case
    // the connection string is composed of the SmtpEndpoint endpoint reference.
    public ReferenceExpression ConnectionStringExpression =>
        ReferenceExpression.Create(
            $"Endpoint=smtp://{SmtpEndpoint.Property(EndpointProperty.Host)}:{SmtpEndpoint.Property(EndpointProperty.Port)};Username={UserNameReference};Password={PasswordParameter}"
        );
}

Estas actualizaciones agregan las propiedades UsernameParameter y PasswordParameter. Estas propiedades se usan para almacenar los parámetros del nombre de usuario y la contraseña del MailDev. La propiedad ConnectionStringExpression se actualiza para incluir los parámetros de nombre de usuario y contraseña en la cadena de conexión. A continuación, actualice el archivo MailDevResourceBuilderExtensions.cs en el proyecto de MailDev.Hosting con el siguiente código de C#:

using Aspire.Hosting.ApplicationModel;

// Put extensions in the Aspire.Hosting namespace to ease discovery as referencing
// the .NET Aspire hosting package automatically adds this namespace.
namespace Aspire.Hosting;

public static class MailDevResourceBuilderExtensions
{
    private const string UserEnvVarName = "MAILDEV_INCOMING_USER";
    private const string PasswordEnvVarName = "MAILDEV_INCOMING_PASS";

    /// <summary>
    /// Adds the <see cref="MailDevResource"/> to the given
    /// <paramref name="builder"/> instance. Uses the "2.1.0" tag.
    /// </summary>
    /// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
    /// <param name="name">The name of the resource.</param>
    /// <param name="httpPort">The HTTP port.</param>
    /// <param name="smtpPort">The SMTP port.</param>
    /// <returns>
    /// An <see cref="IResourceBuilder{MailDevResource}"/> instance that
    /// represents the added MailDev resource.
    /// </returns>
    public static IResourceBuilder<MailDevResource> AddMailDev(
        this IDistributedApplicationBuilder builder,
        string name,
        int? httpPort = null,
        int? smtpPort = null,
        IResourceBuilder<ParameterResource>? userName = null,
        IResourceBuilder<ParameterResource>? password = null)
    {
        var passwordParameter = password?.Resource ??
            ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(
                builder, $"{name}-password");

        // The AddResource method is a core API within .NET Aspire and is
        // used by resource developers to wrap a custom resource in an
        // IResourceBuilder<T> instance. Extension methods to customize
        // the resource (if any exist) target the builder interface.
        var resource = new MailDevResource(
            name, userName?.Resource, passwordParameter);

        return builder.AddResource(resource)
                      .WithImage(MailDevContainerImageTags.Image)
                      .WithImageRegistry(MailDevContainerImageTags.Registry)
                      .WithImageTag(MailDevContainerImageTags.Tag)
                      .WithHttpEndpoint(
                          targetPort: 1080,
                          port: httpPort,
                          name: MailDevResource.HttpEndpointName)
                      .WithEndpoint(
                          targetPort: 1025,
                          port: smtpPort,
                          name: MailDevResource.SmtpEndpointName)
                      .WithEnvironment(context =>
                      {
                          context.EnvironmentVariables[UserEnvVarName] = resource.UserNameReference;
                          context.EnvironmentVariables[PasswordEnvVarName] = resource.PasswordParameter;
                      });
    }
}

// This class just contains constant strings that can be updated periodically
// when new versions of the underlying container are released.
internal static class MailDevContainerImageTags
{
    internal const string Registry = "docker.io";

    internal const string Image = "maildev/maildev";

    internal const string Tag = "2.1.0";
}

El código anterior actualiza el método de extensión AddMailDev para incluir los parámetros userName y password. El método WithEnvironment se actualiza para incluir las variables de entorno UserEnvVarName y PasswordEnvVarName. Estas variables de entorno se usan para establecer el nombre de usuario y la contraseña de MailDev.

Actualización del host de la aplicación

Ahora que el recurso se actualiza para incluir los parámetros de nombre de usuario y contraseña, debe actualizar el host de la aplicación para incluir estos parámetros. Actualice el archivo Program.cs en el proyecto de MailDevResource.AppHost con el siguiente código de C#:

var builder = DistributedApplication.CreateBuilder(args);

var mailDevUsername = builder.AddParameter("maildev-username");
var mailDevPassword = builder.AddParameter("maildev-password");

var maildev = builder.AddMailDev(
    name: "maildev",
    userName: mailDevUsername,
    password: mailDevPassword);

builder.AddProject<Projects.MailDevResource_NewsletterService>("newsletterservice")
       .WithReference(maildev);

builder.Build().Run();

El código anterior agrega dos parámetros para el nombre de usuario y la contraseña de MailDev. Asigna estos parámetros a las variables de entorno MAILDEV_INCOMING_USER y MAILDEV_INCOMING_PASS. El método AddMailDev tiene dos llamadas encadenadas a WithEnvironment que incluye estas variables de entorno. Para obtener más información sobre los parámetros, consulte Parámetros externos.

A continuación, configure los secretos para estos parámetros. Haga clic con el botón derecho en el proyecto de MailDevResource.AppHost y seleccione Manage User Secrets. Agregue el siguiente JSON al archivo secrets.json:

{
  "Parameters:maildev-username": "@admin",
  "Parameters:maildev-password": "t3st1ng"
}

Advertencia

Estas credenciales son solo para fines de demostración y MailDev está pensada para el desarrollo local. Estas credenciales son ficticias y no deben usarse en un entorno de producción.

Actualización de la integración de MailKit

Es buena práctica que las integraciones de cliente esperen que las cadenas de conexión contengan varios pares clave-valor y transformen estos pares en las propiedades adecuadas. Actualice el archivo MailKitClientSettings.cs en el proyecto de MailKit.Client con el siguiente código de C#:

using System.Data.Common;
using System.Net;

namespace MailKit.Client;

/// <summary>
/// Provides the client configuration settings for connecting MailKit to an SMTP server.
/// </summary>
public sealed class MailKitClientSettings
{
    internal const string DefaultConfigSectionName = "MailKit:Client";

    /// <summary>
    /// Gets or sets the SMTP server <see cref="Uri"/>.
    /// </summary>
    /// <value>
    /// The default value is <see langword="null"/>.
    /// </value>
    public Uri? Endpoint { get; set; }

    /// <summary>
    /// Gets or sets the network credentials that are optionally configurable for SMTP
    /// server's that require authentication.
    /// </summary>
    /// <value>
    /// The default value is <see langword="null"/>.
    /// </value>
    public NetworkCredential? Credentials { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the database health check is disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableHealthChecks { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableTracing { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableMetrics { get; set; }

    internal void ParseConnectionString(string? connectionString)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw new InvalidOperationException($"""
                    ConnectionString is missing.
                    It should be provided in 'ConnectionStrings:<connectionName>'
                    or '{DefaultConfigSectionName}:Endpoint' key.'
                    configuration section.
                    """);
        }

        if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
        {
            Endpoint = uri;
        }
        else
        {
            var builder = new DbConnectionStringBuilder
            {
                ConnectionString = connectionString
            };
            
            if (builder.TryGetValue("Endpoint", out var endpoint) is false)
            {
                throw new InvalidOperationException($"""
                        The 'ConnectionStrings:<connectionName>' (or 'Endpoint' key in
                        '{DefaultConfigSectionName}') is missing.
                        """);
            }

            if (Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out uri) is false)
            {
                throw new InvalidOperationException($"""
                        The 'ConnectionStrings:<connectionName>' (or 'Endpoint' key in
                        '{DefaultConfigSectionName}') isn't a valid URI.
                        """);
            }

            Endpoint = uri;
            
            if (builder.TryGetValue("Username", out var username) &&
                builder.TryGetValue("Password", out var password))
            {
                Credentials = new(
                    username.ToString(), password.ToString());
            }
        }
    }
}

La clase de configuración anterior ahora incluye una propiedad Credentials de tipo NetworkCredential. El método ParseConnectionString se actualiza para analizar las claves Username y Password de la cadena de conexión. Si las claves Username y Password están presentes, se crea un NetworkCredential y se asigna a la propiedad Credentials.

Con la clase de configuración actualizada para comprender y rellenar las credenciales, actualice la factoría para usar las credenciales condicionalmente si están configuradas. Actualice el archivo MailKitClientFactory.cs en el proyecto de MailKit.Client con el siguiente código de C#:

using System.Net;
using MailKit.Net.Smtp;

namespace MailKit.Client;

/// <summary>
/// A factory for creating <see cref="ISmtpClient"/> instances
/// given a <paramref name="smtpUri"/> (and optional <paramref name="credentials"/>).
/// </summary>
/// <param name="settings">
/// The <see cref="MailKitClientSettings"/> settings for the SMTP server
/// </param>
public sealed class MailKitClientFactory(MailKitClientSettings settings) : IDisposable
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    private SmtpClient? _client;

    /// <summary>
    /// Gets an <see cref="ISmtpClient"/> instance in the connected state
    /// (and that's been authenticated if configured).
    /// </summary>
    /// <param name="cancellationToken">Used to abort client creation and connection.</param>
    /// <returns>A connected (and authenticated) <see cref="ISmtpClient"/> instance.</returns>
    /// <remarks>
    /// Since both the connection and authentication are considered expensive operations,
    /// the <see cref="ISmtpClient"/> returned is intended to be used for the duration of a request
    /// (registered as 'Scoped') and is automatically disposed of.
    /// </remarks>
    public async Task<ISmtpClient> GetSmtpClientAsync(
        CancellationToken cancellationToken = default)
    {
        await _semaphore.WaitAsync(cancellationToken);

        try
        {
            if (_client is null)
            {
                _client = new SmtpClient();

                await _client.ConnectAsync(settings.Endpoint, cancellationToken)
                             .ConfigureAwait(false);

                if (settings.Credentials is not null)
                {
                    await _client.AuthenticateAsync(settings.Credentials, cancellationToken)
                                 .ConfigureAwait(false);
                }
            }
        }
        finally
        {
            _semaphore.Release();
        }       

        return _client;
    }

    public void Dispose()
    {
        _client?.Dispose();
        _semaphore.Dispose();
    }
}

Cuando la fábrica determina que se han configurado las credenciales, se autentica con el servidor SMTP después de conectarse y antes de devolver el SmtpClient.

Ejecución del ejemplo

Ahora que ha actualizado el recurso, los proyectos de integración correspondientes y el host de la aplicación, está listo para ejecutar la aplicación de ejemplo. Para ejecutar el ejemplo desde el IDE, seleccione F5 o use dotnet run en el directorio raíz de la solución para iniciar la aplicación; debería ver el panel de .NET.NET Aspire. Vaya al recurso de contenedor maildev y vea los detalles. Debería ver los parámetros de nombre de usuario y contraseña en los detalles del recurso, en la sección Variables de entorno:

.NET Aspire Panel: MailDev detalles del recurso del contenedor.

Del mismo modo, debería ver la cadena de conexión en los detalles del recurso de newsletterservice, en la sección variables de entorno:

.NET.NET Aspire Panel: detalles del recurso del servicio newsletter.

Valide que todo funciona según lo previsto.

Resumen

En este artículo se muestra cómo fluir las credenciales de autenticación de un recurso personalizado a una integración de cliente personalizada. El recurso personalizado es un contenedor de MailDev que permite credenciales entrantes o salientes. La integración de cliente personalizada es un cliente mailKit que envía correos electrónicos. Al actualizar el recurso para incluir los parámetros username y password, y actualizar la integración para analizar y usar estos parámetros, la autenticación fluye las credenciales de la integración de hospedaje a la integración de cliente.