Comunicação segura entre hospedagem e integrações client
Este artigo é a continuação de dois artigos anteriores que demonstram a criação de integrações de hospedagem personalizadas e de integrações personalizadas client.
Um dos principais benefícios do .NET.NET Aspire é como ele simplifica a configurabilidade de recursos e o consumo de clientes (ou integrações). Este artigo demonstra como partilhar credenciais de autenticação de um recurso personalizado numa integração de alojamento, para o client de consumo numa integração personalizada de client. O recurso personalizado é um contêiner MailDev que permite credenciais de entrada ou saída. A integração personalizada client é um MailKit client que envia e-mails.
Pré-requisitos
Como este artigo continua a partir do conteúdo anterior, você já deve ter criado a solução resultante como ponto de partida para este artigo. Se ainda não o fez, complete os seguintes artigos:
- Crie integrações personalizadas de hospedagem .NET.NET Aspire
- Crie integrações .NET Aspireclient personalizadas
A solução resultante destes artigos anteriores contém os seguintes projetos:
- MailDev. Hosting: Contém o tipo de recurso personalizado para o contêiner MailDev.
- MailDevResource.AppHost: O host de aplicativo que usa o recurso personalizado e o define como uma dependência para um serviço de boletim informativo.
- MailDevResource.NewsletterService: Um projeto de API Web ASP.NET Core que envia e-mails usando o contêiner MailDev.
- MailDevResource.ServiceDefaults: Contém as configurações de serviço padrão destinadas ao compartilhamento.
-
MailKit.Client: Contém a integração de client personalizada que expõe o MailKit
SmtpClient
através de uma fábrica.
Atualizar o recurso MailDev
Para transferir as credenciais de autenticação do recurso MailDev para a integração do MailKit, deve atualizar o recurso MailDev para incluir os parâmetros de nome de utilizador e palavra-passe.
O contêiner MailDev suporta autenticação básica para SMTP (Simple Mail Transfer Protocol) de entrada e saída. Para configurar as credenciais de entrada, você precisa definir as variáveis de ambiente MAILDEV_INCOMING_USER
e MAILDEV_INCOMING_PASS
. Para obter mais informações, consulte MailDev: Utilização. Atualize o arquivo MailDevResource.cs no projeto MailDev.Hosting
, substituindo seu conteúdo pelo seguinte código 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}"
);
}
Essas atualizações adicionam as propriedades UsernameParameter
e PasswordParameter
. Essas propriedades são usadas para armazenar os parâmetros para o MailDev nome de usuário e senha. A propriedade ConnectionStringExpression
é atualizada para incluir os parâmetros username e password na cadeia de conexão. Em seguida, atualize o arquivo MailDevResourceBuilderExtensions.cs no projeto MailDev.Hosting
com o seguinte código 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";
}
O código anterior atualiza o método de extensão AddMailDev
para incluir os parâmetros userName
e password
. O método WithEnvironment
é atualizado para incluir as variáveis de ambiente UserEnvVarName
e PasswordEnvVarName
. Essas variáveis de ambiente são usadas para definir o MailDev nome de usuário e senha.
Atualizar o host do aplicativo
Agora que o recurso foi atualizado para incluir os parâmetros de nome de usuário e senha, você precisa atualizar o host do aplicativo para incluir esses parâmetros. Atualize o arquivo Program.cs no projeto MailDevResource.AppHost
com o seguinte código 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();
O código anterior adiciona dois parâmetros para o MailDev nome de usuário e senha. Ele atribui esses parâmetros às variáveis de ambiente MAILDEV_INCOMING_USER
e MAILDEV_INCOMING_PASS
. O método AddMailDev
tem duas chamadas encadeadas para WithEnvironment
que inclui essas variáveis de ambiente. Para obter mais informações sobre parâmetros, consulte Parâmetros externos.
Em seguida, configure os segredos para esses parâmetros. Clique com o botão direito do mouse no projeto MailDevResource.AppHost
e selecione Manage User Secrets
. Adicione os seguintes JSON aos segredos.json arquivo:
{
"Parameters:maildev-username": "@admin",
"Parameters:maildev-password": "t3st1ng"
}
Advertência
Estas credenciais destinam-se apenas a fins de demonstração e MailDev se destinam ao desenvolvimento local. Essas credenciais são fictícias e não devem ser usadas em um ambiente de produção.
Atualizar a integração do MailKit
É uma boa prática, nas integrações client, esperar que as strings de conexão possuam vários pares chave/valor e que esses pares sejam extraídos e atribuídos às propriedades apropriadas. Atualize o arquivo MailKitClientSettings.cs no projeto MailKit.Client
com o seguinte código 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());
}
}
}
}
A classe de configurações anterior, agora inclui uma propriedade Credentials
do tipo NetworkCredential
. O método ParseConnectionString
é atualizado para analisar as teclas Username
e Password
da cadeia de conexão. Se as chaves Username
e Password
estiverem presentes, uma NetworkCredential
será criada e atribuída à propriedade Credentials
.
Com a classe settings atualizada para entender e preencher as credenciais, atualize a fábrica para usar condicionalmente as credenciais, se elas estiverem configuradas. Atualize o arquivo MailKitClientFactory.cs no projeto MailKit.Client
com o seguinte código 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();
}
}
Quando a fábrica determina que as credenciais foram configuradas, ela autentica-se com o SMTP server depois de se conectar antes de devolver o SmtpClient
.
Executar o exemplo
Agora que você atualizou o recurso, os projetos de integração correspondentes e o host do aplicativo, está pronto para executar o aplicativo de exemplo. Para executar o exemplo a partir do IDE, selecione F5 ou use dotnet run
no diretório raiz da solução para iniciar o aplicativo — você deve ver o painel .NET.NET Aspire. Navegue até o recurso de contêiner maildev
e visualize os detalhes. Você deve ver os parâmetros de nome de usuário e senha nos detalhes do recurso, na seção
Da mesma forma, deves ver a string de conexão nos detalhes do recurso newsletterservice
, na secção Variáveis de Ambiente.
Valide se tudo está funcionando conforme o esperado.
Resumo
Este artigo demonstrou como transferir credenciais de autenticação de um recurso personalizado para uma integração personalizada client. O recurso personalizado é um contêiner MailDev que permite credenciais de entrada ou saída. A integração personalizada client é um MailKit client que envia e-mails. Ao atualizar o recurso para incluir os parâmetros username
e password
, e ao adaptar a integração para analisar e usar esses parâmetros, as credenciais de autenticação são transferidas da integração de hospedagem para a integração client.