основное сделано
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.SharePoint;
|
||||
|
||||
public interface ICertificateProvider
|
||||
{
|
||||
Task<X509Certificate2> GetCertificateAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сертификат для входа в SharePoint — из таблицы settings (option_name задаётся
|
||||
/// в SharePointOptions.CertificateSettingName, по умолчанию 'AzurePfx').
|
||||
///
|
||||
/// Синглтон с кешем: разбор .pfx стоит заметно дороже запроса к базе, а сам
|
||||
/// сертификат меняется раз в год. Блокировка — чтобы параллельные запросы не
|
||||
/// разбирали его одновременно при холодном старте.
|
||||
///
|
||||
/// Собственная таблица, а не общая с api-tnsu-portal, — осознанный выбор: базы
|
||||
/// у сервисов разные и связывать их не хотим. Цена — копию сертификата придётся
|
||||
/// обновлять в двух местах.
|
||||
/// </summary>
|
||||
public sealed class DbCertificateProvider(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<SharePointOptions> options) : ICertificateProvider
|
||||
{
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private X509Certificate2? _cached;
|
||||
|
||||
public async Task<X509Certificate2> GetCertificateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cached is not null) return _cached;
|
||||
|
||||
await _lock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (_cached is not null) return _cached;
|
||||
|
||||
var settingName = options.Value.CertificateSettingName;
|
||||
|
||||
// Своя область: провайдер — синглтон, а DbContext зарегистрирован
|
||||
// как scoped, напрямую внедрять его сюда нельзя.
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApprovalRouteDbContext>();
|
||||
|
||||
var pfx = await db.Settings.AsNoTracking()
|
||||
.Where(x => x.OptionName == settingName)
|
||||
.Select(x => x.FileData)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (pfx is null || pfx.Length == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"Сертификат не найден: в таблице settings нет записи «{settingName}» с непустым file_data.");
|
||||
|
||||
_cached = X509CertificateLoader.LoadPkcs12(
|
||||
pfx,
|
||||
options.Value.CertificatePassword,
|
||||
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet);
|
||||
|
||||
return _cached;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user