使用托管标识与 Azure SQL 的 EF Core 连接

Posted

技术标签:

【中文标题】使用托管标识与 Azure SQL 的 EF Core 连接【英文标题】:EF Core Connection to Azure SQL with Managed Identity 【发布时间】: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;
    

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

我的问题:

    这是正确的方法还是会出现性能问题? 我需要担心令牌过期吗?我现在没有缓存令牌。 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。 【参考方案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 我将在下面描述。这两个问题是:

    您自己负责创建连接对象。但是你不处理它。在您的实施过程中,处置会很棘手,这就是您可能跳过它的原因。 您的代码被阻塞,因为您在等待访问令牌时使用.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

【讨论】:

以上是关于使用托管标识与 Azure SQL 的 EF Core 连接的主要内容,如果未能解决你的问题,请参考以下文章

具有托管标识(用户分配)的 Azure SQL 无法针对 AAD 使用

如果用户是用户分配托管标识,则确定 Azure SQL Server 中的用户名

在 Azure 中整合托管标识与身份验证/授权标识

为啥我的 Azure 应用服务无法使用托管标识连接到 Azure 存储帐户?

如何使用服务托管标识在 Azure 中使用 Terraform 预配资源

Azure 使用 REST api 和托管标识创建 blob 容器 - 403 错误