EF Core 使用托管身份连接到 Azure SQL

EF Core Connection to Azure SQL with Managed Identity

我正在使用 EF Core 连接到部署到 Azure 应用服务的 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;
    }
}

这很好用,我可以从数据库中获取数据。但我不确定这样做是否正确。

我的问题:

  1. 这是正确的方法吗?还是会出现性能问题?
  2. 我需要担心令牌过期吗?我现在没有缓存令牌。
  3. EF Core 是否有更好的方法来处理这个问题?

Is this a right way to do it or will it have issues with performance?

这是正确的方法。为每个新的 DbContext 调用 OnConfiguring,因此假设您没有任何长期存在的 DbContext 实例,这是正确的模式。

Do I need to worry about token expiration? I am not caching the token as of now.

AzureServiceTokenProvider 负责缓存。

Does EF Core has any better way to handle this?

记录了 .NET Core 中 SqlClient 的 AAD 身份验证方法 here

对于使用 .NET Framework for Managed Identity 的开发人员,以下代码可能有助于获取实体连接:

app.config:

<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#文件

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;
}

对于那些仍然遇到同样问题的人,我已经使用 DbInterceptor 解决了这个问题,这样我就可以在不阻塞应用程序的情况下异步获取令牌。我在 EF Core 存储库上打开了一个问题,但我已经用解决方案关闭了:

https://github.com/dotnet/efcore/issues/21043

希望对您有所帮助。

虽然这种方法通常是正确的,因为除了必须编写自定义代码来设置连接的 AccessToken 之外别无他法,但您的实现中可能存在一些问题通过使用 DbConnectionInterceptor 来避免,我将在下面描述。这两个问题是:

  1. 您负责自己创建连接对象。但是你不处理它。处置在您的实施中会很棘手,这就是您可能跳过它的原因。
  2. 您的代码正在阻塞,因为您在等待访问令牌时使用 .Result 进行阻塞。

更好的替代方法是使用 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 ValueTask<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;
}

已投票。

这是对 Romar 出色回答的附加回答。这对我们非常有用,使我们能够消除 ConnectionString 中的用户凭据。然而,这给我们带来了需要使用秘密检索访问令牌的问题,这是我们也不希望包含在 appsettings 文件中的敏感信息。因此,我们将一个问题换成了另一个问题。

网络上还有其他帖子处理此问题。因此,我发布了一个综合而全面的答案,从 appsettings 文件中完全删除了敏感数据。注意:您需要将机密迁移到 KeyVault 中。在这种情况下,我们将其命名为AzureSqlSecret。这是为了检索数据库用户的凭据。

调用AzureAuthenticationInterceptor的实体class构造函数如下:

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
    }
}

Microsoft.Data.SqlClient 到来后 - Entity framework 核心连接器的新版本 sql - 现在非常简单:

Install-Package Microsoft.Data.SqlClient -Version 4.0.1

将连接字符串添加到 Dotnet 核心应用程序,如下所示:

"Server=tcp:<server-name>.database.windows.net;Authentication=Active Directory Default; Database=<database-name>;"

然后使用它通过 Azure SQL 连接使用托管身份连接到 Azure SQL,如下所示:

            using (SqlConnection _connection = new SqlConnection(sqlConnectionString))
            {
                _connection.Open();

                // do some stuff with the sqlconnection to read or write record in SQL.

                _connection.Close();

                return true;
            }

Refer here for detailed article