У меня есть около дюжины приложений ASP.NET Core, которые должны реализовать одну и ту же базовую логику аутентификации. В общих чертах мне нужно...
Вместо того, чтобы повторять свои слова дюжину раз, я хотел бы использовать общую библиотеку аутентификации, чтобы содержать общую логику и позволить приложениям настраивать несколько уникальных для них параметров, таких как имя файла cookie. Вот что у меня есть до сих пор и, кажется, работает.
// MyAuthenticationExtensions.cs
public class MyCookieAuthenticationOptions
{
// cookie only options
public string CookieName { get; set; }
// oauth options
public string ClientId { get; set; }
public string ClientSecret { get; set; }
// misc options
public bool IsDevelopment { get; set; } = false;
}
public static class MyAuthenticationExtensions
{
private const string _KeyVaultKey = "KeyVaultName";
private const string _PortalUrlKey = "Global:PortalUrl";
private const string _DataProtectionKeyLocationKey = "Global:DataProtection:KeyLocation";
private const string _DataProtectionKeyIdentifierKey = "Global:DataProtection:KeyIdentifier";
public static IServiceCollection AddMyCookieAuthentication(
this IServiceCollection services,
Action<MyCookieAuthenticationOptions> configureOptions,
IConfiguration configuration,
DefaultAzureCredential credential)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configureOptions);
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(credential);
var theOptions = new MyCookieAuthenticationOptions();
configureOptions.Invoke(theOptions);
if (!theOptions.IsDevelopment)
{
var keyVault = configuration[_KeyVaultKey];
var dpKeyIdentifier = configuration[_DataProtectionKeyIdentifierKey];
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(
configuration[_DataProtectionKeyLocationKey]))
.ProtectKeysWithAzureKeyVault(
new Uri($"https://{keyVault}.vault.usgovcloudapi.net/keys/{dpKeyIdentifier}"),
credential);
}
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.Cookie.Name = theOptions.CookieName;
if (theOptions.IsDevelopment)
{
options.Cookie.Path = "/";
}
options.Events.OnRedirectToLogin = (ctx) =>
{
// Override ApiControllers to return a 401 and not redirect
// https://github.com/dotnet/aspnetcore/issues/9039
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.Headers.Location = ctx.RedirectUri;
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = (ctx) =>
{
// Override ApiControllers to return a 401 and not redirect
// https://github.com/dotnet/aspnetcore/issues/9039
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.Headers.Location = ctx.RedirectUri;
ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
return Task.CompletedTask;
};
options.Events.OnSigningOut = (ctx) =>
{
// Revoke the user's refresh token on signout
return Task.Run(async () =>
{
var clientId = theOptions.ClientId;
var token = await ctx.HttpContext.GetTokenAsync("refresh_token");
var revokeTokenParams = new FormUrlEncodedContent(
new Dictionary<string, string> {
{ "client_id", clientId },
{ "auth_token", token },
{ "f", "json" }
});
var baseUri = configuration[_PortalUrlKey];
var revokeTokenUri = new Uri($"{baseUri}/sharing/rest/oauth2/revokeToken");
var clientFactory = ctx
.HttpContext
.RequestServices
.GetRequiredService<IHttpClientFactory>();
var client = clientFactory.CreateClient();
var result = await client.PostAsync(revokeTokenUri, revokeTokenParams);
});
};
}).AddArcGIS(options =>
{
options.ClientId = theOptions.ClientId ?? string.Empty;
options.ClientSecret = theOptions.ClientSecret ?? string.Empty;
var baseUri = configuration[_PortalUrlKey];
options.AuthorizationEndpoint = $"{baseUri}/sharing/rest/oauth2/authorize";
options.TokenEndpoint = $"{baseUri}/sharing/rest/oauth2/token";
options.UserInformationEndpoint = $"{baseUri}/sharing/rest/community/self";
options.SaveTokens = true;
// Override default claims as email isn't stored in an "email" property
options.ClaimActions.Clear();
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "username");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "username");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "fullName");
options.ClaimActions.MapJsonKey("LastLogin", "lastLogin");
});
return services;
}
}
Его использование.
// Program.cs
builder.Services.AddMyCookieAuthentication(options =>
{
options.CookieName = "my_cookie_name";
options.ClientId = builder.Configuration["OAuth:ClientId:MyApp"];
options.ClientSecret = builder.Configuration["OAuth:ClientSecret:MyApp"];
options.IsDevelopment = builder.Environment.IsDevelopment();
}, builder.Configuration, azCredential);
Это основано на примере Объединение коллекции служб из собственных документов Microsoft.
Я также хотел бы внедрить экземпляр ILogger, чтобы обеспечить базовую регистрацию в этом общем компоненте, но на момент вызова builder.Services.AddMyCookieAuthentication
у меня нет экземпляра ILogger. У меня также нет доступа к app.Services.GetRequiredService
из метода расширения.
Я больше привык позволять платформе вставлять объекты по мере необходимости, указывая зависимости в конструкторе. Например, вот некоторая общая логика для одинаковой настройки пересылаемых заголовков во всех приложениях.
public sealed class ConfigureMyForwardedHeaders : IConfigureOptions<ForwardedHeadersOptions>
{
private readonly IConfiguration _config;
private readonly ILogger _logger;
private const string _KnownProxiesKey = "DotNet:ForwardedHeaderOptions:KnownProxies";
private const string _KnownNetworksKey = "DotNet:ForwardedHeaderOptions:KnownNetworks";
public ConfigureMyForwardedHeaders(IConfiguration config, ILogger<ConfigureMyForwardedHeaders> logger)
{
_config = config;
_logger = logger;
}
public void Configure(ForwardedHeadersOptions options)
{
_logger.LogInformation("Configuring My Forwarded Headers");
options.ForwardedHeaders = ForwardedHeaders.All;
var proxyIP = _config[_KnownProxiesKey];
options.KnownProxies.Add(IPAddress.Parse(proxyIP));
var network = _config[_KnownNetworksKey].Split("/");
var networkIP = network[0];
var networkMask = int.Parse(network[1]);
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse(networkIP), networkMask));
}
}
Как я могу аккуратно внедрить ILogger в свой метод расширения? Нужно ли мне полностью отказаться от использования метода расширения?
Решение, которое я реализовал на основе комментариев, заключалось в добавлении ILogger в качестве параметра в метод расширения AddMyCookieAuthentication
, как показано ниже. Мне пришлось вручную создать экземпляр ILogger<Program>
из NLogFactory, вместо того, чтобы полагаться на фреймворк, который создаст экземпляр за меня.
// Program.cs
var logFactory = new NLog.Extensions.Logging.NLogLoggerFactory();
var logger = logFactory.CreateLogger<Program>();
builder.Services.AddMyCookieAuthentication(options =>
{
options.CookieName = "my_cookie_name";
options.ClientId = builder.Configuration["OAuth:ClientId:MyApp"];
options.ClientSecret = builder.Configuration["OAuth:ClientSecret:MyApp"];
options.IsDevelopment = builder.Environment.IsDevelopment();
}, builder.Configuration, logger, azCredential);
В соответствии с вашим сценарием, вместо того, чтобы полагаться на внедрение зависимостей в методе расширения, вы можете принять экземпляр ILogger<T> в качестве параметра. Это позволяет вызывающей стороне обеспечить необходимые функции ведения журнала. Потому что нет необходимости полностью отказываться от подхода метода расширения. Этот метод предлагает несколько преимуществ.