Azure MSI с AdlsClient: токен доступа истек
Я использую Azure Managed Service Identity (MSI) для создания статического (одиночного) AdlsClient.
Затем я использую AdlsClient в приложении Functions для записи в хранилище озера данных.
Приложение работает нормально около дня, но потом перестает работать, и я вижу эту ошибку.
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;
}
}
Спасибо!
2 ответа
Если кто-то еще столкнулся с этой проблемой, я смог заставить ее работать следующим образом.
Из ответа Варуна мы знаем, что "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;
}
Маркер доступа, который возвращает GetAccessTokenAsync, гарантированно не истечет в течение следующих 5 минут. По умолчанию токены доступа Azure AD истекают через час [1].
Таким образом, если вы используете один и тот же токен (со сроком действия по умолчанию) более часа, вы получите сообщение об ошибке "expired token". Пожалуйста, инициализируйте AdlsClient токеном, полученным из GetAccessTokenAsync каждый раз, когда вам нужно использовать AdlsClient. GetAccessTokenAsync кэширует токен доступа в памяти и автоматически получит новый токен, если он истекает в течение 5 минут.
Ленивый объект всегда возвращает тот же объект, который был инициализирован с помощью [2]. Итак, AdlsClient продолжает использовать старый токен.
Рекомендации
[2] https://docs.microsoft.com/en-us/dotnet/framework/performance/lazy-initialization
По ссылке ниже появилось недавнее обновление для автоматического обновления токенов для учетных записей хранения: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);
Ниже приведен класс, который я изменил из статьи для общего обновления токенов. Теперь это можно использовать для автоматического обновления токенов MSI как для ADLS Gen1("https://datalake.azure.net/"), так и для учетных записей хранения ("https://storage.azure.com/"), предоставив соответствующий ресурс. адрес при создании экземпляраAzureManagedIdentityAuthentication
. Обязательно используйте код из ссылки для создания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<Microsoft.Azure.Storage.Auth.NewTokenAndFrequency>.</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);
}
}
}
Предпосылки
Чтобы найти эффективное решение, нам необходимо знать следующую информацию:
- Ваша сборка в приложении функции Azure загружается при запуске функции. Однако для каждого вызова одна и та же загруженная сборка используется для вызова метода вашего приложения-функции. Это означает, что любые синглтоны будут сохраняться при вызовах вашей функции Azure.
AzureServiceTokenProvider
кэширует ваш токен между вызовамиGetAccessTokenAsync
для каждого ресурса.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);
}
}