带有 AdlsClient 的 Azure MSI:访问令牌已过期
Azure MSI with AdlsClient: Access token expired
我正在使用 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 令牌提供商没有 return 令牌的到期日期,因此我无法检查令牌是否仍然有效。
处理这个问题的正确方法是什么?感谢任何帮助。
这是我的代码。
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;
}
}
谢谢!
GetAccessTokenAsync returns 保证在接下来的 5 分钟内不会过期的访问令牌。默认情况下,Azure AD 访问令牌在一小时后过期 [1]。
因此,如果您使用同一个令牌(具有默认过期时间)超过一个小时,您将收到 "expired token" 错误消息。每次需要使用 AdlsClient 时,请使用从 GetAccessTokenAsync 获取的令牌初始化 AdlsClient。 GetAccessTokenAsync 将访问令牌缓存在内存中,如果令牌在 5 分钟内过期,将自动获取新令牌。
惰性对象总是 returns 与使用 [2] 初始化的对象相同。因此,AdlsClient 继续使用旧令牌。
参考资料
如果其他人遇到此问题,我可以通过以下方式使其正常工作。
我们从 Varun 的回答中得知 "GetAccessTokenAsync caches the access token in memory, and will automatically get a new token if it is within 5 minutes of expiry"
因此,我们可以只检查当前访问令牌是否与旧访问令牌不同。只有在令牌到期后 5 分钟内才会出现这种情况,在这种情况下,我们将创建一个新的静态客户端。在所有其他情况下,我们只 return 现有客户。
像这样...
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;
}
最近的更新出现在下面的 link 中以自动刷新存储帐户的令牌:
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);
下面是我从文章中修改的 class 以一般刷新令牌。现在,通过在实例化 AzureManagedIdentityAuthentication
时提供相关资源地址,这可以用于自动刷新 ADLS Gen1(“https://datalake.azure.net/") and Storage Accounts("https://storage.azure.com/”)的 MSI 令牌。确保使用 link 中的代码为存储帐户创建 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 Function 应用程序中的程序集在 Function 启动时加载。但是,对于每次调用,相同的加载程序集用于调用函数应用程序的方法。这意味着任何单例都将在您的 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);
}
}
我正在使用 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 令牌提供商没有 return 令牌的到期日期,因此我无法检查令牌是否仍然有效。
处理这个问题的正确方法是什么?感谢任何帮助。
这是我的代码。
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;
}
}
谢谢!
GetAccessTokenAsync returns 保证在接下来的 5 分钟内不会过期的访问令牌。默认情况下,Azure AD 访问令牌在一小时后过期 [1]。
因此,如果您使用同一个令牌(具有默认过期时间)超过一个小时,您将收到 "expired token" 错误消息。每次需要使用 AdlsClient 时,请使用从 GetAccessTokenAsync 获取的令牌初始化 AdlsClient。 GetAccessTokenAsync 将访问令牌缓存在内存中,如果令牌在 5 分钟内过期,将自动获取新令牌。
惰性对象总是 returns 与使用 [2] 初始化的对象相同。因此,AdlsClient 继续使用旧令牌。
参考资料
如果其他人遇到此问题,我可以通过以下方式使其正常工作。
我们从 Varun 的回答中得知 "GetAccessTokenAsync caches the access token in memory, and will automatically get a new token if it is within 5 minutes of expiry"
因此,我们可以只检查当前访问令牌是否与旧访问令牌不同。只有在令牌到期后 5 分钟内才会出现这种情况,在这种情况下,我们将创建一个新的静态客户端。在所有其他情况下,我们只 return 现有客户。
像这样...
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;
}
最近的更新出现在下面的 link 中以自动刷新存储帐户的令牌: 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);
下面是我从文章中修改的 class 以一般刷新令牌。现在,通过在实例化 AzureManagedIdentityAuthentication
时提供相关资源地址,这可以用于自动刷新 ADLS Gen1(“https://datalake.azure.net/") and Storage Accounts("https://storage.azure.com/”)的 MSI 令牌。确保使用 link 中的代码为存储帐户创建 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 Function 应用程序中的程序集在 Function 启动时加载。但是,对于每次调用,相同的加载程序集用于调用函数应用程序的方法。这意味着任何单例都将在您的 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);
}
}