次の方法で共有


ホスティングと client の統合間のセキュリティで保護された通信

この記事では、カスタム ホスティング統合 とカスタム 統合の作成を示す 2 つの前の記事の続きです。

.NET .NET Aspire の主な利点の 1 つは、リソースとクライアント(または統合)の設定の容易さを簡素化することです。 この記事では、ホスティング統合のカスタム リソースから、カスタム client 統合で使用する client に認証資格情報を共有する方法について説明します。 カスタム リソースは、受信資格情報または送信資格情報を許可する MailDev コンテナーです。 カスタム client 統合は、メールを送信する MailKit client です。

前提 条件

この記事は以前のコンテンツから続いているため、この記事の開始点として結果のソリューションを既に作成しておく必要があります。 まだ行っていない場合は、以下の記事に取り組んでください。

これらの前の記事の結果のソリューションには、次のプロジェクトが含まれています。

  • MailDev.ホスティング: MailDev コンテナーのカスタムリソースタイプが含まれます。
  • MailDevResource.AppHost: カスタム リソースを使用し、それをニュースレター サービスの依存関係として定義する アプリ ホスト
  • MailDevResource.NewsletterService: MailDev コンテナーを使用して電子メールを送信する ASP.NET Core Web API プロジェクト。
  • MailDevResource.ServiceDefaults: 共有を目的とした既定のサービス構成 が含まれています。
  • MailKit を します。Client: ファクトリーを通じて MailKit SmtpClient を公開するカスタム client インテグレーションを含みます。

MailDev リソースを更新する

MailDev リソースから MailKit 統合に認証資格情報をフローするには、ユーザー名とパスワードのパラメーターを含むように MailDev リソースを更新する必要があります。

MailDev コンテナーは、受信と送信の両方の簡易メール転送プロトコル (SMTP) の基本認証をサポートしています。 受信用の資格情報を構成するには、MAILDEV_INCOMING_USERMAILDEV_INCOMING_PASS 環境変数を設定する必要があります。 詳細については、「MailDev: 使用法」を参照してください。 MailDev.Hosting プロジェクトの MailDevResource.cs ファイルを、その内容を次の 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}"
        );
}

これらの更新プログラムは、UsernameParameterPasswordParameter プロパティを追加します。 これらのプロパティは、MailDev ユーザー名とパスワードのパラメーターを格納するために使用されます。 ConnectionStringExpression プロパティが更新され、接続文字列にユーザー名とパスワードのパラメーターが含まれます。 次に、MailDev.Hosting プロジェクトの MailDevResourceBuilderExtensions.cs ファイルを次の 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";
}

上記のコードは、userName パラメーターと password パラメーターを含むように、AddMailDev 拡張メソッドを更新します。 WithEnvironment メソッドは、UserEnvVarNamePasswordEnvVarName 環境変数を含むように更新されます。 これらの環境変数は、MailDev ユーザー名とパスワードを設定するために使用されます。

アプリ ホストを更新する

ユーザー名とパスワードのパラメーターを含むようにリソースが更新されたので、これらのパラメーターを含むようにアプリ ホストを更新する必要があります。 次の C# コードを使用して、MailDevResource.AppHost プロジェクトの Program.cs ファイルを更新します。

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 ユーザー名とパスワードの 2 つのパラメーターが追加されています。 これらのパラメーターは、MAILDEV_INCOMING_USERMAILDEV_INCOMING_PASS 環境変数に割り当てられます。 AddMailDev メソッドには、これらの環境変数を含む WithEnvironment への 2 つのチェーン呼び出しがあります。 パラメーターの詳細については、「外部パラメーターの」を参照してください。

次に、これらのパラメーターのシークレットを構成します。 MailDevResource.AppHost プロジェクトを右クリックし、[Manage User Secrets] を選択します。 シークレットに次の JSON を追加します。json ファイル:

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

警告

これらの資格情報はデモンストレーションのみを目的としており、MailDev はローカル開発を目的としています。 これらの資格情報は架空のものであり、運用環境では使用しないでください。

MailKit の統合を更新する

client 統合では、接続文字列にさまざまなキーと値のペアが含まれていることを想定し、これらのペアを適切なプロパティに解析することをお勧めします。 次の C# コードを使用して、MailKit.Client プロジェクトの MailKitClientSettings.cs ファイルを更新します。

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());
            }
        }
    }
}

上記の settings クラスに、NetworkCredential型の Credentials プロパティが含まれるようになりました。 ParseConnectionString メソッドが更新され、接続文字列から Username キーと Password キーが解析されます。 Username キーと Password キーが存在する場合は、NetworkCredential が作成され、Credentials プロパティに割り当てられます。

資格情報を理解し入力するように設定クラスを更新した後、資格情報が設定されている場合には条件に応じて使用するようにファクトリを更新します。 次の C# コードを使用して、MailKit.Client プロジェクトの MailKitClientFactory.cs ファイルを更新します。

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 server で認証を行い、SmtpClientを返します。

サンプルを実行する

リソース、対応する統合プロジェクト、アプリ ホストを更新したので、サンプル アプリを実行する準備ができました。 IDE からサンプルを実行するには、F5 選択するか、ソリューションのルート ディレクトリから を使用してアプリケーションを起動します。 ダッシュボードが表示されます。 maildev コンテナー リソースに移動し、詳細を表示します。 リソースの詳細の 環境変数 セクションに、ユーザー名とパスワードのパラメーターが表示されます。

.NET Aspire ダッシュボード: MailDev コンテナリソースの詳細。

同様に、環境変数の セクションの newsletterservice リソースの詳細に接続文字列が表示されます。

.NET.NET Aspire ダッシュボード: ニュースレター サービスリソースの詳細。

すべてが期待どおりに動作していることを確認します。

概要

この記事では、カスタム リソースからカスタム client 統合に認証資格情報をフローする方法について説明しました。 カスタム リソースは、受信資格情報または送信資格情報を許可する MailDev コンテナーです。 カスタム client 統合は、メールを送信する MailKit client です。 username パラメーターと password パラメーターを含むようにリソースを更新し、これらのパラメーターを解析して使用するように統合を更新することで、認証はホスティング統合から client 統合に資格情報をフローします。