Поделиться через


Безопасное взаимодействие между размещением и интеграцией клиентов

Эта статья является продолжением двух предыдущих статей, демонстрирующих создание интеграций пользовательского размещения и пользовательских интеграций клиентов .

Одним из основных преимуществ .NET.NET Aspire является упрощение конфигурируемости ресурсов и их использования клиентами (или интеграциями). В этой статье объясняется, как предоставить доступ к учетным данным проверки подлинности из пользовательского ресурса в рамках интеграции размещения и передать их потребляющему клиенту в пользовательской интеграции клиента. Пользовательский ресурс — это контейнер MailDev, который позволяет использовать учетные данные как для входа, так и для выхода. Пользовательская интеграция клиента — это клиент MailKit, который отправляет сообщения электронной почты.

Необходимые условия

Так как эта статья продолжается с предыдущего содержимого, необходимо уже создать итоговое решение в качестве отправной точки для этой статьи. Если вы еще этого не сделали, завершите следующие задания:

Полученное решение из этих предыдущих статей содержит следующие проекты:

  • MailDev. Хостинг: Содержит пользовательский тип ресурса для контейнера MailDev.
  • MailDevResource.AppHost: хост приложения , который использует пользовательский ресурс и определяет его как зависимость для службы новостной рассылки.
  • MailDevResource.NewsletterService: проект веб-API ASP.NET Core, который отправляет сообщения электронной почты с помощью контейнера MailDev.
  • MailDevResource.ServiceDefaults: содержит конфигурации служб по умолчанию , предназначенные для общего доступа.
  • MailKit.Client: содержит настраиваемую интеграцию клиента, которая предоставляет SmtpClient MailKit через фабрику.

Обновление ресурса MailDev

Чтобы передавать учетные данные проверки подлинности из ресурса MailDev в интеграцию MailKit, необходимо обновить ресурс MailDev, чтобы включить параметры имени пользователя и пароля.

Контейнер MailDev поддерживает базовую проверку подлинности как для входящих, так и исходящих простых протоколов передачи почты (SMTP). Чтобы настроить учетные данные для входящих, необходимо задать MAILDEV_INCOMING_USER и MAILDEV_INCOMING_PASS переменные среды. Дополнительные сведения см. в разделе MailDev: использование. Обновите файл MailDevResource.cs в проекте MailDev.Hosting, заменив его содержимое следующим кодом 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}"
        );
}

Эти обновления добавляют свойство UsernameParameter и PasswordParameter. Эти свойства используются для хранения параметров для MailDev имени пользователя и пароля. Свойство ConnectionStringExpression обновляется, чтобы включить параметры имени пользователя и пароля в строку подключения. Затем обновите файл MailDevResourceBuilderExtensions.cs в проекте MailDev.Hosting следующим кодом 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";
}

Предыдущий код обновляет метод расширения AddMailDev, чтобы включить параметры userName и password. Метод WithEnvironment обновляется, чтобы включить переменные среды UserEnvVarName и PasswordEnvVarName. Эти переменные среды используются для задания имени пользователя и пароля MailDev.

Обновление узла приложения

Теперь, когда ресурс обновляется, чтобы включить параметры имени пользователя и пароля, необходимо обновить узел приложения, чтобы включить эти параметры. Обновите файл Program.cs в проекте MailDevResource.AppHost следующим кодом 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();

Приведенный выше код добавляет два параметра для имени пользователя и пароля MailDev. Он присваивает эти параметры переменным среды MAILDEV_INCOMING_USER и MAILDEV_INCOMING_PASS. Метод AddMailDev имеет два цепных вызова WithEnvironment, которые включают эти переменные среды. Дополнительные сведения о внешних параметрах см. в .

Затем настройте секреты для этих параметров. Щелкните правой кнопкой мыши проект MailDevResource.AppHost и выберите Manage User Secrets. Добавьте следующий JSON в файл secrets.json:

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

Предупреждение

Эти учетные данные предназначены только для демонстрационных целей и MailDev предназначены для локальной разработки. Эти учетные данные вымышлены и не должны использоваться в рабочей среде.

Обновление интеграции MailKit

Для интеграции с клиентами следует предполагать, что строки подключения будут содержать различные пары "ключ-значение", а также преобразовывать эти пары в соответствующие свойства. Обновите файл MailKitClientSettings.cs в проекте MailKit.Client следующим кодом 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());
            }
        }
    }
}

Предыдущий класс параметров теперь включает свойство Credentials типа NetworkCredential. Метод ParseConnectionString обновляется для анализа Username и Password ключей из строки подключения. Если ключи Username и Password присутствуют, создается NetworkCredential и оно назначается свойству Credentials.

После обновления класса параметров для распознавания и заполнения учетных данных, обновите объект фабрики, чтобы использовать учетные данные при условии, что они настроены. Обновите файл MailKitClientFactory.cs в проекте MailKit.Client следующим кодом 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();
    }
}

Когда фабрика определяет, что учетные данные настроены, она аутентифицируется на SMTP-сервере после подключения перед возвратом SmtpClient.

Запустите пример

Теперь, когда вы обновили ресурс, соответствующие проекты интеграции и хост приложения, вы готовы запустить образец приложения. Чтобы запустить пример из интегрированной среды разработки, выберите F5 или используйте dotnet run из корневого каталога решения, чтобы запустить приложение — вы должны увидеть панель мониторинга .NET.NET Aspire. Перейдите к ресурсу контейнера maildev и просмотрите сведения. Параметры имени пользователя и пароля должны отображаться в сведениях о ресурсе в разделе Переменные среды:

Панель мониторинга .NET Aspire: детали ресурса контейнера MailDev.

Аналогично, вы должны увидеть строку подключения в сведениях о ресурсе newsletterservice, в разделе переменных среды :

панель управления .NET.NET Aspire: информация о ресурсах службы бюллетеней.

Убедитесь, что все работает должным образом.

Сводка

В этой статье показано, как передавать учетные данные проверки подлинности из пользовательского ресурса в пользовательскую интеграцию клиента. Пользовательский ресурс — это контейнер MailDev, который позволяет использовать учетные данные как для входа, так и для выхода. Пользовательская интеграция клиента — это клиент MailKit, который отправляет сообщения электронной почты. Обновив ресурс, чтобы включить параметры username и password, а также обновив интеграцию для синтаксического анализа и использования этих параметров, аутентификация передает учетные данные из интеграции размещения в интеграцию клиента.