Интеграция MSAL.NET с Microsoft. Identity.Web в .NET Framework

В этом руководстве показано, как использовать кэш токенов и пакеты сертификатов Microsoft.Identity.Web с MSAL.NET в .NET Framework, .NET Standard 2.0 и классических приложениях .NET (.NET 4.7.2+).

Понимание общих сведений

Начиная с Microsoft.Identity.Web 1.17+, можно использовать служебные пакеты Microsoft.Identity.Web с MSAL.NET в средах, отличных от ASP.NET Core.

Определение преимуществ пакета

Функция Преимущество
Сериализация кэша токенов Многократно используемые адаптеры кэша для оперативной памяти, SQL Server, PostgreSQL, Redis, Cosmos DB
Вспомогательные службы сертификатов Упрощенная загрузка сертификатов из KeyVault, файловой системы или хранилища сертификатов
Расширения запросов Служебные методы для обработки ClaimsPrincipal
.NET Standard 2.0 Совместим с .NET Framework 4.7.2+, .NET Core и .NET 5+
Минимальные зависимости Целевые пакеты без зависимостей ASP.NET Core

Просмотр поддерживаемых сценариев

Следующие сценарии поддерживаются с целевыми пакетами служебной программы.

  • Консольные приложения платформы .NET Framework (демон-сценарии)
  • Desktop Applications (.NET Framework)
  • Worker Services (.NET Framework)
  • .NET Стандартные библиотеки 2.0 (кроссплатформенная совместимость)
  • Независимые от веб приложения MSAL.NET

Замечание

Для приложений ASP.NET MVC/веб-API см. OWIN Integration.


Выбор пакетов

Выберите пакет, соответствующий вашему сценарию.

Определение основных пакетов для MSAL.NET

Package Purpose Зависимости целевой объект .NET
Microsoft. Identity.Web.TokenCache Сериализаторы кэша токенов, ClaimsPrincipal расширения Минимальный .NET standard 2.0
Microsoft. Identity.Web.Certificate Служебные программы загрузки сертификатов Минимальный .NET standard 2.0

Установка пакетов

Используйте один из следующих методов, чтобы добавить пакеты в проект.

диспетчер пакетов Console:

# Token cache serialization
Install-Package Microsoft.Identity.Web.TokenCache

# Certificate management
Install-Package Microsoft.Identity.Web.Certificate

.NET CLI:

dotnet add package Microsoft.Identity.Web.TokenCache
dotnet add package Microsoft.Identity.Web.Certificate

Общие сведения об ограничениях основных пакетов

Основной пакет Microsoft.Identity.Web включает зависимости ASP.NET Core (Microsoft.AspNetCore.*), которые:

  • Несовместимы с ASP.NET Framework
  • Увеличение размера пакета ненужным образом
  • Создание конфликтов зависимостей

Используйте целевые пакеты для сценариев .NET Framework и .NET Standard.


Настройка сериализации кэша токенов

Понимание кэш-адаптеров токенов

Microsoft.Identity.Web предоставляет адаптеры кэша токенов, которые легко работают с IConfidentialClientApplication MSAL.NET.

Создание конфиденциального клиента с кэшем маркеров

В следующем примере создается конфиденциальное клиентское приложение и подключается кэш маркеров в памяти.

using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders;

public class MsalAppBuilder
{
    private static IConfidentialClientApplication _app;

    public static IConfidentialClientApplication BuildConfidentialClientApplication()
    {
        if (_app == null)
        {
            string clientId = ConfigurationManager.AppSettings["AzureAd:ClientId"];
            string clientSecret = ConfigurationManager.AppSettings["AzureAd:ClientSecret"];
            string tenantId = ConfigurationManager.AppSettings["AzureAd:TenantId"];

            // Create the confidential client application
            _app = ConfidentialClientApplicationBuilder.Create(clientId)
                .WithClientSecret(clientSecret)
                .WithTenantId(tenantId)
                .WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
                .Build();

            // Add token cache serialization (choose one option below)
            _app.AddInMemoryTokenCache();
        }

        return _app;
    }
}

Выберите параметры кэша токенов

Выберите поставщик кэша, который лучше всего подходит для сценария развертывания.

Настройка кэша токенов в памяти

В следующем примере добавляется простой кэш в памяти:

using Microsoft.Identity.Web.TokenCacheProviders;

_app.AddInMemoryTokenCache();

Кэш в памяти с ограничениями размера (Microsoft. Identity.Web 1.20+):

using Microsoft.Extensions.Caching.Memory;

_app.AddInMemoryTokenCache(services =>
{
    // Configure memory cache options
    services.Configure<MemoryCacheOptions>(options =>
    {
        options.SizeLimit = 5000000;  // 5 MB limit
    });
});

Характеристики.

  • Быстрый доступ
  • Нет внешних зависимостей
  • Не разделяется между процессами
  • Потеряно при перезапуске приложения

Вариант использования: Одноэкземплярные консольные приложения, настольные приложения


Настройка распределенного кэша маркеров в памяти

Используйте следующий код для добавления распределенного кэша в памяти для сред с несколькими экземплярами:

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.Memory (NuGet)
    services.AddDistributedMemoryCache();
});

Характеристики.

  • Разделено между экземплярами приложения
  • Лучше подходит для сценариев балансировки нагрузки
  • Требуется дополнительный пакет NuGet
  • Данные по-прежнему теряются при перезапуске приложения

Вариант использования: Службы с несколькими экземплярами с приемлемым получением токена


Настройка кэша маркеров SQL Server

Используйте следующий код для добавления постоянного распределенного SQL Server кэша:

using Microsoft.Extensions.Caching.SqlServer;

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.SqlServer (NuGet)
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString;
        options.SchemaName = "dbo";
        options.TableName = "TokenCache";

        // IMPORTANT: Set expiration above token lifetime
        // Access tokens typically expire after 1 hour
        options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
    });
});

Выполните следующий SQL, чтобы создать требуемую таблицу кэша:

-- Create the cache table
CREATE TABLE [dbo].[TokenCache] (
    [Id] NVARCHAR(449) NOT NULL,
    [Value] VARBINARY(MAX) NOT NULL,
    [ExpiresAtTime] DATETIMEOFFSET NOT NULL,
    [SlidingExpirationInSeconds] BIGINT NULL,
    [AbsoluteExpiration] DATETIMEOFFSET NULL,
    PRIMARY KEY ([Id])
);

-- Create index for performance
CREATE INDEX [Index_ExpiresAtTime] ON [dbo].[TokenCache] ([ExpiresAtTime]);

Характеристики.

  • Сохраняется при перезапусках
  • Общий для нескольких экземпляров
  • Надежная и масштабируемая
  • Требуется настройка SQL Server

Вариант использования: Производственные демоны сервиса, запланированные задачи, многопоточные рабочие процессы


Настройка кэша маркеров Redis

Используйте следующий код, чтобы добавить высокопроизводительный распределенный кэш Redis:

using StackExchange.Redis;
using Microsoft.Extensions.Caching.StackExchangeRedis;

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.StackExchangeRedis (NuGet)
    services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"];
        options.InstanceName = "TokenCache_";
    });
});

В следующем примере показана конфигурация Redis, готовая к работе.

services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"];
    options.InstanceName = "MyDaemonApp_";

    // Optional: Configure Redis options
    options.ConfigurationOptions = new ConfigurationOptions
    {
        AbortOnConnectFail = false,
        ConnectTimeout = 5000,
        SyncTimeout = 5000
    };
});

Характеристики.

  • Очень быстро
  • Общий доступ между экземплярами
  • Постоянный (с включенной сохраняемостью Redis)
  • Требуется сервер Redis

Вариант использования: демон-приложения с высокой нагрузкой, распределенные системы, микросервисы


Настройка кэша маркеров Cosmos DB

Используйте следующий код для добавления глобально распределенного кэша Cosmos DB:

using Microsoft.Extensions.Caching.Cosmos;

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.Cosmos (preview)
    services.AddCosmosCache(options =>
    {
        options.ContainerName = "TokenCache";
        options.DatabaseName = "IdentityCache";
        options.ClientBuilder = new CosmosClientBuilder(
            ConfigurationManager.AppSettings["CosmosConnectionString"]);
        options.CreateIfNotExists = true;
    });
});

Характеристики.

  • Глобально распределенное
  • Высокая доступность
  • Автоматическое масштабирование
  • Более высокая задержка, чем Redis
  • Более высокая стоимость

Вариант использования: Глобальные службы управляющей программы, географически распределенные приложения


Настройка кэша токенов PostgreSQL

Используйте следующий код для добавления распределенного кэша PostgreSQL:

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.Postgres (NuGet)
    services.AddDistributedPostgresCache(options =>
    {
        options.ConnectionString = ConfigurationManager.ConnectionStrings["PostgresCache"].ConnectionString;
        options.SchemaName = ConfigurationManager.AppSettings["PostgresCache:SchemaName"];
        options.TableName = ConfigurationManager.AppSettings["PostgresCache:TableName"];
        options.CreateIfNotExists = bool.Parse(
            ConfigurationManager.AppSettings["PostgresCache:CreateIfNotExists"] ?? "true");

        // Set expiration above token lifetime.
        // Access tokens typically expire after 1 hour.
        options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
    });
});

Характеристики.

  • Сохраняется при перезапусках
  • Общий для нескольких экземпляров
  • Знакомая семантика SQL
  • Работает с База данных Azure для PostgreSQL
  • Требуется сервер PostgreSQL

Use case: Приложения, которые уже используют PostgreSQL в качестве основной базы данных, или службы, размещенные на Azure, использующие База данных Azure для PostgreSQL


Создание полного приложения управляющей программы

В следующем примере показано полное демон-приложение, которое получает токены с помощью учетных данных клиента и кэша токенов SQL Server.

using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders;
using System;
using System.Threading.Tasks;

namespace DaemonApp
{
    class Program
    {
        private static IConfidentialClientApplication _app;

        static async Task Main(string[] args)
        {
            // Build confidential client with token cache
            _app = BuildConfidentialClient();

            // Acquire token for app-only access
            string[] scopes = new[] { "https://graph.microsoft.com/.default" };

            try
            {
                var result = await _app.AcquireTokenForClient(scopes)
                    .ExecuteAsync();

                Console.WriteLine($"Token acquired successfully!");
                Console.WriteLine($"Token source: {result.AuthenticationResultMetadata.TokenSource}");
                Console.WriteLine($"Expires on: {result.ExpiresOn}");

                // Use token to call API
                await CallProtectedApi(result.AccessToken);
            }
            catch (MsalServiceException ex)
            {
                Console.WriteLine($"Error acquiring token: {ex.ErrorCode}");
                Console.WriteLine($"CorrelationId: {ex.CorrelationId}");
            }
        }

        private static IConfidentialClientApplication BuildConfidentialClient()
        {
            var app = ConfidentialClientApplicationBuilder
                .Create(ConfigurationManager.AppSettings["ClientId"])
                .WithClientSecret(ConfigurationManager.AppSettings["ClientSecret"])
                .WithTenantId(ConfigurationManager.AppSettings["TenantId"])
                .Build();

            // Add SQL Server token cache for persistence
            app.AddDistributedTokenCaches(services =>
            {
                services.AddDistributedSqlServerCache(options =>
                {
                    options.ConnectionString = ConfigurationManager
                        .ConnectionStrings["TokenCache"].ConnectionString;
                    options.SchemaName = "dbo";
                    options.TableName = "TokenCache";
                    options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
                });
            });

            return app;
        }

        private static async Task CallProtectedApi(string accessToken)
        {
            // Your API call logic
        }
    }
}

Управление сертификатами

Общие сведения о загрузке сертификата

Microsoft. Identity.Web упрощает загрузку сертификатов из различных источников для потоков учетных данных клиента.

Загрузка сертификатов с помощью DefaultCertificateLoader

В следующем примере показано, как загрузить сертификат из Azure Key Vault и создать конфиденциальное клиентское приложение.

using Microsoft.Identity.Web;
using Microsoft.Identity.Client;

public class CertificateHelper
{
    public static IConfidentialClientApplication CreateAppWithCertificate()
    {
        string clientId = ConfigurationManager.AppSettings["AzureAd:ClientId"];
        string tenantId = ConfigurationManager.AppSettings["AzureAd:TenantId"];

        // Define certificate source
        var certDescription = CertificateDescription.FromKeyVault(
            keyVaultUrl: "https://my-keyvault.vault.azure.net",
            keyVaultCertificateName: "MyCertificate"
        );

        // Load certificate
        ICertificateLoader certificateLoader = new DefaultCertificateLoader();
        certificateLoader.LoadIfNeeded(certDescription);

        // Create confidential client with certificate
        var app = ConfidentialClientApplicationBuilder.Create(clientId)
            .WithCertificate(certDescription.Certificate)
            .WithTenantId(tenantId)
            .Build();

        // Add token cache
        app.AddInMemoryTokenCache();

        return app;
    }
}

Выбор источников сертификатов

Загрузка из Azure Key Vault

Загрузите сертификат, хранящийся в Azure Key Vault, указав URL-адрес хранилища и имя сертификата.

var certDescription = CertificateDescription.FromKeyVault(
    keyVaultUrl: "https://my-keyvault.vault.azure.net",
    keyVaultCertificateName: "MyApplicationCert"
);

ICertificateLoader loader = new DefaultCertificateLoader();
loader.LoadIfNeeded(certDescription);

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithCertificate(certDescription.Certificate)
    .WithTenantId(tenantId)
    .Build();

Необходимые условия:

  • Управляемое удостоверение или служебный принципал с доступом к Кей Волт
  • пакет NuGet Azure.Identity
  • разрешение Key Vault: Get для сертификатов

Загрузка из хранилища сертификатов

Загрузите сертификат из хранилища сертификатов Windows по отличительному имени.

var certDescription = CertificateDescription.FromStoreWithDistinguishedName(
    distinguishedName: "CN=MyApp.contoso.com",
    storeName: StoreName.My,
    storeLocation: StoreLocation.CurrentUser
);

ICertificateLoader loader = new DefaultCertificateLoader();
loader.LoadIfNeeded(certDescription);

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithCertificate(certDescription.Certificate)
    .WithTenantId(tenantId)
    .Build();

Вы также можете найти сертификат по отпечатку:

var certDescription = CertificateDescription.FromStoreWithThumbprint(
    thumbprint: "ABCDEF1234567890ABCDEF1234567890ABCDEF12",
    storeName: StoreName.My,
    storeLocation: StoreLocation.LocalMachine
);

Загрузка из файловой системы

Загрузите сертификат из PFX-файла в локальной файловой системе.

var certDescription = CertificateDescription.FromPath(
    path: @"C:\Certificates\MyAppCert.pfx",
    password: ConfigurationManager.AppSettings["Certificate:Password"]
);

ICertificateLoader loader = new DefaultCertificateLoader();
loader.LoadIfNeeded(certDescription);

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithCertificate(certDescription.Certificate)
    .WithTenantId(tenantId)
    .Build();

Примечание по безопасности: Никогда не закодировать пароли. Используйте безопасную конфигурацию.


Загрузка из строки в кодировке Base64

Загрузите сертификат из строки в кодировке Base64, хранящейся в конфигурации.

string base64Cert = ConfigurationManager.AppSettings["Certificate:Base64"];

var certDescription = CertificateDescription.FromBase64Encoded(
    base64EncodedValue: base64Cert,
    password: ConfigurationManager.AppSettings["Certificate:Password"]  // Optional
);

ICertificateLoader loader = new DefaultCertificateLoader();
loader.LoadIfNeeded(certDescription);

Настройка загрузки сертификатов из App.config

Определите параметры сертификата в файле App.config и загрузите их во время выполнения.

App.config:

<appSettings>
  <add key="AzureAd:ClientId" value="your-client-id" />
  <add key="AzureAd:TenantId" value="your-tenant-id" />

  <!-- Option 1: KeyVault -->
  <add key="Certificate:SourceType" value="KeyVault" />
  <add key="Certificate:KeyVaultUrl" value="https://my-vault.vault.azure.net" />
  <add key="Certificate:KeyVaultCertificateName" value="MyCert" />

  <!-- Option 2: Store -->
  <!--
  <add key="Certificate:SourceType" value="StoreWithThumbprint" />
  <add key="Certificate:CertificateThumbprint" value="ABCD..." />
  <add key="Certificate:CertificateStorePath" value="CurrentUser/My" />
  -->
</appSettings>

<connectionStrings>
  <add name="TokenCache"
       connectionString="Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TokenCache;Integrated Security=True;" />
</connectionStrings>

Используйте следующий вспомогательный метод для загрузки сертификата на основе конфигурации:

public static CertificateDescription GetCertificateFromConfig()
{
    string sourceType = ConfigurationManager.AppSettings["Certificate:SourceType"];

    return sourceType switch
    {
        "KeyVault" => CertificateDescription.FromKeyVault(
            ConfigurationManager.AppSettings["Certificate:KeyVaultUrl"],
            ConfigurationManager.AppSettings["Certificate:KeyVaultCertificateName"]
        ),

        "StoreWithThumbprint" => CertificateDescription.FromStoreWithThumbprint(
            ConfigurationManager.AppSettings["Certificate:CertificateThumbprint"],
            StoreName.My,
            StoreLocation.CurrentUser
        ),

        _ => throw new ConfigurationErrorsException("Invalid certificate source type")
    };
}

Изучение примеров приложений

Просмотрите эти примеры, чтобы просмотреть рабочие реализации.

Просмотр официальных примеров Microsoft

В следующей таблице перечислены официальные примеры, демонстрирующие кэширование маркеров и загрузку сертификатов.

Образец Platform Описание
ConfidentialClientTokenCache Консоль (.NET Framework) Шаблоны сериализации кэша токенов
active-directory-dotnetcore-daemon-v2 Консоль (.NET Core) Загрузка сертификата из Key Vault

Следуйте лучшим практикам

Примените эти шаблоны для создания надежных и безопасных приложений.

1. Используйте одноэлементный шаблон для IConfidentialClientApplication:

Создайте один экземпляр и повторно используйте его в приложении.

private static IConfidentialClientApplication _app;

public static IConfidentialClientApplication GetApp()
{
    if (_app == null)
    {
        _app = ConfidentialClientApplicationBuilder.Create(clientId)
            .WithClientSecret(clientSecret)
            .WithTenantId(tenantId)
            .Build();

        _app.AddDistributedTokenCaches(/* ... */);
    }

    return _app;
}

2. Установите соответствующий срок действия кэша маркеров:

Настройте время динамического истечения, превышающее срок действия маркера, чтобы избежать ненужного повторного получения.

// Access tokens typically expire after 1 hour
// Set cache expiration ABOVE token lifetime
options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);

3. Используйте безопасное хранилище сертификатов:

Храните сертификаты в Azure Key Vault или правильно защищенном хранилище сертификатов.

// Azure Key Vault (production)
var cert = CertificateDescription.FromKeyVault(keyVaultUrl, certName);

// Certificate store with proper permissions
var cert = CertificateDescription.FromStoreWithThumbprint(
    thumbprint, StoreName.My, StoreLocation.LocalMachine);

4. Реализуйте правильную обработку ошибок:

Перехват исключений MSAL и ведение журнала идентификатора корреляции для устранения неполадок.

try
{
    var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
}
catch (MsalServiceException ex)
{
    logger.Error($"Token acquisition failed. CorrelationId: {ex.CorrelationId}, ErrorCode: {ex.ErrorCode}");
    throw;
}

5. Использование распределенного кэша для рабочей среды:

Распределенный кэш обменивается токенами между экземплярами и сохраняет их после перезапусков.

// Correct for daemon services
app.AddDistributedTokenCaches(services =>
{
    services.AddDistributedSqlServerCache(/* ... */);
});

Избегайте распространенных ошибок

1. Не создавайте экземпляры IConfidentialClientApplication многократно:

// Wrong - creates new instance every time
public void AcquireToken()
{
    var app = ConfidentialClientApplicationBuilder.Create(clientId).Build();
    // ...
}

// Correct - use singleton
private static readonly IConfidentialClientApplication _app = BuildApp();

2. Не задавайте секреты жесткого кода:

// Wrong
.WithClientSecret("supersecretvalue123")

// Correct
.WithClientSecret(ConfigurationManager.AppSettings["AzureAd:ClientSecret"])

3. Не используйте кэш в памяти для служб с несколькими экземплярами:

// Wrong for services with multiple instances
app.AddInMemoryTokenCache();

// Correct - use distributed cache
app.AddDistributedTokenCaches(services =>
{
    services.AddDistributedSqlServerCache(/* ... */);
});

4. Не игнорируйте проверку сертификата:

// Wrong - skips validation
ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, errors) => true;

// Correct - validate certificates properly

Миграция из ADAL.NET

Просмотрите основные различия и обновите код, чтобы использовать MSAL.NET с Microsoft. Identity.Web.

Общие сведения о ключевых различиях

Аспект ADAL.NET (не рекомендуется) MSAL.NET + Microsoft. Identity.Web
Области применения На основе ресурсов (https://graph.microsoft.com) На основе области (https://graph.microsoft.com/.default)
Кэш токенов Требуется сериализация вручную Встроенные адаптеры с помощью методов расширения
Certificates Загрузка вручную X509Certificate2 DefaultCertificateLoader с несколькими источниками
Авторитет Исправлено на этапе строительства Может быть переопределено для каждого запроса

Сравнение примеров миграции

ADAL.NET (Старое):

AuthenticationContext authContext = new AuthenticationContext(authority);
ClientCredential credential = new ClientCredential(clientId, clientSecret);
AuthenticationResult result = await authContext.AcquireTokenAsync(resource, credential);

MSAL.NET с Microsoft. Identity.Web (New):

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithClientSecret(clientSecret)
    .WithTenantId(tenantId)
    .Build();

app.AddInMemoryTokenCache();  // Add token cache

string[] scopes = new[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync();

Используйте эти ресурсы, чтобы узнать больше о связанных сценариях.