【问题标题】:EF Core Connection to Azure SQL with Managed Identity使用托管标识与 Azure SQL 的 EF Core 连接
【发布时间】:2019-01-14 18:32:22
【问题描述】:

我正在使用 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 有没有更好的方法来处理这个问题?

【问题讨论】:

  • 你能给我一个编辑过的连接字符串吗,我不确定我是否使用了正确的
  • 我正在使用server=tcp:my-server.database.windows.net,1433;Initial Catalog=my-database;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;,但我仍然收到匿名登录失败错误
  • 这是我的连接字符串Data Source=tcp:dbserver.database.windows.net,1433;Initial Catalog=dbname;,类型是SQLAzure。检查您的 appservice 帐户是否已添加到 Azure SQLServer。

标签: c# entity-framework-core azure-active-directory azure-sql-database ef-core-2.2


【解决方案1】:

这是一种正确的方法吗?还是会出现性能问题?

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

我需要担心令牌过期吗?我现在没有缓存令牌。

AzureServiceTokenProvider 负责缓存。

EF Core 有没有更好的方法来处理这个问题?

设置 SqlConnection.AccessToken 是目前在 .NET Core 的 SqlClient 中使用 AAD Auth 的唯一方法。

【讨论】:

  • 嗨,您能否指定 IDBAuthTokenService 使用了哪个 Nuget 包或命名空间。谢谢。
  • 默认选择可能是nuget.org/packages/Microsoft.Azure.Services.AppAuthentication,但获取令牌有多种方式(包括REST)。
  • @PacodelaCruz 是的,请参阅docs.microsoft.com/en-us/azure/key-vault/…
  • @DavidBrowne-Microsoft IDBAuthTokenService 不能再在Microsoft.Azure.Services.AppAuthentication 中找到(如@buzzripper 所述)。我无法在其他任何地方找到它,因为对这个界面的唯一引用就是这个问题!
  • @DavidBrowne-Microsoft 这个解决方案调用.Result 会阻塞线程(即使你采用 REST 方式,你也会遇到同样的问题)。我知道它在 ctor 上,所以没有 async 来电。有没有更好的方法来设置 async/await 时尚?
【解决方案2】:

虽然该方法通常是正确的,因为除了必须编写自定义代码来设置连接的 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;
}

【讨论】:

  • 1) DbContext 将负责关闭 SqlConnection。参见 docs.microsoft.com/en-us/dotnet/api/… 2) Sync 调用仅在缓存中没有令牌可用的情况下阻塞线程,并且阻塞线程不会花费任何 CPU 时间。
  • 这里有一篇关于 DbConnectionInterceptor 方法和 Azure Identity + EF 的写得很好的文章:devblogs.microsoft.com/azure-sdk/… 它做了更多检查以查看是否需要令牌。
【解决方案3】:

对于那些仍然遇到同样问题的人,我已经通过使用DbInterceptor 解决了这个问题,因此我可以异步获取令牌而不会阻塞应用程序。我在 EF Core repo 上打开了一个问题,但我已经关闭了解决方案:

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

希望对你有帮助。

【讨论】:

    【解决方案4】:

    对于使用 .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;
    }
    

    【讨论】:

      【解决方案5】:

      赞成。

      这是对 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
          }
      }
      

      【讨论】:

        【解决方案6】:

        在 Microsoft.Data.SqlClient 到来之后 - 新版本的实体框架核心连接器到 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

        【讨论】:

          猜你喜欢
          • 2021-11-12
          • 1970-01-01
          • 2021-08-15
          • 2020-01-08
          • 2022-12-19
          • 2019-03-15
          • 1970-01-01
          • 2022-11-11
          • 1970-01-01
          相关资源
          最近更新 更多