Delen via


Beveiligde communicatie tussen hosting en clientintegraties

Dit artikel is een vervolg van twee vorige artikelen waarin het maken van aangepaste hostingintegraties en aangepaste clientintegratieswordt gedemonstreerd.

Een van de belangrijkste voordelen van Aspire is hoe het de configureerbaarheid van resources en het gemak voor klanten (of integraties) vereenvoudigt. In dit artikel wordt gedemonstreert hoe u verificatiereferenties van een aangepaste resource in een hostingintegratie kunt delen met de verbruikende client in een aangepaste clientintegratie. De aangepaste resource is een MailDev container die binnenkomende en uitgaande referenties toestaat. De aangepaste clientintegratie is een MailKit-client waarmee e-mailberichten worden verzonden.

Voorwaarden

Aangezien dit artikel verdergaat met eerdere inhoud, moet u de resulterende oplossing al hebben gemaakt als uitgangspunt voor dit artikel. Als u dat nog niet hebt gedaan, voltooit u de volgende artikelen:

De resulterende oplossing uit deze vorige artikelen bevat de volgende projecten:

  • MailDev. Hosting: bevat het aangepaste resourcetype voor de MailDev container.
  • MailDevResource.AppHost: De AppHost die gebruikmaakt van de aangepaste resource en definieert deze als een afhankelijkheid voor een nieuwsbriefservice.
  • MailDevResource.NewsletterService: een ASP.NET Core Web-API-project waarmee e-mailberichten worden verzonden met behulp van de MailDev container.
  • MailDevResource.ServiceDefaults: bevat de standaardserviceconfiguraties bedoeld voor delen.
  • MailKit.Client: bevat de aangepaste clientintegratie waarmee de MailKit-SmtpClient via een fabriek beschikbaar wordt gemaakt.

De MailDev-resource bijwerken

Als u verificatiereferenties van de MailDev-resource naar de MailKit-integratie wilt laten stromen, moet u de MailDev resource bijwerken om de gebruikersnaam- en wachtwoordparameters op te nemen.

De MailDev-container ondersteunt basisverificatie voor zowel binnenkomende als uitgaande SIMPLE Mail Transfer Protocol (SMTP). Als u de referenties voor binnenkomende e-mail wilt configureren, moet u de MAILDEV_INCOMING_USER en MAILDEV_INCOMING_PASS omgevingsvariabelen instellen. Zie MailDevvoor meer informatie: Gebruik. Werk het MailDevResource.cs-bestand in het MailDev.Hosting project bij door de inhoud ervan te vervangen door de volgende C#-code:

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

Met deze updates wordt een eigenschap UsernameParameter en PasswordParameter toegevoegd. Deze eigenschappen worden gebruikt voor het opslaan van de parameters voor de gebruikersnaam en het wachtwoord van de MailDev. De eigenschap ConnectionStringExpression wordt bijgewerkt om de gebruikersnaam- en wachtwoordparameters in de verbindingsreeks op te nemen. Werk vervolgens het bestand MailDevResourceBuilderExtensions.cs in het MailDev.Hosting project bij met de volgende C#-code:

using Aspire.Hosting.ApplicationModel;

// Put extensions in the Aspire.Hosting namespace to ease discovery as referencing
// the 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 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";
}

Met de voorgaande code wordt de AddMailDev-extensiemethode bijgewerkt om de parameters userName en password op te nemen. De methode WithEnvironment wordt bijgewerkt om de UserEnvVarName en PasswordEnvVarName omgevingsvariabelen op te nemen. Deze omgevingsvariabelen worden gebruikt om de MailDev gebruikersnaam en wachtwoord in te stellen.

De AppHost bijwerken

Nu de resource is bijgewerkt om de parameters voor gebruikersnaam en wachtwoord op te nemen, moet u de AppHost bijwerken om deze parameters op te nemen. Werk het AppHost.cs-bestand in het MailDevResource.AppHost project bij met de volgende C#-code:

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

Met de voorgaande code worden twee parameters toegevoegd voor de MailDev gebruikersnaam en wachtwoord. Deze parameters worden toegewezen aan de omgevingsvariabelen MAILDEV_INCOMING_USER en MAILDEV_INCOMING_PASS. De methode AddMailDev heeft twee gekoppelde aanroepen naar WithEnvironment die deze omgevingsvariabelen bevat. Zie Externe parametersvoor meer informatie over parameters.

Configureer vervolgens de geheimen voor deze parameters. Klik met de rechtermuisknop op het MailDevResource.AppHost project en selecteer Manage User Secrets. Voeg de volgende JSON toe aan het secrets.json-bestand:

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

Waarschuwing

Deze inloggegevens zijn alleen bedoeld voor demonstratiedoeleinden en MailDev is bedoeld voor lokale ontwikkeling. Deze referenties zijn fictief en mogen niet worden gebruikt in een productieomgeving.

De MailKit-integratie bijwerken

Het is een goede gewoonte voor clientintegraties om te verwachten dat verbindingsreeksen verschillende sleutel-/waardeparen bevatten en om deze paren te parseren in de juiste eigenschappen. Werk het MailKitClientSettings.cs-bestand in het MailKit.Client project bij met de volgende C#-code:

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

De voorgaande instellingenklasse bevat nu een Credentials eigenschap van het type NetworkCredential. De methode ParseConnectionString wordt bijgewerkt om de Username en Password sleutels van de verbindingsreeks te parseren. Als de Username en Password sleutels aanwezig zijn, wordt er een NetworkCredential gemaakt en toegewezen aan de eigenschap Credentials.

Wanneer de instellingenklasse is bijgewerkt om de referenties te begrijpen en in te vullen, werkt u de 'factory' bij om de referenties voorwaardelijk te gebruiken indien ze geconfigureerd zijn. Werk het MailKitClientFactory.cs-bestand in het MailKit.Client project bij met de volgende C#-code:

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

Wanneer de fabriek bepaalt dat referenties zijn geconfigureerd, authenticeert het met de SMTP-server nadat verbinding is gemaakt voordat de SmtpClientwordt geretourneerd.

Het voorbeeld uitvoeren

Nu u de resource, bijbehorende integratieprojecten en de AppHost hebt bijgewerkt, kunt u de voorbeeld-app uitvoeren. Als u het voorbeeld wilt uitvoeren vanuit uw IDE, selecteert u F5 of gebruikt u dotnet run in de hoofdmap van de oplossing om de toepassing te starten; u zou het Aspire dashboard moeten zien. Navigeer naar de maildev containerresource en bekijk de details. U ziet de parameters voor gebruikersnaam en wachtwoord in de resourcedetails, onder de sectie Omgevingsvariabelen:

Aspire Dashboard: MailDev container resourcegegevens.

Op dezelfde manier ziet u de verbindingsreeks in de newsletterservice resourcedetails, onder de sectie Omgevingsvariabelen:

Aspire Dashboard: Resourcegegevens van nieuwsbriefdienst.

Controleer of alles werkt zoals verwacht.

Samenvatting

In dit artikel wordt gedemonstreerd hoe u verificatiereferenties van een aangepaste resource naar een aangepaste clientintegratie kunt doorstromen. De aangepaste resource is een MailDev container die binnenkomende en uitgaande referenties toestaat. De aangepaste clientintegratie is een MailKit-client waarmee e-mailberichten worden verzonden. Door de resource bij te werken om de username- en password parameters op te nemen en de integratie bij te werken om deze parameters te parseren en te gebruiken, stromen verificatiereferenties van de hostingintegratie naar de clientintegratie.