Udostępnij za pośrednictwem


Bezpieczna komunikacja między hostingem a integracją client

Ten artykuł jest kontynuacją dwóch poprzednich artykułów demonstrujących tworzenie niestandardowych integracji hostingu i niestandardowych integracji client.

Jedną z podstawowych zalet .NET.NET Aspire jest uproszczenie konfigurowania zasobów oraz klientów końcowych (lub integracji). W tym artykule pokazano, jak udostępniać poświadczenia uwierzytelniania z zasobu niestandardowego w integracji hostingu do korzystania z client w niestandardowej integracji client. Zasób niestandardowy to kontener MailDev, który umożliwia zarządzanie poświadczeniami przychodzącymi lub wychodzącymi. Niestandardowa integracja client to client MailKit, która wysyła wiadomości e-mail.

Warunki wstępne

Ponieważ ten artykuł jest kontynuowany z poprzedniej zawartości, należy już utworzyć wynikowe rozwiązanie jako punkt wyjścia dla tego artykułu. Jeśli jeszcze tego nie zrobiłeś/zrobiłaś, ukończ następujące artykuły:

Wynikowe rozwiązanie z tych poprzednich artykułów zawiera następujące projekty:

  • MailDev. Hostowanie: zawiera niestandardowy typ zasobu dla kontenera MailDev.
  • MailDevResource.AppHost: host aplikacji , który używa zasobu niestandardowego i definiuje go jako zależność dla usługi newslettera.
  • MailDevResource.NewsletterService: projekt internetowego interfejsu API ASP.NET Core, który wysyła wiadomości e-mail przy użyciu kontenera MailDev.
  • MailDevResource.ServiceDefaults: zawiera domyślne konfiguracje usługi przeznaczone do udostępniania.
  • MailKit.Client: zawiera niestandardową integrację client, która udostępnia SmtpClient MailKit przez fabrykę.

Aktualizowanie zasobu MailDev

Aby przepływać poświadczenia uwierzytelniania z zasobu MailDev do integracji biblioteki MailKit, należy zaktualizować zasób MailDev, aby uwzględnić parametry nazwy użytkownika i hasła.

Kontener MailDev obsługuje uwierzytelnianie podstawowe zarówno dla przychodzącego, jak i wychodzącego prostego protokołu transferu poczty (SMTP). Aby skonfigurować poświadczenia dla przychodzących, należy ustawić zmienne środowiskowe MAILDEV_INCOMING_USER i MAILDEV_INCOMING_PASS. Aby uzyskać więcej informacji, zobacz MailDev: Użytkowanie. Zaktualizuj plik MailDevResource.cs w projekcie MailDev.Hosting, zastępując jego zawartość następującym kodem 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}"
        );
}

Te aktualizacje dodają właściwość UsernameParameter i PasswordParameter. Te właściwości są używane do przechowywania parametrów dla nazwy użytkownika i hasła MailDev. Właściwość ConnectionStringExpression jest aktualizowana w celu uwzględnienia parametrów nazwy użytkownika i hasła w parametrach połączenia. Następnie zaktualizuj plik MailDevResourceBuilderExtensions.cs w projekcie MailDev.Hosting przy użyciu następującego kodu w języku 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";
}

Powyższy kod aktualizuje metodę rozszerzenia AddMailDev w celu uwzględnienia parametrów userName i password. Metoda WithEnvironment jest aktualizowana w celu uwzględnienia zmiennych środowiskowych UserEnvVarName i PasswordEnvVarName. Te zmienne środowiskowe służą do ustawiania nazwy użytkownika i hasła MailDev.

Aktualizowanie hosta aplikacji

Po zaktualizowaniu zasobu w celu uwzględnienia parametrów nazwy użytkownika i hasła należy zaktualizować hosta aplikacji, aby uwzględnić te parametry. Zaktualizuj plik Program.cs w projekcie MailDevResource.AppHost przy użyciu następującego kodu w języku 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();

Powyższy kod dodaje dwa parametry dla nazwy użytkownika i hasła MailDev. Przypisuje te parametry do zmiennych środowiskowych MAILDEV_INCOMING_USER i MAILDEV_INCOMING_PASS. Metoda AddMailDev ma dwa wywołania łańcuchowe do WithEnvironment, które obejmują te zmienne środowiskowe. Aby uzyskać więcej informacji na temat parametrów, zobacz Parametry zewnętrzne.

Następnie skonfiguruj tajne dane dla tych parametrów. Kliknij prawym przyciskiem myszy projekt MailDevResource.AppHost i wybierz pozycję Manage User Secrets. Dodaj następujące JSON do tajemnic . Plikjson:

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

Ostrzeżenie

Te poświadczenia są przeznaczone tylko do celów demonstracyjnych, MailDev jest przeznaczona do lokalnego rozwoju. Te poświadczenia są fikcyjne i nie powinny być używane w środowisku produkcyjnym.

Zaktualizuj integrację MailKit

Dobrą praktyką dla integracji client jest oczekiwanie, że ciągi połączeń będą zawierać różne pary klucz/wartość, oraz że te pary zostaną przeanalizowane na odpowiednie właściwości. Zaktualizuj plik MailKitClientSettings.cs w projekcie MailKit.Client przy użyciu następującego kodu w języku 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());
            }
        }
    }
}

Poprzednia klasa ustawień zawiera teraz właściwość Credentials typu NetworkCredential. Metoda ParseConnectionString jest aktualizowana w celu analizowania kluczy Username i Password z ciągu połączenia. Jeśli istnieją klucze Username i Password, zostanie utworzona i przypisana NetworkCredential do właściwości Credentials.

Po zaktualizowaniu klasy ustawień, aby zrozumiała i wypełniła poświadczenia, zaktualizuj fabrykę, aby warunkowo używała poświadczeń, jeśli są skonfigurowane. Zaktualizuj plik MailKitClientFactory.cs w projekcie MailKit.Client przy użyciu następującego kodu w języku 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();
    }
}

Kiedy fabryka ustaliła, że poświadczenia zostały skonfigurowane, uwierzytelnia się z użyciem SMTP server po nawiązaniu połączenia, zanim zwróci SmtpClient.

Uruchamianie przykładu

Teraz, gdy zaktualizowałeś zasób, odpowiadające projekty integracji oraz host aplikacji, jesteś gotowy do uruchomienia przykładowej aplikacji. Aby uruchomić przykład ze środowiska IDE, wybierz pozycję F5 lub użyj dotnet run z katalogu głównego rozwiązania, aby uruchomić aplikację — powinien zostać wyświetlony pulpit nawigacyjny .NET.NET Aspire. Przejdź do zasobu kontenera maildev i wyświetl szczegóły. Parametry nazwy użytkownika i hasła powinny być widoczne w szczegółach zasobu w sekcji Zmienne środowiskowe:

.NET Aspire Dashboard: szczegóły zasobu kontenera MailDev.

Podobnie parametry połączenia powinny być widoczne w szczegółach zasobu newsletterservice w sekcji Zmienne środowiskowe:

.NET.NET Aspire Dashboard: szczegóły zasobów usługi biuletynu.

Sprawdź, czy wszystko działa zgodnie z oczekiwaniami.

Streszczenie

W tym artykule pokazano, jak przekazywać dane uwierzytelniające z zasobu niestandardowego do niestandardowej integracji client. Zasób niestandardowy to kontener MailDev, który umożliwia obsługę poświadczeń przychodzących lub wychodzących. Niestandardowa integracja client to client MailKit, która wysyła wiadomości e-mail. Aktualizując zasób, aby uwzględnić parametry username i password, oraz aktualizując integrację, aby analizować i wykorzystywać te parametry, dane uwierzytelniające przepływają z integracji hostingu do integracji client.