Sdílet prostřednictvím


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

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

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 demonstruje, jak sdílet přihlašovací údaje pro ověřování z vlastního prostředku v integračním řešení hostování, k využívajícímu klientovi v rámci vlastní integrace klienta. Vlastní prostředek je kontejner MailDev, který podporuje použití buď příchozích, nebo odchozích přihlašovacích údajů. Vlastní integrace klienta je klient MailKit, 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 klienta, která zpřístupňuje sadu 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.HostAndPort))};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 souboru JSON přidejte následující :

{
  "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 klientů 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, ověří se serverem SMTP po připojení a teprve potom 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 ke kontejnerovému prostředku 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í:

.NET.NET Aspire Řídicí panel: 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 přihlašovací údaje pro ověřování z vlastního prostředku do vlastní klientské integrace. Vlastní prostředek je kontejner MailDev, který podporuje použití buď příchozích, nebo odchozích přihlašovacích údajů. Vlastní integrace klienta je klient MailKit, který odesílá e-maily. Aktualizací prostředku tak, aby zahrnoval parametry username a password, a aktualizací integrace pro správné zpracování a použití těchto parametrů, ověřovací toky předávají přihlašovací údaje z integrace hostování do integrace klienta.