我正在使用EF Core连接到已部署到Azure App Services的Azure SQL数据库。我正在使用访问令牌(通过托管身份获取)来连接到Azure SQL数据库。
这是我的做法:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
//code ignored for simplicity
services.AddDbContext<MyCustomDBContext>();
services.AddTransient<IDBAuthTokenService, AzureSqlAuthTokenService>();
}
MyCustomDBContext.cs
public partial class MyCustomDBContext : DbContext
{
public IConfiguration Configuration { get; }
public IDBAuthTokenService authTokenService { get; set; }
public CortexContext(IConfiguration configuration, IDBAuthTokenService tokenService, DbContextOptions<MyCustomDBContext> options)
: base(options)
{
Configuration = configuration;
authTokenService = tokenService;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
SqlConnection connection = new SqlConnection();
connection.ConnectionString = Configuration.GetConnectionString("defaultConnection");
connection.AccessToken = authTokenService.GetToken().Result;
optionsBuilder.UseSqlServer(connection);
}
}
AzureSqlAuthTokenService.cs
public class AzureSqlAuthTokenService : IDBAuthTokenService
{
public async Task<string> GetToken()
{
AzureServiceTokenProvider provider = new AzureServiceTokenProvider();
var token = await provider.GetAccessTokenAsync("https://database.windows.net/");
return token;
}
}
这很好,我可以从数据库中获取数据。但是我不确定这是否是正确的方法。
我的问题:
答案 0 :(得分:8)
从某种意义上说,这种方法通常是正确的,除了必须编写设置连接AccessToken
的自定义代码外,没有其他方法,但是在实现中存在一些可以避免的问题,使用DbConnectionInterceptor
,如下所述。这两个问题是:
.Result
进行阻塞,因此您的代码正在阻塞,这浪费了宝贵的CPU时间。一个更好的选择是使用EF Core支持的拦截器。您将从这样的DbContext
开始:
public class MyCustomDbContextFactory : IMyCustomDbContextFactory
{
private readonly string _connectionString;
private readonly AzureAuthenticationInterceptor _azureAuthenticationInterceptor;
public MyCustomDbContextFactory(DbContextFactoryOptions options, AzureAuthenticationInterceptor azureAuthenticationInterceptor)
{
_connectionString = options.ConnectionString;
_azureAuthenticationInterceptor = azureAuthenticationInterceptor;
}
public MyCustomDbContext Create()
{
var optionsBuilder = new DbContextOptionsBuilder<MyCustomDbContext>();
optionsBuilder
.UseSqlServer(_connectionString)
.AddInterceptors(_azureAuthenticationInterceptor);
return new MyCustomDbContext(optionsBuilder.Options);
}
}
这是拦截器实现:
public class AzureAuthenticationInterceptor : DbConnectionInterceptor
{
private const string AzureDatabaseResourceIdentifier = "https://database.windows.net";
private readonly AzureServiceTokenProvider _azureServiceTokenProvider;
public AzureAuthenticationInterceptor(AzureServiceTokenProvider azureServiceTokenProvider) : base()
{
_azureServiceTokenProvider = azureServiceTokenProvider;
}
public override async Task<InterceptionResult> ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default)
{
if (connection is SqlConnection sqlConnection)
{
sqlConnection.AccessToken = await GetAccessToken();
}
return result;
}
public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result)
{
if (connection is SqlConnection sqlConnection)
{
sqlConnection.AccessToken = GetAccessToken().Result;
}
return result;
}
private Task<string> GetAccessToken() => _azureServiceTokenProvider.GetAccessTokenAsync(AzureDatabaseResourceIdentifier);
}
这是配置服务的方式:
services.AddSingleton(new DbContextFactoryOptions(connection_string));
services.AddSingleton(new AzureAuthenticationInterceptor(new AzureServiceTokenProvider()));
最后,这是在存储库中实例化DbContext
对象的方法:
public async Task<IEnumerable<MyCustomEntity>> GetAll()
{
using var context = _notificationsDbContextFactory.Create(); // Injected in ctor
var dbos = await context.MyCustomEntity.ToListAsync();
return ... // something;
}
答案 1 :(得分:6)
这是正确的方法还是会出现性能问题?
那是正确的方法。每个新的DbContext都会调用OnConfiguring,因此,假设您没有任何长期存在的DbContext实例,那么这是正确的模式。
我是否需要担心令牌到期?到目前为止,我还没有缓存令牌。
AzureServiceTokenProvider
负责缓存。
EF Core是否有更好的方法来处理此问题?
设置SqlConnection.AccessToken是当前在SqlClient for .NET Core中使用AAD Auth的唯一方法。
答案 2 :(得分:1)
对于仍然遇到相同问题的人,我已经使用DbInterceptor
解决了这个问题,因此我可以异步获取令牌而不会阻塞应用程序。我已经在EF Core仓库上打开了一个问题,但是我已经解决了:
https://github.com/dotnet/efcore/issues/21043
希望对您有帮助。
答案 3 :(得分:0)
对于使用.net Framework作为托管身份的开发人员,以下代码可能有助于获得实体连接。
app config file
<add key="ResourceId" value="https://database.windows.net/" />
<add key="Con" value="data source=tcp:sampledbserver.database.windows.net,1433;initial catalog=sampledb;MultipleActiveResultSets=True;Connect Timeout=30;" />
c# file
using System;
using System.Configuration;
using System.Data.Entity.Core.EntityClient;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.SqlClient;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Azure.Services.AppAuthentication;
public static EntityConnection GetEntityConnectionString()
{
MetadataWorkspace workspace = new MetadataWorkspace(
new string[] { "res://*/" },
new Assembly[] { Assembly.GetExecutingAssembly() });
SqlConnection sqlConnection = new SqlConnection(Con);
var result = (new
AzureServiceTokenProvider()).GetAccessTokenAsync(ResourceId).Result;
sqlConnection.AccessToken = result ?? throw new
InvalidOperationException("Failed to obtain the access token");
EntityConnection entityConnection = new EntityConnection(workspace,
sqlConnection);
return entityConnection;
}
答案 4 :(得分:0)
已投票。
这是对 Romar 出色回答的附加回答。这对我们非常有用,并允许我们消除 ConnectionString 中的用户凭据。但是,这给我们留下了需要使用机密检索访问令牌的问题,这是我们也不希望包含在 appsettings 文件中的敏感信息。因此,我们以一个问题换了另一个问题。
网络上还有其他帖子讨论了这个问题。因此,我发布了一个综合且全面的答案,从 appsettings 文件中完全删除了敏感数据。注意:您需要将机密迁移到 KeyVault 中。在本例中,我们将其命名为 AzureSqlSecret
。这是为了检索数据库用户的凭据。
调用AzureAuthenticationInterceptor
的Entities类构造函数如下:
public ProjectNameEntities() :
base(new DbContextOptionsBuilder<ProjectNameEntities>()
.UseSqlServer(ConfigurationManager.ConnectionStrings["ProjectNameEntities"].ConnectionString)
.AddInterceptors(new AzureAuthenticationInterceptor())
.Options)
{ }
AzureAuthenticationInterceptor:
#region NameSpaces
using Azure.Core;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Configuration;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
#endregion
namespace <ProjectName>.DataAccess.Helpers
{
public class AzureAuthenticationInterceptor : DbConnectionInterceptor
{
#region Constructor
public AzureAuthenticationInterceptor()
{
SecretClientOptions objSecretClientOptions;
string strAzureKeyVaultResourceIdentifier;
string strAzureKeyVault;
string strAzureKeyVaultUri;
strAzureKeyVaultResourceIdentifier = ConfigurationManager.AppSettings["Azure:ResourceIdentifiers:KeyVault"];
strAzureKeyVault = ConfigurationManager.AppSettings["Azure:KeyVaults:TaxPaymentSystem"];
strAzureKeyVaultUri = strAzureKeyVaultResourceIdentifier.Replace("{0}", strAzureKeyVault);
// Set the options on the SecretClient. These are default values that are recommended by Microsoft.
objSecretClientOptions = new SecretClientOptions()
{
Retry =
{
Delay= TimeSpan.FromSeconds(2),
MaxDelay = TimeSpan.FromSeconds(16),
MaxRetries = 5,
Mode = RetryMode.Exponential
}
};
this.SecretClient = new SecretClient(
vaultUri: new Uri(strAzureKeyVaultUri),
credential: new DefaultAzureCredential(),
objSecretClientOptions
);
this.KeyVaultSecret = this.SecretClient.GetSecret("AzureSqlSecret");
this.strKeyVaultSecret = this.KeyVaultSecret.Value;
this.strAzureResourceIdentifierAuthentication = ConfigurationManager.AppSettings["Azure:ResourceIdentifiers:Authentication"];
this.strAzureResourceIdentifierDatabase = ConfigurationManager.AppSettings["Azure:ResourceIdentifiers:DataBase"];
this.strClientId = ConfigurationManager.AppSettings["Azure:DatabaseUsername:ClientId"];
this.strTenantId = ConfigurationManager.AppSettings["Azure:TenantId"];
}
#endregion
#region Methods
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection objDbConnection,
ConnectionEventData objEventData,
InterceptionResult objReturn,
CancellationToken objCancellationToken = default)
{
_ILogger.Debug("Reached the Async Interceptor method");
if (objDbConnection is SqlConnection objSqlConnection)
{
objSqlConnection.AccessToken = GetAccessToken();
}
return objReturn;
}
public override InterceptionResult ConnectionOpening(
DbConnection objDbConnection,
ConnectionEventData objConnectionEventData,
InterceptionResult objReturn)
{
_ILogger.Debug("Reached the non-Async Interceptor method");
if (objDbConnection is SqlConnection objSqlConnection)
{
objSqlConnection.AccessToken = GetAccessToken();
}
return objReturn;
}
private string GetAccessToken()
{
AuthenticationContext objAuthenticationContext;
AuthenticationResult objAuthenticationResult;
ClientCredential objClientCredential;
objAuthenticationContext = new AuthenticationContext(string.Format("{0}/{1}"
, this.strAzureResourceIdentifierAuthentication
, this.strTenantId));
objClientCredential = new ClientCredential(this.strClientId, this.strKeyVaultSecret);
objAuthenticationResult = objAuthenticationContext.AcquireTokenAsync(this.strAzureResourceIdentifierDatabase, objClientCredential).Result;
return objAuthenticationResult.AccessToken;
}
#endregion
#region Properties
readonly <ProjectName>.Common.Logging.ILogger _ILogger = <ProjectName>.Common.Logging.LogWrapper.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private SecretClient SecretClient;
private KeyVaultSecret KeyVaultSecret;
private string strAzureResourceIdentifierDatabase;
private string strAzureResourceIdentifierAuthentication;
private string strKeyVaultSecret;
private string strClientId;
private string strTenantId;
#endregion
}
}