Безопасное взаимодействие между размещением и интеграцией клиентов
Эта статья является продолжением двух предыдущих статей, демонстрирующих создание интеграций пользовательского размещения и пользовательских интеграций клиентов .
Одним из основных преимуществ .NET.NET Aspire является упрощение конфигурируемости ресурсов и их использования клиентами (или интеграциями). В этой статье объясняется, как предоставить доступ к учетным данным проверки подлинности из пользовательского ресурса в рамках интеграции размещения и передать их потребляющему клиенту в пользовательской интеграции клиента. Пользовательский ресурс — это контейнер MailDev, который позволяет использовать учетные данные как для входа, так и для выхода. Пользовательская интеграция клиента — это клиент MailKit, который отправляет сообщения электронной почты.
Необходимые условия
Так как эта статья продолжается с предыдущего содержимого, необходимо уже создать итоговое решение в качестве отправной точки для этой статьи. Если вы еще этого не сделали, завершите следующие задания:
- Создание пользовательских интеграций .NET.NET Aspire размещения
- Создайте пользовательские интеграции для клиентов .NET.NET Aspire
Полученное решение из этих предыдущих статей содержит следующие проекты:
- 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
и просмотрите сведения. Параметры имени пользователя и пароля должны отображаться в сведениях о ресурсе в разделе Переменные среды:
Аналогично, вы должны увидеть строку подключения в сведениях о ресурсе newsletterservice
, в разделе переменных среды :
Убедитесь, что все работает должным образом.
Сводка
В этой статье показано, как передавать учетные данные проверки подлинности из пользовательского ресурса в пользовательскую интеграцию клиента. Пользовательский ресурс — это контейнер MailDev, который позволяет использовать учетные данные как для входа, так и для выхода. Пользовательская интеграция клиента — это клиент MailKit, который отправляет сообщения электронной почты. Обновив ресурс, чтобы включить параметры username
и password
, а также обновив интеграцию для синтаксического анализа и использования этих параметров, аутентификация передает учетные данные из интеграции размещения в интеграцию клиента.
.NET Aspire