带有AdlsClient的Azure MSI:访问令牌已过期

时间:2018-08-15 20:37:06

标签: singleton azure-data-lake azure-msi

我正在使用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;
    }
}

谢谢!

4 个答案:

答案 0 :(得分:2)

保证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

答案 1 :(得分:1)

下面的链接中出现了最近的更新,以自动刷新存储帐户的令牌: 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);
        }
    }
}

答案 2 :(得分:0)

如果其他任何人遇到此问题,我都可以按照以下方式进行操作。

我们从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 :(得分:0)

先决条件

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

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