安全的通訊在主機與 client 整合之間
本文是前兩篇文章的延續,示範如何建立 自定義的主機整合,以及 自定義的 client 整合。
.NET .NET Aspire 的主要優點之一在於它能夠簡化資源的設置功能和取用用戶端(或整合應用)。 本文示範如何將驗證憑證從託管整合中的自定義資源分享至自定義 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_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
屬性會更新為在連接字串中包含使用者名稱和密碼參數。 接下來,使用下列 C# 程式代碼更新 MailDev.Hosting
專案中的 MailDevResourceBuilderExtensions.cs 檔案:
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 使用者名稱和密碼。
更新應用程式主機
現在資源已更新為包含使用者名稱和密碼參數,您必須更新應用程式主機以包含這些參數。 使用下列 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 用戶名稱和密碼新增兩個參數。 它會將這些參數指派給 MAILDEV_INCOMING_USER
和 MAILDEV_INCOMING_PASS
環境變數。
AddMailDev
方法包含兩次連鎖調用至 WithEnvironment
,其中包括這些環境變數。 如需參數的詳細資訊,請參閱
接下來,設定與這些參數相關的機密資訊。 以滑鼠右鍵按兩下 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());
}
}
}
}
上述設定類別現在包含 類型為 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 或使用解決方案根目錄中的 dotnet run
來啟動應用程式,您應該會看到 .NET.NET Aspire 儀表板。 流覽至 maildev
容器資源,並檢視詳細數據。 您應該在資源詳細資料中看到使用者名稱和密碼參數,請參閱 環境變數 一節:
同樣地,您應該會在 newsletterservice
資源詳細數據中看到連接字串,請參閱 環境變數 區段:
驗證所有專案是否如預期般運作。
總結
本文示範如何將驗證認證從自定義資源流向自定義 client 整合。 自定義資源是 MailDev 容器,允許傳入或傳出認證。 客製化的 client 整合是 MailKit client,用於傳送電子郵件。 藉由更新資源以包含 username
和 password
參數,以及更新整合以剖析和使用這些參數,驗證會將認證從裝載整合流向 client 整合。