Dela via


Säker kommunikation mellan värd- och client-integreringar

Den här artikeln är en fortsättning på två tidigare artiklar som visar skapandet av anpassade värdintegreringar och anpassade client integreringar.

En av de främsta fördelarna med .NET.NET Aspire är hur det förenklar konfigurationen för resurser och konsumerande klienter (eller integrationer). Den här artikeln visar hur du delar autentiseringsuppgifter från en anpassad resurs i en värdintegration till den konsumerande client i en anpassad client-integration. Den anpassade resursen är en MailDev container som tillåter inkommande eller utgående autentiseringsuppgifter. Den anpassade client-integreringen är en MailKit-client som skickar e-postmeddelanden.

Förutsättningar

Eftersom den här artikeln fortsätter från tidigare innehåll bör du redan ha skapat den resulterande lösningen som utgångspunkt för den här artikeln. Om du inte redan har gjort det slutför du följande artiklar:

Den resulterande lösningen från dessa tidigare artiklar innehåller följande projekt:

  • MailDev. Värd för: Innehåller den anpassade resurstypen för MailDev-containern.
  • MailDevResource.AppHost: applikationsvärd som använder den anpassade resursen och definierar den som ett beroende för en nyhetsbrevstjänst.
  • MailDevResource.NewsletterService: Ett ASP.NET Core webb-API-projekt som skickar e-postmeddelanden med hjälp av containern MailDev.
  • MailDevResource.ServiceDefaults: Innehåller de standardtjänstkonfigurationer som avsedda för delning.
  • MailKit.Client: Innehåller den anpassade client integrering som exponerar MailKit-SmtpClient via en fabrik.

Uppdatera MailDev resursen

Om du vill skicka autentiseringsuppgifter från MailDev resursen till MailKit-integreringen måste du uppdatera den MailDev resursen så att den innehåller parametrarna användarnamn och lösenord.

Containern MailDev stöder grundläggande autentisering för både inkommande och utgående protokoll för enkel e-postöverföring (SMTP). För att konfigurera autentiseringsuppgifterna för inkommande måste du ange miljövariablerna MAILDEV_INCOMING_USER och MAILDEV_INCOMING_PASS. Mer information finns i MailDev: Användning. Uppdatera MailDevResource.cs-filen i MailDev.Hosting-projektet genom att ersätta dess innehåll med följande C#-kod:

// 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}"
        );
}

Dessa uppdateringar lägger till en egenskap för UsernameParameter och PasswordParameter. Dessa egenskaper används för att lagra parametrarna för MailDev användarnamn och lösenord. Egenskapen ConnectionStringExpression uppdateras så att den innehåller parametrarna användarnamn och lösenord i anslutningssträngen. Uppdatera sedan MailDevResourceBuilderExtensions.cs-filen i MailDev.Hosting-projektet med följande C#-kod:

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";
}

Föregående kod uppdaterar AddMailDev-tilläggsmetoden så att den innehåller parametrarna userName och password. Metoden WithEnvironment uppdateras med miljövariablerna UserEnvVarName och PasswordEnvVarName. Dessa miljövariabler används för att ange MailDev användarnamn och lösenord.

Uppdatera värden för appen

Nu när resursen har uppdaterats så att den innehåller parametrarna för användarnamn och lösenord måste du uppdatera appvärden så att de innehåller dessa parametrar. Uppdatera Program.cs-filen i MailDevResource.AppHost-projektet med följande C#-kod:

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

Föregående kod lägger till två parametrar för MailDev användarnamn och lösenord. Den tilldelar dessa parametrar till miljövariablerna MAILDEV_INCOMING_USER och MAILDEV_INCOMING_PASS. Metoden AddMailDev har två länkade anrop till WithEnvironment som innehåller dessa miljövariabler. Mer information om parametrar finns i Externa parametrar.

Konfigurera sedan hemligheterna för dessa parametrar. Högerklicka på projektet MailDevResource.AppHost och välj Manage User Secrets. Lägg till följande JSON i hemligheter.json fil:

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

Varning

Dessa autentiseringsuppgifter är endast i demonstrationssyfte och MailDev är avsedd för lokal utveckling. Dessa autentiseringsuppgifter är fiktiva och bör inte användas i en produktionsmiljö.

Uppdatera MailKit-integreringen

Det är bra praxis att förvänta sig att client-integreringar hanterar anslutningssträngar som innehåller olika nyckel/värde-par och analyserar dessa par till lämpliga egenskaper. Uppdatera MailKitClientSettings.cs-filen i MailKit.Client-projektet med följande C#-kod:

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

Den föregående inställningsklassen innehåller nu en Credentials egenskap av typen NetworkCredential. Metoden ParseConnectionString uppdateras för att parsa Username- och Password nycklar från anslutningssträngen. Om nycklarna Username och Password finns skapas en NetworkCredential och tilldelas egenskapen Credentials.

När inställningsklassen har uppdaterats för att hantera och fylla i autentiseringsuppgifterna, uppdaterar du fabriken så att den använder autentiseringsuppgifterna om de är konfigurerade. Uppdatera MailKitClientFactory.cs-filen i MailKit.Client-projektet med följande C#-kod:

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

När fabriken fastställer att autentiseringsuppgifterna har konfigurerats, använder den SMTP-server för autentisering efter att ha anslutit innan den återgår till SmtpClient.

Kör exemplet

Nu när du har uppdaterat resursen, motsvarande integrationsprojekt och appvärden är du redo att köra exempelappen. Om du vill köra exemplet från din IDE väljer du F5 eller använder dotnet run från rotkatalogen i lösningen för att starta programmet; du bör se .NET.NET Aspire instrumentpanelen. Gå till resursen maildev container och visa detaljerna. Du bör se parametrarna för användarnamn och lösenord i resursinformationen under avsnittet Miljövariabler:

.NET Aspire Instrumentpanel: MailDev information om containerresurser.

På samma sätt bör du se anslutningssträngen i newsletterservice resursinformation under avsnittet Miljövariabler:

.NET.NET Aspire Instrumentpanel: Resursinformation för nyhetsbrevstjänsten.

Kontrollera att allt fungerar som förväntat.

Sammanfattning

Den här artikeln visar hur du flödar autentiseringsuppgifter från en anpassad resurs till en anpassad client integrering. Den anpassade resursen är en MailDev container som tillåter inkommande eller utgående autentiseringsuppgifter. Den anpassade client-integreringen är en MailKit-client som skickar e-postmeddelanden. Genom att uppdatera resursen så att den innehåller parametrarna username och password och uppdatera integreringen för att parsa och använda dessa parametrar flödar autentiseringsuppgifter från värdintegrering till client integrering.