Sdílet prostřednictvím


Zabezpečená komunikace mezi hostováním a integracemi client

Tento článek je pokračováním dvou předchozích článků demonstrujících vytváření vlastních hostitelských integrací a vlastních client integrací.

Jednou z hlavních výhod .NET.NET Aspire je to, jak zjednodušuje konfiguraci prostředků a využívání klientů (nebo integrací). Tento článek ukazuje, jak sdílet ověřovací přihlašovací údaje z vlastního zdroje v hostovací integraci do spotřebovávajícího client ve vlastní integraci client. Vlastní prostředek je kontejner MailDev, který podporuje použití buď příchozích, nebo odchozích přihlašovacích údajů. Vlastní integrace client je MailKit client, který odesílá e-maily.

Požadavky

Vzhledem k tomu, že tento článek pokračuje od předchozího obsahu, měli byste už vytvořit výsledné řešení jako výchozí bod pro tento článek. Pokud jste to ještě neudělali, vyplňte následující články:

Výsledné řešení z těchto předchozích článků obsahuje následující projekty:

  • MailDev. Hostování: Obsahuje vlastní typ prostředku pro kontejner MailDev.
  • MailDevResource.AppHost: Hostitel aplikace , který používá vlastní prostředek a definuje ho jako závislost pro službu newsletteru.
  • MailDevResource.NewsletterService: Projekt webového rozhraní API ASP.NET Core, který odesílá e-maily pomocí kontejneru MailDev.
  • MailDevResource.ServiceDefaults: Obsahuje výchozí konfigurace služby, které jsou určeny ke sdílení.
  • MailKit.Client: Obsahuje vlastní integraci client, která zpřístupňuje MailKit SmtpClient prostřednictvím továrny.

Aktualizace prostředku MailDev

Pokud chcete tok přihlašovacích údajů pro ověřování z prostředku MailDev do integrace MailKitu, musíte aktualizovat prostředek MailDev tak, aby zahrnoval parametry uživatelského jména a hesla.

Kontejner MailDev podporuje základní ověřování pro příchozí i odchozí protokol SMTP (Simple Mail Transfer Protocol). Pokud chcete nakonfigurovat přihlašovací údaje pro příchozí, musíte nastavit MAILDEV_INCOMING_USER a MAILDEV_INCOMING_PASS proměnných prostředí. Pro více informací si přečtěte téma MailDev: Použití. Aktualizujte soubor MailDevResource.cs v projektu MailDev.Hosting nahrazením jeho obsahu následujícím kódem jazyka 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}"
        );
}

Tyto aktualizace přidávají vlastnost UsernameParameter a PasswordParameter. Tyto vlastnosti slouží k uložení parametrů pro MailDev uživatelské jméno a heslo. Vlastnost ConnectionStringExpression se aktualizuje tak, aby zahrnovala parametry uživatelského jména a hesla do připojovacího řetězce. Dále aktualizujte soubor MailDevResourceBuilderExtensions.cs v projektu MailDev.Hosting následujícím kódem jazyka 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";
}

Předchozí kód aktualizuje metodu rozšíření AddMailDev tak, aby zahrnovala parametry userName a password. Metoda WithEnvironment se aktualizuje tak, aby zahrnovala proměnné prostředí UserEnvVarName a PasswordEnvVarName. Tyto proměnné prostředí se používají k nastavení MailDev uživatelského jména a hesla.

Aktualizace hostitele aplikace

Teď, když je prostředek aktualizovaný tak, aby zahrnoval parametry uživatelského jména a hesla, musíte aktualizovat hostitele aplikace tak, aby tyto parametry zahrnoval. Aktualizujte soubor Program.cs v projektu MailDevResource.AppHost následujícím kódem jazyka 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();

Předchozí kód přidá dva parametry pro MailDev uživatelské jméno a heslo. Tyto parametry přiřadí MAILDEV_INCOMING_USER a MAILDEV_INCOMING_PASS proměnným prostředí. Metoda AddMailDev má dvě zřetězená volání WithEnvironment, která zahrnují tyto proměnné prostředí. Další informace o parametrech naleznete v tématu Externí parametry.

Dále nakonfigurujte tajné kódy pro tyto parametry. Klikněte pravým tlačítkem na projekt MailDevResource.AppHost a vyberte Manage User Secrets. Do tajných kódů přidejte následující JSON.json soubor:

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

Varování

Tyto přihlašovací údaje slouží jenom pro demonstrační účely a MailDev je určená pro místní vývoj. Tyto přihlašovací údaje jsou fiktivní a neměly by se používat v produkčním prostředí.

Aktualizujte integraci MailKit

Je vhodné, aby integrace client očekávaly, že připojovací řetězce budou obsahovat různé páry klíč/hodnota a parsovat tyto páry do příslušných vlastností. Aktualizujte soubor MailKitClientSettings.cs v projektu MailKit.Client následujícím kódem jazyka 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());
            }
        }
    }
}

Předchozí třída nastavení nyní obsahuje vlastnost Credentials typu NetworkCredential. Metoda ParseConnectionString je aktualizována pro parsování klíčů Username a Password z připojovacího řetězce. Pokud jsou k dispozici klíče Username a Password, vytvoří se NetworkCredential a přiřadí se k Credentials vlastnosti.

Jakmile je třída nastavení aktualizována tak, aby rozuměla přihlašovacím údajům a používala je, aktualizujte továrnu, aby podmíněně používala tyto přihlašovací údaje, pokud jsou nakonfigurované. Aktualizujte soubor MailKitClientFactory.cs v projektu MailKit.Client následujícím kódem jazyka 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();
    }
}

Když továrna zjistí, že jsou přihlašovací údaje nakonfigurovány, provádí ověření přes protokol SMTP server po připojení, než vrátí SmtpClient.

Spusťte ukázku

Teď, když jste aktualizovali prostředek, odpovídající projekty integrace a hostitele aplikace, jste připraveni spustit ukázkovou aplikaci. Pokud chcete spustit ukázku z integrovaného vývojového prostředí, vyberte F5 nebo spusťte aplikaci pomocí dotnet run z kořenového adresáře řešení. Měli byste vidět řídicí panel .NET.NET Aspire. Přejděte k prostředku kontejneru maildev a prohlédněte si podrobnosti. Parametry uživatelského jména a hesla byste měli vidět v podrobnostech o prostředku v části Proměnné prostředí:

Řídicí panel .NET Aspire: MailDev podrobnosti o prostředcích kontejneru.

Stejně tak byste měli v podrobnostech o prostředku newsletterservice vidět připojovací řetězec v části Proměnné prostředí:

řídicí panel .NET.NET Aspire: Podrobnosti o prostředku služby bulletinu

Ověřte, že všechno funguje podle očekávání.

Shrnutí

Tento článek ukazuje, jak přenášet ověřovací přihlašovací údaje z vlastní prostředku do vlastní integrace client. Vlastní prostředek je kontejner MailDev, který umožňuje přicházející nebo odcházející autentizační údaje. Vlastní integrace client je balíček MailKit client, který odesílá e-maily. Aktualizací prostředku tak, aby zahrnoval parametry username a password, a aktualizací integrace pro analýzu a používání těchto parametrů, proudí přihlašovací údaje z hostující integrace do integrace client.