Проблемы с использованием MSI для аутентификации в Azure BlobStorage
Я пытаюсь подключить консольное приложение C# (.net core 2.1) к хранилищу больших двоичных объектов. Я инициализирую клиент хранилища BLOB-объектов несколькими способами. Они есть:
- Строка подключения - полезна при разработке
- Принцип обслуживания - возможность развертывания производства
- MSI-аутентификация - более безопасна, ключи автоматически зацикливаются для нас
В моем коде, если строка подключения не установлена явно, я генерирую ее, используя принцип обслуживания или MSI, в зависимости от определенных настроек приложения (пример кода инициализации ниже). Независимо от того, какой из трех способов я использую, я в конечном итоге инициализирую клиента, используя строку подключения (либо ее явно устанавливают в случае 1., либо генерируют с моим кодом в случае 2. и 3.).
Приведенный ниже код работает на 100% нормально для 1 (строка подключения) и 2 (принцип обслуживания), НО я получаю ошибку при попытке достичь 3 (MSI).
При локальном запуске я получаю эту ошибку:
Маркер доступа получен от неверного эмитента ' https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/'. Он должен соответствовать арендатору https://sts.windows.net/{my-subscription-id} / ', связанному с этой подпиской. Пожалуйста, используйте полномочия (URL) " https://login.windows.net/{my-subscription-id}" для получения токена. Обратите внимание, что если подписка передается другому арендатору, это не влияет на услуги, но распространение информации о новом арендаторе может занять некоторое время (до часа). Если вы только что перенесли свою подписку и видите это сообщение об ошибке, повторите попытку позже.
При этом я понятия не имею, откуда взялся "f8cdef31-a31e-4b4a-93e4-5f571e91255a", это может быть глобальный экземпляр Microsoft. Я попытался смягчить это, запустив мой код в веб-задании в Azure, в котором включен MSI, чтобы увидеть, работает ли это, и я получаю:
System.AggregateException: произошла одна или несколько ошибок. (Исключение произошло во время подключения к службе, более подробно см. Внутреннее исключение) ---> System.Exception: Исключение произошло при подключении к службе, подробнее см. Внутреннее исключение ---> Microsoft.Rest.Azure.CloudException: клиент "{my-subscription-id}" с идентификатором объекта "{my-subscription-id}" не имеет полномочий для выполнения действия "Microsoft.Storage/storageAccounts/read" over scope "/subscription /{my-subscription-id} ".
(обратите внимание, что учетная запись MSI установлена как "владелец" и "оператор ключа учетной записи хранения" хранилища больших двоичных объектов)
Я инициализирую клиента CloudStorageAccount следующим образом:
public void InitializeClient()
{
// Always using the connection string, no matter how it's generated.
if (ConnectionString.IsNullOrEmpty()) // if not already set, then build.
ConnectionString = BuildStorageConnection().GetAwaiter().GetResult();
CloudStorageAccount.TryParse(ConnectionString, out var storageAccount);
if (storageAccount == null)
throw new InvalidOperationException("Cannot find storage account");
// CloudBlobClient that represents the Blob storage endpoint.
_cloudBlobClient = storageAccount.CreateCloudBlobClient();
}
И создайте строку подключения следующим образом:
internal async Task<string> BuildStorageConnection()
{
try
{
string token = null;
if (Config.UseMsi)
{
// Managed Service Identity (MSI) authentication.
var provider = new AzureServiceTokenProvider();
token = provider.GetAccessTokenAsync("https://management.azure.com/").GetAwaiter().GetResult();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Could not authenticate using Managed Service Identity");
_expiryTime = DateTime.Now.AddDays(1);
}
else
{
// Service Principle authentication
// Grab an authentication token from Azure.
var context = new AuthenticationContext("https://login.windows.net/" + Config.TenantId);
var credential = new ClientCredential(Config.AppId, Config.AppSecret);
var tokenResult = context.AcquireTokenAsync("https://management.azure.com/", credential).GetAwaiter().GetResult();
if (tokenResult == null || tokenResult.AccessToken == null)
throw new InvalidOperationException($"Could not authenticate using Service Principle");
_expiryTime = tokenResult.ExpiresOn;
token = tokenResult.AccessToken;
}
// Set credentials and grab the authenticated REST client.
var tokenCredentials = new TokenCredentials(token);
var client = RestClient.Configure()
.WithEnvironment(AzureEnvironment.AzureGlobalCloud)
.WithLogLevel(HttpLoggingDelegatingHandler.Level.BodyAndHeaders)
.WithCredentials(new AzureCredentials(tokenCredentials, tokenCredentials, string.Empty, AzureEnvironment.AzureGlobalCloud))
.WithRetryPolicy(new RetryPolicy(new HttpStatusCodeErrorDetectionStrategy(), new FixedIntervalRetryStrategy(3, TimeSpan.FromMilliseconds(500))))
.Build();
// Authenticate against the management layer.
var azureManagement = Azure.Authenticate(client, string.Empty).WithSubscription(Config.SubscriptionId);
// Get the storage namespace for the passed in instance name.
var storageNamespace = azureManagement.StorageAccounts.List().FirstOrDefault(n => n.Name == Config.StorageInstanceName);
// If we cant find that name, throw an exception.
if (storageNamespace == null)
{
throw new InvalidOperationException($"Could not find the storage instance {Config.StorageInstanceName} in the subscription with ID {Config.SubscriptionId}");
}
// Storage accounts use access keys - this will be used to build a connection string.
var accessKeys = await storageNamespace.GetKeysAsync();
// If the access keys are not found (not configured for some reason), throw an exception.
if (accessKeys == null)
{
throw new InvalidOperationException($"Could not find access keys for the storage instance {Config.StorageInstanceName}");
}
// We just default to the first key.
var key = accessKeys[0].Value;
// Build and return the connection string.
return $"DefaultEndpointsProtocol=https;AccountName={Config.StorageInstanceName};AccountKey={key};EndpointSuffix=core.windows.net";
}
catch (Exception e)
{
Logger?.LogError(e, "An exception occured during connection to blob storage");
throw new Exception("An exception occurred during service connection, see inner exception for more detail", e);
}
}
Основное различие в том, как я получаю токен доступа, состоит в том, что, используя принцип обслуживания, у меня есть контекст аутентификации, а с помощью MSI - нет. Влияет ли это на объем аутентификации? Любая помощь и советы с этим очень ценятся!
1 ответ
Только что понял, как решить проблему выше - изменение GetTokenAsync на второй параметр TenantId дает контекст для вызова аутентификации.
Вот код, который вам понадобится:
token = await provider.GetAccessTokenAsync("https://management.azure.com/", Config.TenantId);