【问题标题】:Azure MSI with AdlsClient: Access token expired带有 AdlsClient 的 Azure MSI:访问令牌已过期
【发布时间】:2019-01-22 18:21:44
【问题描述】:

我正在使用 Azure 托管服务标识 (MSI) 创建静态(单例)AdlsClient。

然后,我使用 Functions 应用中的 AdlsClient 写入 Data Lake 存储。

该应用程序可以正常运行大约一天,但随后停止运行,并且我看到了此错误。

The access token in the 'Authorization' header is expired.”

Operation: CREATE failed with HttpStatus:Unauthorized Error

显然,MSI 令牌每天都会在没有警告的情况下过期。

很遗憾,MSI 令牌提供程序没有随令牌返回到期日期,因此我无法检查令牌是否仍然有效。

处理这个问题的正确方法是什么?任何帮助表示赞赏。

这是我的代码。

public static class AzureDataLakeUploaderClient
{
    private static Lazy<AdlsClient> lazyClient = new Lazy<AdlsClient>(InitializeADLSClientAsync);

    public static AdlsClient AzureDataLakeClient => lazyClient.Value;

    private static AdlsClient InitializeADLSClientAsync()
    {

        var azureServiceTokenProvider = new AzureServiceTokenProvider();
        string accessToken = azureServiceTokenProvider.GetAccessTokenAsync("https://datalake.azure.net/").Result;
        var client = AdlsClient.CreateClient(GetAzureDataLakeConnectionString(), "Bearer " + accessToken);
        return client;
    }
}

谢谢!

【问题讨论】:

  • OP 提到他正在使用 Azure 函数,这意味着它每次都会被新鲜触发。我们还在运行一个计时器触发的 Azure 函数,该函数持续时间不到 30 秒。我们为每个我们拥有的 ADLS 的函数的生命周期维护一个惰性实例。但是,鉴于令牌应该至少持续 5 分钟,并且我们的函数每次调用仅持续 30 秒,我们不知道为什么令牌会过期。

标签: singleton azure-data-lake azure-managed-identity


【解决方案1】:

GetAccessTokenAsync 返回的访问令牌保证在接下来的 5 分钟内不会过期。默认情况下,Azure AD 访问令牌会在一小时后过期 [1]。

因此,如果您使用同一个令牌(默认过期时间)超过一个小时,您将收到“过期令牌”错误消息。每次需要使用 AdlsClient 时,请使用从 GetAccessTokenAsync 获取的令牌初始化 AdlsClient。 GetAccessTokenAsync 将访问令牌缓存在内存中,如果在到期后 5 分钟内会自动获取新令牌。

惰性对象总是返回与 [2] 初始化的对象相同的对象。因此,AdlsClient 继续使用旧令牌。

参考文献

[1]https://docs.microsoft.com/en-us/azure/active-directory/active-directory-configurable-token-lifetimes#token-types

[2]https://docs.microsoft.com/en-us/dotnet/framework/performance/lazy-initialization#basic-lazy-initialization

【讨论】:

  • 谢谢,@varun-msft 那么,如果我想要一个静态 ADLS 客户端,我就无法将 MSI 与 ADLS 一起使用吗?使用 Client-Id/Secret 授权方式会更好吗?
  • 我将在内部跟进,看看 AzureServiceTokenProvider 如何更好地与 AdlsClient 集成,以便它使用委托来获取令牌。为了安全起见,我建议在客户端 ID/密码上使用 MSI,并在密码过期时避免停机。
  • @Varun-MSFT 这方面有进展吗?使用 ADLS 作为 AAS 的来源似乎在非手动身份验证方面停滞不前。
【解决方案2】:

如果其他人遇到此问题,我可以通过以下方式使其工作。

我们从 Varun 的回答中得知“GetAccessTokenAsync 将访问令牌缓存在内存中,如果在到期后 5 分钟内会自动获取新令牌”

因此,我们可以检查当前访问令牌是否与旧访问令牌不同。只有在令牌到期后 5 分钟内才会出现这种情况,在这种情况下,我们将创建一个新的静态客户端。在所有其他情况下,我们只返回现有客户端。

这样的……

    private static AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();

    private static string accessToken = GetAccessToken();

    private static AdlsClient azureDataLakeClient = null;

    public static AdlsClient GetAzureDataLakeClient()
    {
        var newAccessToken = GetAccessToken();
        if (azureDataLakeClient == null || accessToken != newAccessToken)
        {
            // Create new AdlsClient with the new token
            CreateDataLakeClient(newAccessToken);
        }

        return azureDataLakeClient;
    }

    private static string GetAccessToken()
    {
        return azureServiceTokenProvider.GetAccessTokenAsync("https://datalake.azure.net/").Result;
    }

【讨论】:

    【解决方案3】:

    最近的更新出现在下面的链接中,用于自动刷新存储帐户的令牌: https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-msi

    我已经修改了上面的代码,并成功地使用 Azure Data Lake Store Gen1 对其进行了测试,以自动刷新 MSI 令牌。

    要实现 ADLS Gen1 的代码,我需要两个库:

    <PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.2.0-preview3" />
    <PackageReference Include="Microsoft.Azure.Storage.Common" Version="10.0.3" />
    

    然后我使用这段代码创建了一个带有不断刷新令牌的 AdlsClient 实例:

    var miAuthentication = new AzureManagedIdentityAuthentication("https://datalake.azure.net/");
    var tokenCredential = miAuthentication.GetAccessToken();
    ServiceClientCredentials serviceClientCredential = new TokenCredentials(tokenCredential.Token);
    var dataLakeClient = AdlsClient.CreateClient(clientAccountPath, serviceClientCredential);
    

    下面是我从文章中修改的类,用于一般刷新令牌。现在可以通过在实例化 AzureManagedIdentityAuthentication 时提供相关资源地址来自动刷新 ADLS Gen1("https://datalake.azure.net/") 和存储帐户("https://storage.azure.com/") 的 MSI 令牌。确保使用链接中的代码为存储帐户创建 StorageCredentials 对象。

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Azure.Services.AppAuthentication;
    using Microsoft.Azure.Storage.Auth;
    
    namespace SharedCode.Authentication
    {
        /// <summary>
        /// Class AzureManagedIdentityAuthentication.
        /// </summary>
        public class AzureManagedIdentityAuthentication
        {
            private string _resource = null;
            /// <summary>
            /// Initializes a new instance of the <see cref="AzureManagedIdentityAuthentication"/> class.
            /// </summary>
            /// <param name="resource">The resource.</param>
            public AzureManagedIdentityAuthentication(string resource)
            {
                _resource = resource;
            }
            /// <summary>
            /// Gets the access token.
            /// </summary>
            /// <returns>TokenCredential.</returns>
            public TokenCredential GetAccessToken()
            {
                // Get the initial access token and the interval at which to refresh it.
                AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
                var tokenAndFrequency = TokenRenewerAsync(azureServiceTokenProvider, CancellationToken.None).GetAwaiter().GetResult();
    
                // Create credentials using the initial token, and connect the callback function 
                // to renew the token just before it expires
                TokenCredential tokenCredential = new TokenCredential(tokenAndFrequency.Token,
                                                                        TokenRenewerAsync,
                                                                        azureServiceTokenProvider,
                                                                        tokenAndFrequency.Frequency.Value);
                return tokenCredential;
            }
            /// <summary>
            /// Renew the token
            /// </summary>
            /// <param name="state">The state.</param>
            /// <param name="cancellationToken">The cancellation token.</param>
            /// <returns>System.Threading.Tasks.Task&lt;Microsoft.Azure.Storage.Auth.NewTokenAndFrequency&gt;.</returns>
            private async Task<NewTokenAndFrequency> TokenRenewerAsync(Object state, CancellationToken cancellationToken)
            {
                // Use the same token provider to request a new token.
                var authResult = await ((AzureServiceTokenProvider)state).GetAuthenticationResultAsync(_resource);
    
                // Renew the token 5 minutes before it expires.
                var next = (authResult.ExpiresOn - DateTimeOffset.UtcNow) - TimeSpan.FromMinutes(5);
                if (next.Ticks < 0)
                {
                    next = default(TimeSpan);
                }
    
                // Return the new token and the next refresh time.
                return new NewTokenAndFrequency(authResult.AccessToken, next);
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

      先决条件

      我们需要了解以下信息才能提出有效的解决方案:

      1. Azure Function 应用程序中的程序集在 Function 启动时加载。但是,对于每次调用,相同的加载程序集用于调用函数应用的方法。这意味着任何单例都将在 Azure 函数的调用中保持不变。
      2. AzureServiceTokenProvider 在对每个资源的 GetAccessTokenAsync 调用之间缓存您的令牌。
      3. AdlsClient 以线程安全的方式保存令牌,并且仅在您要求它做某事时使用它。此外,它还提供了一种以线程安全的方式更新令牌的方法。

      解决方案

          using System;
          using System.Collections.Concurrent;
          using System.Threading;
          using System.Threading.Tasks;
      
          using Microsoft.Azure.DataLake.Store;
          using Microsoft.Azure.Services.AppAuthentication;
      
          public class AdlsClientFactory
          {
              private readonly ConcurrentDictionary<string, Lazy<AdlsClient>> adlsClientDictionary;
      
              public AdlsClientFactory()
              {
                  this.adlsClientDictionary = new ConcurrentDictionary<string, Lazy<AdlsClient>>();
              }
      
              public async Task<IDataStoreClient> CreateAsync(string fqdn)
              {
                  Lazy<AdlsClient> lazyClient = this.adlsClientDictionary.GetOrAdd(fqdn, CreateLazyAdlsClient);
                  AdlsClient adlsClient = lazyClient.Value;
      
                  // Get new token if old token expired otherwise use same token
                  var azureServiceTokenProvider = new AzureServiceTokenProvider();
                  string freshSerializedToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://datalake.azure.net/");
      
                  // "Bearer" + accessToken is done by the <see cref="AdlsClient.SetToken" /> command.
                  adlsClient.SetToken(freshSerializedToken);
      
                  return new AdlDataStoreClient(adlsClient);
              }
      
              private Lazy<AdlsClient> CreateLazyAdlsClient(string fqdn)
              {
                  // TODO: This is just a sample. Figure out how to remove thread blocking while using lazy if that's important to you.
                  var azureServiceTokenProvider = new AzureServiceTokenProvider();
                  string freshSerializedToken = azureServiceTokenProvider.GetAccessTokenAsync("https://datalake.azure.net/").Result;
                  return new Lazy<AdlsClient>(() => AdlsClient.CreateClient(fqdn, "Bearer " + freshSerializedToken), LazyThreadSafetyMode.ExecutionAndPublication);
              }
          }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-04-04
        • 1970-01-01
        • 2018-06-05
        • 1970-01-01
        • 1970-01-01
        • 2015-12-27
        • 2016-04-10
        • 2018-02-17
        相关资源
        最近更新 更多