将 MSAL.NET 与 Microsoft.Identity.Web 集成到 .NET Framework 中

本指南介绍如何在 .NET Framework、.NET Standard 2.0 和经典 .NET 应用程序(.NET 4.7.2+)中,将 Microsoft.Identity.Web 令牌缓存和证书包与 MSAL.NET 配合使用。

了解概述

Microsoft.Identity.Web 1.17+ 开始,您可以在非 ASP.NET Core 环境中将 MSAL.NET 与 Microsoft.Identity.Web 实用工具包一起使用。

确定包的优势

功能 益处
令牌缓存序列化 内存中、SQL Server、Redis、Cosmos DB、PostgreSQL 的可重用缓存适配器
证书助手 从 KeyVault、文件系统或证书存储中简化的证书加载
声明扩展 用于 ClaimsPrincipal 操作的实用工具方法
.NET 标准版 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/Web 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 提供可与 MSAL.NET 的 IConfidentialClientApplication 无缝集成的令牌缓存适配器。

使用令牌缓存生成机密客户端

以下示例创建机密客户端应用程序并附加内存中令牌缓存。

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 Database for PostgreSQL 一起使用
  • 需要 PostgreSQL 服务器

使用案例:已使用 PostgreSQL 作为主数据库的应用程序,或使用 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 密钥保管库加载证书并创建机密客户端应用程序。

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 密钥保管库加载

通过指定保管库 URL 和证书名称来加载存储在Azure 密钥保管库中的证书。

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

先决条件

  • 具有 密钥保管库 访问权限的托管标识或服务主体
  • Azure.Identity NuGet 包
  • 密钥保管库权限:证书上的 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:

<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示例

下表列出了演示令牌缓存和证书加载的官方示例。

示例 平台 说明
ConfidentialClientTokenCache 控制台(.NET框架) 令牌缓存序列化模式
active-directory-dotnetcore-daemon-v2 控制台(.NET核心) 从密钥保管库加载证书

遵循最佳做法

应用这些模式以生成可靠且安全的应用程序。

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 密钥保管库或正确保护的证书存储中。

// 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 异常并记录关联 ID 以便进行故障排除。

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
令牌缓存 需要手动序列化 通过扩展方法的内置适配器
证书 手动加载 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(新增):

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

使用这些资源了解有关相关方案的详细信息。