我正在尝试开发 .net API 作为副项目,并且我想添加日志记录功能。通过在线搜索,我发现了“Serilog”包,现在我正在尝试实现它。但问题是它需要一个到数据库的连接字符串。据我所知,我必须对其进行硬编码或从“appsettings.json”文件中调用它。但我想要一种更安全的方法。
我想出的解决方案是编写一个“Secret Manager”接口,以便稍后我可以实现“Azure Vault”等安全解决方案,但现在它只是解析 json 文件来获取机密。这是我的
ISecretManager
界面
public interface ISecretManager
{
public string? GetPasswordManagerAuthDatabaseConnectionString();
public string? GetLogDatabaseConnectionString();
public byte[]? GetPepperSymmetricKey();
public byte[]? GetHmacPrivateKey();
public JwtInfo? GetJwtInfo();
public byte[]? GetJwtKey();
public MailInfo? GetMailInfo();
public string GetRsaPrivateKey();
}
我在为数据库创建
dbContext
时遇到了类似的问题,但我能够在 dbContext
文件中解决此问题。
这是我的
dbContext
文件。
public class AuthDbContext : DbContext
{
private readonly ISecretManager _secretManager;
public AuthDbContext(DbContextOptions<AuthDbContext> options, ISecretManager secretManager) : base(options)
{
_secretManager = secretManager;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured == false)
{
var connectionString = _secretManager.GetPasswordManagerAuthDatabaseConnectionString();
if (connectionString == null)
{
throw new SecretNotAvailableException();
}
optionsBuilder.UseNpgsql(connectionString);
}
base.OnConfiguring(optionsBuilder);
}
public DbSet<User> Users { get; set; }
public DbSet<ActivationCode> ActivationCodes { get; set; }
public DbSet<UserPassword> UserPasswords { get; set; }
}
我将我的秘密管理器注入到
dbContext
文件中并在那里处理配置。但是从在线教程来看 Serilog 是在构建器阶段创建的,我不能在这个阶段使用注入。
这是
Program.cs
文件的前半部分。
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
builder.Services.AddTransient<ISecretManager, FileBasedSecretManager>();
builder.Services.AddDbContext<AuthDbContext>();
builder.Services.AddTransient<IJwtService, JwtService>();
builder.Services.AddTransient<IUserService, UserService>();
builder.Services.AddTransient<IUserPasswordService, UserPasswordService>();
builder.Services.AddTransient<IUserRepository, UserRepository>();
builder.Services.AddTransient<IUserPasswordRepository, UserPasswordRepository>();
builder.Services.AddTransient<IActivationCodeRepository, ActivationCodeRepository>();
builder.Services.AddTransient<IActivationCodeService, ActivationCodeService>();
builder.Services.AddTransient<ICommunicationChannel, Mail>();
builder.Services.AddTransient<AuthenticationFacade>();
builder.Services.AddHttpContextAccessor();
builder.Host.UseSerilog(/* I need to pass connection string here */);
var app = builder.Build();
处理这个问题的最佳方法是什么?我可以直接创建一个秘密管理器对象,但这会违反“依赖倒置”原则,并且硬编码不是一种安全的方法。
我在这篇文章中解释了如何处理连接字符串而不是字符串,而是可以单独针对.Net配置系统覆盖的单独信息。
就我个人而言,我将实际密码放在来自 Kubernetes 机密的环境变量中,但您喜欢的任何其他 .Net 配置源都应该可以正常工作。
Dapper
和 Entity Framework
一起使用。选项示例:
{
"SqlServer": {
"Server": "my.sqlserver.enterprise.example.com",
"Port": 1433,
"Database": "my-db",
"IntegratedSecurity": false,
"Username": "sys_username",
"Password": "undisclosed",
"Mars": true,
"ConnectionTimeout": "00:00:10"
}
}
POCO 类使用
IOptions<>
(选项模式)将选项注入到任何服务:
public class SqlServerConnectionOptions
{
#region Properties
public string Server { get; set; }
public int Port { get; set; }
public string Database { get; set; }
public bool IntegratedSecurity { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool Mars { get; set; }
public TimeSpan? ConnectionTimeout { get; set; }
public int? MaxPoolSize { get; set; }
#endregion
}
使用选项模式的连接工厂的 MS SQL Server 实现:
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
public class SqlServerConnectionFactory : IDbConnectionFactory
{
#region Fields
private Lazy<string> _connString;
#endregion
#region Constructors
public SqlServerConnectionFactory(
IOptions<SqlServerConnectionOptions> options
)
{
_connString = new Lazy<string>(() =>
{
var opts = options.Value;
// We use a special string builder.
SqlConnectionStringBuilder sb = new SqlConnectionStringBuilder();
sb.DataSource = $"{opts.Server},{opts.Port}";
sb.InitialCatalog = opts.Database;
sb.IntegratedSecurity = opts.IntegratedSecurity;
if (!opts.IntegratedSecurity)
{
sb.UserID = opts.Username;
sb.Password = opts.Password;
}
sb.MultipleActiveResultSets = opts.Mars;
if (opts.ConnectionTimeout.HasValue)
{
sb.ConnectTimeout = (int)opts.ConnectionTimeout.Value.TotalSeconds;
}
if (opts.MaxPoolSize.HasValue)
{
sb.MaxPoolSize = opts.MaxPoolSize.Value;
}
// Return the resulting connection string.
return sb.ToString();
});
}
#endregion
#region IDbConnectionFactory
public async Task<IDbConnection> CreateAsync()
{
SqlConnection conn = new(_connString.Value);
await conn.OpenAsync();
return conn;
}
#endregion
}