在 WinForms/WPF 应用中集成 Entra ID 认证 —— MSAL.NET 与 WAM Broker 的实务架构

· · Windows, C#, .NET, WinForms, WPF, Entra ID, 认证, 安全, 技术咨询

“公司内部的每个业务应用都各自做了登录界面,密码都存在自己的数据库里。每次有人离职,就要一个个应用去把账号停掉,太痛苦了。”“公司已经全员引入了 Microsoft 365,能不能直接用那个账号登录?”这类话题,在桌面应用改造相关的咨询中,近几年稳步增多。也有的时候,是信息系统部门出于密码泄露事故或零信任的考量,向业务部门提出”不要再自己管理密码了”的要求。

先说结论:如果贵组织已经在使用 Microsoft 365,把 WinForms / WPF 内部应用的登录方式迁移到 Entra ID(原 Azure AD),是一项思路正确的投资。应用将完全不再持有任何密码,多因素认证、条件访问、登录日志等租户一侧的防护与审计手段,也能直接延伸到内部应用上。实现上也只需要一个叫作 MSAL.NET 的库和几十行代码。

不过,桌面应用也有一些特有的坑。”用文本框接收用户名和密码、在后台完成认证”的老式做法(ROPC),官方已明确走向弃用,不应在新开发中采用。如果不对令牌缓存做持久化,每次启动都会弹出登录界面;而在 Windows 上是否使用 Broker(WAM),会极大地影响体验和安全性。本文将从最基础的概念梳理开始,依次讲解应用注册、MSAL.NET 的实现、WAM、缓存、导入判断,直至运维中的常见陷阱。

1. 先说结论

  • 停止自建 ID 与密码管理,转向 Entra ID 之后,密码保管、重置支持、离职人员账号停用、登录审计都会变成租户一侧的工作。应用一侧的责任范围大幅缩小,是最大的好处。
  • 桌面应用是公共客户端。exe 分发到任何地方都可能被反编译分析,因此不能(也不应该)持有客户端密钥。应用注册也需要按照公共客户端进行配置。1
  • 由应用直接接收用户名和密码的 ROPC(Resource Owner Password Credentials)已被官方明确标注为「弃用(deprecated)」,并发布了迁移指南。它与 MFA、条件访问不兼容,实际上正走向无法使用的方向。应当视为新开发中的禁止项。23
  • 实现上使用 MSAL.NET(Microsoft.Identity.Client),调用模式只有一种基本形态:先调用 AcquireTokenSilent,仅在抛出 MsalUiRequiredException 时才回退到 AcquireTokenInteractive4
  • 在 Windows 上,推荐通过 WAM(Web Account Manager)Broker 完成认证。与已登录 Windows 的账号进行 SSO、支持条件访问 / Windows Hello / FIDO 密钥、刷新令牌与设备绑定,只需一行 WithBroker 即可获得。5
  • 忘记对令牌缓存做持久化,会导致应用每次重启都弹出登录界面。请从一开始就集成 Microsoft.Identity.Client.Extensions.Msal 的加密缓存。6
  • Entra 认证是一套依赖网络的机制。对于必须完全离线运行的现场应用并不适用,请先参照第 8 章的判断表评估是否可以导入。

2. 全局概览 —— 停止自建密码管理意味着什么

2.1 自建管理的问题所在

如果业务应用用自建的用户表来管理密码,以下责任都会落在应用(也就是我们开发者)身上。

  • 保管:哈希方式的选型与实现(至今仍能见到 15 年前留下来的、依然使用无盐 MD5 的老表)
  • 运维:处理密码重置咨询、锁定账号、分发初始密码
  • 生命周期:员工离职、调岗时停用账号。如果有 5 个应用,就要停用 5 次
  • 审计:记录并留存谁在什么时候登录过。多因素认证几乎无法实现

将认证委托给 Entra ID 之后,这 4 项责任会从应用代码中消失,统一归入租户的管理范畴。员工离职时,只要停用其 Entra ID 账号,就能让其在所有应用中立即无法登录,登录日志也会自动留存。对于已经引入 Microsoft 365 的组织来说,继续维持自建认证的理由已经不多了。顺带一提,Google Workspace 组织中将 Windows 登录本身迁移到 Google 账号的对应机制,已在「什么是 GCPW」中介绍。

2.2 最小限度的概念 —— 公共客户端与令牌

这里省略 OAuth 2.0 / OpenID Connect 教科书式的说明,只列出桌面应用实现中必须掌握的概念。

概念 在桌面应用中的含义
公共客户端 exe、移动应用等无法安全持有秘密(客户端密钥)的应用。只能代表用户获取令牌
机密客户端(confidential client) Web 服务器、守护进程等能够持有密钥或证书的应用。桌面应用不属于此类
ID 令牌 表示”这个人是谁”的 JWT。如果只需要登录功能,这个就够用了
访问令牌 调用特定 API(Microsoft Graph 或自家 Web API)的通行证,其中固化了目标(audience)与作用域
刷新令牌 用于无交互更新上述两种令牌的令牌。由 MSAL 在缓存内部自动管理,应用无法直接看到

第一行是关键。由于 exe 分发到任何地方都可以被解析、反编译,嵌入其中的”秘密”就不再是秘密。因此应用要注册为无需密钥即可运行的公共客户端,而认证本身(输入密码、MFA)则委托给浏览器或操作系统的 Broker,应用只接收令牌。应用完全不触碰用户密码,正是这套机制的根基所在。

2.3 ROPC 是一条走到尽头的路 —— 官方文档怎么说

老式的想法往往是”自建登录界面接收用户名和密码,在后台交给 Entra ID 验证就好了”。这就是 ROPC(直接传递用户名密码),虽然它在 MSAL.NET 中仍以 AcquireTokenByUsernamePassword 的形式存在,但官方文档现在的表述已经很明确。

  • 面向公共客户端的 ROPC 已被明确标注为”出于安全风险而弃用(deprecated)”,并公开了迁移到更安全流程的指南。3
  • ROPC 与 MFA、条件访问不兼容。在租户强制要求 MFA 的情况下,使用这一流程的用户会被直接拦截而无法登录2
  • SSO 无法生效,个人 Microsoft 账号也无法使用,无密码(FIDO、Authenticator)账号同样无法登录。2
  • Microsoft 的 Web API 一侧也在逐步转向只接受已完成 MFA 的令牌,官方文档本身也写明”依赖 ROPC 的应用将被拒之门外(locked out),桌面应用应迁移到基于 Broker 的认证”。2

由于租户随时可能通过自身设置强制启用 MFA,如果因为”现在还能用”就采用 ROPC,某天可能会导致所有用户突然全部无法登录。即使现有应用目前基于 ROPC 运行,也应提前规划迁移。桌面应用实际可用的获取方式,本质上只有以下两种。

流程 适用场景
交互式(Broker / 浏览器) 常规 GUI 应用。首选方案
设备代码流程 无法显示浏览器的环境(例如通过 SSH 连接的控制台)。显示 URL 与代码,让用户在另一台设备的浏览器上登录

3. 应用注册 —— 在 Entra 管理中心的配置

在编写代码之前,需要先在租户中注册应用。如果开发者本人无法操作,可以直接把本节内容作为提交给信息系统部门的申请单。

3.1 注册本体

在 Microsoft Entra 管理中心(entra.microsoft.com)的 [应用注册] → [新注册] 中创建。7

  • 名称:会出现在同意屏幕和登录日志中,建议用业务侧能理解的名称,例如”库存管理系统”。
  • 支持的账户类型:如果是内部应用,”仅此组织目录中的账户“(单租户)是唯一合理的选择。多租户仅适用于分发给多个组织使用的产品。
  • 注册后显示的应用程序(客户端)ID目录(租户)ID 需要记下来并写入应用配置中(两者都不是秘密信息)。

3.2 重定向 URI —— 平台选择”移动应用程序和桌面应用程序”

这是声明认证完成后接收令牌的位置。在 [身份验证] → [添加平台] → [移动应用程序和桌面应用程序] 中,根据要使用的认证方式注册相应的 URI。1

认证方式 需要注册的重定向 URI
WAM Broker(首选方案,第 5 章) ms-appx-web://microsoft.aad.brokerplugin/{客户端ID}
系统浏览器 http://localhost
嵌入式浏览器 https://login.microsoftonline.com/common/oauth2/nativeclient

WAM 所用的 ms-appx-web://... 不会写在 MSAL 一侧的代码中,但在应用注册一侧是必须的8 考虑到 WAM 不可用时会回退到浏览器(第 5 章),实务上建议从一开始就把表中的三项全部注册好。特别需要留意 WithDefaultRedirectUri() 的行为——其解析结果取决于平台:在 .NET Framework 上会解析为 https://login.microsoftonline.com/common/oauth2/nativeclient,在 .NET(Core 及以后)上则解析为 http://localhost9 如果一个 .NET Framework 应用只注册了 ms-appx-webhttp://localhost,一旦从 WAM 回退到浏览器,就会因为与 nativeclient 不一致而出现认证错误。要么三项全部注册,要么用 WithRedirectUri(...) 显式固定。另一个常见的踩坑点,是误将其注册到”Web”平台下,同样会导致认证错误。

3.3 API 权限与管理员同意

[API 权限] 中,添加应用所调用 API 的委托权限(delegated permission)。如果只是登录并显示个人资料,默认授予的 Microsoft Graph 的 User.Read 就足够了。

添加完成后,请人执行 [为(租户名)授予管理员同意]7 这样可以消除首次登录时逐用户弹出的同意对话框。在用户自身同意功能已被禁用的租户中,如果没有管理员同意,首次登录会卡在”需要管理员批准”这一步,因此对于内部分发的应用,原则上应在分发前就完成管理员同意

3.4 “允许公共客户端流程”开关

[身份验证] 高级设置中的”允许公共客户端流程“,需要在使用不依赖重定向 URI 的流程(例如设备代码流程、集成 Windows 身份验证)时设为”是”。1 如果只使用交互式(浏览器 / Broker),则不是必须项。另外需要注意,这个应用注册不会创建任何客户端密钥或证书。”证书和密钥”一栏为空,正是公共客户端的正确状态(这一点经常被混淆,第 9 章会再次提及)。

4. 用 MSAL.NET 实现 —— Silent 到 Interactive 的基本形态

通过 NuGet 添加 Microsoft.Identity.Client。实现模式只需要记住一种:必须先调用 AcquireTokenSilent,仅在收到 MsalUiRequiredException 时才回退到交互式AcquireTokenInteractive 的设计完全不会查看缓存,如果直接调用它,每次都会弹出登录界面。4

using Microsoft.Identity.Client;

public sealed class AuthService
{
    private const string ClientId = "应用程序(客户端)ID";
    private const string TenantId = "目录(租户)ID";
    private static readonly string[] Scopes = { "User.Read" };

    private readonly IPublicClientApplication _app;

    public AuthService()
    {
        _app = PublicClientApplicationBuilder.Create(ClientId)
            .WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
            .WithRedirectUri("http://localhost")  // 用于系统浏览器
            .Build();
        // 在生产环境中,应在此处注册令牌缓存的持久化(第6章)
    }

    public async Task<AuthenticationResult> SignInAsync(IntPtr ownerHwnd)
    {
        // 1. 始终先尝试针对已缓存账号的静默获取
        var accounts = await _app.GetAccountsAsync();
        var account = accounts.FirstOrDefault();
        try
        {
            return await _app.AcquireTokenSilent(Scopes, account)
                             .ExecuteAsync();
        }
        catch (MsalUiRequiredException)
        {
            // 2. 仅在需要交互时才弹出登录界面。
            // .NET Framework的默认方式是老式的嵌入式WebView,
            // 因此显式指定http://localhost重定向=系统浏览器
            // (.NET 6+本来就只使用系统浏览器)
            return await _app.AcquireTokenInteractive(Scopes)
                             .WithAccount(account)
                             .WithParentActivityOrWindow(ownerHwnd)
                             .WithUseEmbeddedWebView(false)
                             .ExecuteAsync();
        }
    }
}

调用方需要传入所属窗口的句柄。这是为了防止认证对话框被应用自身的窗口遮挡——在使用 WAM 时这是必须的。5 还有一点:在 .NET Framework 应用中不要省略 WithUseEmbeddedWebView(false).NET Framework 交互式认证的默认方式是嵌入式 WebView,而 http://localhost 重定向是给系统浏览器用的(如果两者搭配错了,就会落入不支持条件访问、Windows Hello / FIDO 的老式嵌入式浏览器,或者出现重定向 URI 不一致的问题)。10 .NET 6 及以后版本本身就没有嵌入式 WebView,始终使用系统浏览器,因此这一指定是多余的,但无害。

// WinForms(Form的方法内)
var result = await _authService.SignInAsync(this.Handle);

// WPF
var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
var result = await _authService.SignInAsync(hwnd);

this.Text = $"正在登录: {result.Account.Username}";

补充几个需要留意的要点。

  • 应用中应复用同一个 IPublicClientApplication 实例。由于缓存是与实例绑定的,如果每次调用都用 Create 创建新实例,静默获取就会失效。
  • MsalUiRequiredException 并不是”异常”,而是“需要交互”这一正常的控制流。它会在首次启动、刷新令牌失效、条件访问要求变更等场景下发生。
  • 正确的用法是在调用 API 前每次都调用 AcquireTokenSilent。如果缓存中有有效令牌会立即返回,临近过期时会自动续期。4 不应自行持有并管理访问令牌的生命周期。
  • 在 UI 线程上用 .Result.Wait() 等待会导致死锁(参见「WPF/WinForms 的 async 与 UI 线程一页整理」)。

5. WAM Broker —— Windows 上的推荐配置

第 4 章的代码会打开浏览器,但在 Windows 上还有更好的方案。WAM(Web Account Manager)是内置于 Windows 10(1703 及以后)与 Windows Server 2019 及以后版本中的认证 Broker,官方文档列出了以下四项优势。5

  • 增强安全性:刷新令牌与设备绑定,即使被盗取也无法在其他设备上使用(令牌保护)。安全性会随操作系统更新持续改进。
  • 功能支持Windows Hello、条件访问、FIDO 密钥等与操作系统 / 服务集成的认证功能,无需额外代码即可使用。
  • 系统集成:已登录 Windows 的账号会出现在内置的账号选择器中,因此大多数情况下无需输入密码即可完成登录,本质上就是 SSO。
  • 令牌保护:可支持条件访问的令牌保护策略。

只要公司电脑已加入 Entra(或混合加入),体验就会变成”启动应用 → 选择 Windows 账号 → 立即完成登录”,全程不需要在任何地方输入密码。

5.1 实现 —— WithBroker 与所需包

要使用 WAM,需要 MSAL.NET 4.52.0 及以上版本,以及额外的包 Microsoft.Identity.Client.Broker5 在第 4 章的 Builder 上加上 WithBroker

using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;  // 用于WithBroker(BrokerOptions)

var brokerOptions = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
    Title = "库存管理系统"  // 显示在账号选择器中的标题
};

_app = PublicClientApplicationBuilder.Create(ClientId)
    .WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
    .WithDefaultRedirectUri()
    .WithParentActivityOrWindow(() => _ownerHwnd)  // 使用WAM时必须
    .WithBroker(brokerOptions)
    .Build();

静默获取一侧也可以再加强一行。当缓存中没有账号时,传入 PublicClientApplication.OperatingSystemAccount,就可以尝试用”当前已登录 Windows 的账号”进行静默登录。这是从首次启动开始就能实现无对话框登录的官方推荐模式。8

var accounts = await _app.GetAccountsAsync();
var account = accounts.FirstOrDefault()
              ?? PublicClientApplication.OperatingSystemAccount;
try
{
    return await _app.AcquireTokenSilent(Scopes, account).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
    return await _app.AcquireTokenInteractive(Scopes).ExecuteAsync();
}

与此同时,按照 3.2 节的说明,需要在应用注册一侧把 ms-appx-web://microsoft.aad.brokerplugin/{客户端ID} 注册到”移动应用程序和桌面应用程序”平台下。5 忘记这一步会导致交互式认证因 Broker 错误而失败。还有一点:上面示例中的 WithDefaultRedirectUri() 只决定 WAM 不可用而回退到浏览器时所使用的重定向 URI,其解析结果取决于平台(.NET Framework → nativeclient,.NET → http://localhost)。9 如果已经按 3.2 节表格注册了三项,两种解析结果都能正常工作;如果想缩小注册范围,可用 WithRedirectUri(...) 显式指定。

5.2 WAM 的限制 —— 不了解就容易踩坑

限制 内容
操作系统 Windows 10 (1703)+ / Windows Server 2019+。更早版本、Mac、Linux 会自动回退到浏览器5
身份提供商 仅限 Entra ID。Azure AD B2C、AD FS 的 authority 不受支持(会回退到浏览器)5
执行上下文 前提是能在交互式用户会话中显示 UI。Windows 服务、任务计划程序(用户会话之外)、以 runas 运行为其他用户,都会按设计报错5

第三行尤为重要。”在有界面的应用中能正常运行,但把同一份代码挪到夜间批处理中就失败了”,这属于规格行为,而不是 bug。无人值守的场景应该按应用权限(机密客户端)另行设计,而不是复用委托用户令牌。正因为回退行为是作为设计特性内置的,”优先尝试 WAM,失败则回退到浏览器”才能用一份代码就实现。

6. 令牌缓存的持久化 —— 不要在每次重启时都弹出登录界面

MSAL.NET 的令牌缓存默认只存在于内存中,在桌面应用里,持久化的实现是应用一侧的责任。如果不做持久化,每次进程重启后 AcquireTokenSilent 都会失败并回退到交互式登录。4 “试点阶段一切正常,但上线后每天早上都有用户投诉要重新登录”——这类咨询的根源几乎都在这里。

官方推荐使用跨平台缓存库 Microsoft.Identity.Client.Extensions.Msal(NuGet)。6

using Microsoft.Identity.Client.Extensions.Msal;

var storageProperties = new StorageCreationPropertiesBuilder(
        "msal_cache.dat",
        Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "KomuraSoft", "InventoryApp"))
    .Build();

var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
cacheHelper.RegisterCache(_app.UserTokenCache);  // 在Build()之后立即注册一次

在 Windows 上,缓存会以加密形式保存。官方文档中给出的自行实现示例,是用 ProtectedDataDPAPI、DataProtectionScope.CurrentUser)对令牌加密后存入文件,Extensions.Msal 可以理解为把这套思路打磨成生产级质量的库。6 “用户级别的敏感信息应由用户范围的 DPAPI 来保护”这一原则,与「Windows 应用不要把敏感信息以明文存进配置文件的最佳实践」中讲到的配置文件问题是同一回事。如果自行实现把令牌缓存以明文 JSON 保存,应该按照明文保存连接字符串同等严重的问题来看待。

运维方面有三点需要注意。

  • 即使使用 WAM,缓存持久化仍然是必要的。因为 MSAL 仍然会把 ID 令牌和账号元数据保存在自己的缓存中。8
  • 保存位置一般是 %LOCALAPPDATA%\公司名\应用名。由于 DPAPI 的绑定关系,换一台电脑或换一个用户都无法解密,但这只会导致静默获取失败并转为重新登录,不会造成实际损害。
  • “注销”的实现方式是用 GetAccountsAsync 枚举账号,再用 RemoveAsync 删除,不需要删除缓存文件。但 RemoveAsync 清除的只是 MSAL 的本地缓存,WAM、浏览器、Windows 登录一侧的会话都会原样保留。由于下一次交互式登录可能会静默地重新登录同一个账号,如果需要在共享电脑上实现真正的账号切换,就要在设计上加以区分:可以给 AcquireTokenInteractive 加上 WithPrompt(Prompt.SelectAccount) 以强制显示账号选择界面,或者根据需求同时使用租户的注销端点。要把”清除本地缓存”与”真正的注销”区分开来设计。

7. 拿到令牌之后能做什么 —— 三种配置

认证通过之后,具体怎么用会分成三种模式。做到哪一步,所需的额外配置也不一样。

配置 使用的令牌 额外需要的东西
(1) 仅登录 ID 令牌(AuthenticationResult.Account / ClaimsPrincipal 无(只需 User.Read
(2) 调用 Microsoft Graph 面向 Graph 的访问令牌 与要调用的 API 相匹配的 Graph 权限与同意
(3) 保护自家 Web API 面向自家 API 的访问令牌 API 一侧的应用注册与作用域发布、API 一侧的令牌校验

7.1 仅登录的配置 —— 从最小规模开始

如果只是”想替换自建的密码校验,不调用云端 API”,只需将登录结果中的账号信息与应用内部的权限表进行匹配即可实现。将 users 表的主键替换为 Entra 的对象 ID(即使姓名变更导致 UPN 变化也不会改变),并删除密码列。由于不需要改变本地数据库的设计,只是替换认证方式,这是最容易推荐的第一步配置。

7.2 调用 Microsoft Graph

User.Read 的访问令牌直接发给 Microsoft Graph,就能获取登录用户的个人资料与头像。

var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", result.AccessToken);
var me = await http.GetStringAsync("https://graph.microsoft.com/v1.0/me");

如果要扩展到日程表、发送邮件、Teams 通知等场景,需要添加对应的权限(例如 Mail.Send)并重新获取管理员同意。将内部应用的通知邮件统一交由 Graph 处理的设计思路,与「不依赖特定服务的中小企业群发邮件设计方法」中讨论的方向也是契合的。

7.3 保护自家 Web API —— 到 audience 与作用域的校验

如果桌面应用要调用自家的 Web API,需要在 API 一侧另外创建一个应用注册,公开类似 api://{API的客户端ID}/access_as_user 这样的作用域,桌面一侧则用该作用域请求令牌。API 一侧需要注意的关键点是,官方明确指出仅仅加上 [Authorize] 是不够的11 需要校验以下三层:

  1. 签名与发行者:是否由正确租户的 Entra ID 发行的 JWT(如果是 ASP.NET Core + Microsoft.Identity.Web,中间件会处理这一步)
  2. audience(aud:令牌的目标是不是这个 API 本身。不能让发给 Graph 的令牌被挪用到自家 API 上
  3. 作用域(scp 声明):是否包含预期的作用域。使用 Microsoft.Identity.Web 时可以用 [RequiredScope("access_as_user")] 属性声明11

省略第 2、3 步,就会造出”只要看起来像 Entra 发的令牌,谁都能通过的 API”。请将其与「Windows 应用开发安全性最低限度检查清单」中通信与输入校验的相关条目一起纳入设计评审范围。

8. 导入判断 —— 完全内部工具是否应该加入 Entra 认证

并不是所有内部应用都应该加入这套认证。下面给出一张判断表。

情况 建议 理由
已全公司引入 Microsoft 365 / Entra ID,且应用已有登录概念 导入 自建密码管理的负债整体消失,实现成本较低
应用需要调用自家 Web API 或云资源 导入 保护 API 离不开认证基础设施,比自行发明令牌方案更可靠
存在审计要求(记录谁在何时使用过、强制 MFA) 导入 登录日志、条件访问可在租户一侧统一管理
没有登录概念的单一功能工具(转换工具、查看器等) 不需要 没有添加认证的动机,Windows 登录本身已经足够
运行在完全离线环境(封闭的生产线、外带电脑)中 不可行或需专门设计 首次登录与令牌续期都离不开网络
尚未引入 Entra ID(仅有本地 AD,或仅使用 Google Workspace) 应考虑其他方案 前者可用 AD 认证(Windows 集成认证),后者更适合用 Google 一侧的机制

离线要求需要特别注意。只要缓存中的访问令牌仍在有效期内(经验值大约是一个多小时),AcquireTokenSilent 在离线状态下也能返回结果,但一旦过期,续期就需要网络。导入之前,需要结合实际业务的使用模式,确认这一有效期前提是否能满足业务需求。

9. 运维中的坑 —— 导入之后会遇到的咨询

导入并不是终点,运维阶段还会遇到一些常见的咨询。这里先列出来。

  • “昨天还能用,今天突然登录不上了”:首要怀疑对象是条件访问策略的变更。信息系统部门启用”阻止未注册设备”之类的设置后,应用本身什么都没改,登录却开始失败。最快的排查方式是在 Entra 管理中心的登录日志中查看对应用户的错误原因。采用 WAM 配置能提升对策略要求的适应能力,从而减少这类摩擦。5
  • “收到密钥即将过期的通知,这个应用有影响吗”:公共客户端本来就没有密钥也没有证书,所以不存在过期问题。如果收到这类咨询,往往是与机密客户端的应用注册搞混了,或者有人在公共客户端的注册上创建了不必要的密钥(后一种情况删掉即可)。结构上不可能发生因密钥过期而停摆的事故,这是这套架构一个隐藏的优点。
  • “首次启动时显示需要管理员批准”:这是 3.3 节提到的管理员同意遗漏。之后追加权限时,也会出现同样的提示,直到重新获取对应的同意为止。
  • “接入夜间批处理后不能用了”:如第 5.2 节所述,WAM 假定运行在交互式会话中。无人值守的处理不应挪用委托用户令牌,而应基于应用权限单独设计。
  • 分发与更新:MSAL 相关代码更新频繁,需要一套能把库更新分发到所有终端的机制。请结合「自动更新的安全设计」中讲到的更新通道校验一并考虑。

10. 总结

WinForms / WPF 应用集成 Entra ID 认证,可以归纳为以下 6 点。

  • 停止自建密码管理本身就是目的所在。保管、重置、离职人员处理、审计都会统一到租户一侧
  • 桌面应用是公共客户端,不能持有密钥,也不需要
  • ROPC 正走向弃用。不要再新建接收用户名密码的界面
  • 实现上唯一的选择是 MSAL.NET 的 AcquireTokenSilentAcquireTokenInteractive 模式
  • 在 Windows 上,用 WAM Broker(WithBroker 获得 SSO、条件访问、Windows Hello 支持
  • 从一开始就集成令牌缓存持久化(Extensions.Msal / DPAPI 保护)

如果采用”只替换登录”的最小配置(7.1 节),对现有应用的影响可以限定在登录界面和用户表附近,通常几天规模的改造就能完成。而一旦涉及条件访问或离线要求,就需要结合租户设置与业务实际情况做出设计判断。如果您对手上的应用该做到哪一步、该如何规划从自建认证的迁移步骤感到犹豫,我们可以提供帮助。

相关文章

相关咨询领域

合同会社小村软件(KomuraSoft LLC)处理为现有 WinForms / WPF 应用集成 Entra ID 认证(应用注册设计、MSAL.NET 实现、从自建认证迁移的规划)、自家 Web API 令牌校验的设计评审,以及与条件访问相关的登录故障排查等业务。

参考链接

  1. Microsoft Learn, Desktop app that calls web APIs: Code configuration。关于桌面应用的重定向 URI(移动和桌面平台、nativeclient / localhost),以及”允许公共客户端流程”设置的含义。  2 3

  2. Microsoft Learn, Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials。关于不应使用 ROPC、其与 MFA 不兼容而被拦截、依赖 ROPC 的应用将被拒之门外、桌面应用应迁移到基于 Broker 的认证。  2 3 4

  3. Microsoft Learn, Desktop app that calls web APIs: Acquire a token using username and password。关于用户名密码流程(ROPC)因安全风险而被弃用(deprecated)、迁移指南的说明,以及不支持 MFA、条件访问、SSO 的限制。  2

  4. Microsoft Learn, Get a token from the token cache using MSAL.NET。关于先调用 AcquireTokenSilent、遇到 MsalUiRequiredException 时回退到交互式的推荐模式、通过缓存与刷新令牌自动续期,以及通过删除账号清除缓存。  2 3 4

  5. Microsoft Learn, Using MSAL.NET with Web Account Manager (WAM)。关于 Broker 的优势(增强安全性、支持 Windows Hello / 条件访问 / FIDO、账号选择器、令牌保护)、MSAL.NET 4.52.0+ 与 Microsoft.Identity.Client.Broker 包、必须的 WithBroker 与父窗口句柄、ms-appx-web 重定向 URI、支持的操作系统与回退行为,以及必须的交互式会话等限制。  2 3 4 5 6 7 8 9

  6. Microsoft Learn, Token cache serialization。关于桌面应用推荐使用 Microsoft.Identity.Client.Extensions.Msal 的跨平台缓存、MsalCacheHelper 的用法,以及使用 ProtectedData(DPAPI、CurrentUser 范围)的自行序列化示例。  2 3

  7. Microsoft Learn, Register an application with the Microsoft identity platform。关于在 Entra 管理中心进行应用注册的步骤、支持账户类型的选择、获取客户端 ID,以及管理员同意。  2

  8. Microsoft Learn, Desktop app that calls web APIs: Acquire a token by using WAM。关于使用 WAM 时也需要令牌缓存持久化、使用 OperatingSystemAccount 的静默登录推荐模式,以及应用注册一侧的重定向 URI 配置。  2 3

  9. Microsoft Learn, Default reply URI。关于 WithDefaultRedirectUri 设置的重定向 URI 取决于平台(.NET Framework 桌面应用为 https://login.microsoftonline.com/common/oauth2/nativeclient,.NET Core 为 http://localhost)。  2

  10. Microsoft Learn, Using web browsers (MSAL.NET)。关于按框架划分的浏览器支持情况(.NET Framework 4.6.2+ 默认使用嵌入式浏览器,.NET 6+ 只使用系统浏览器)、系统浏览器需要 http://localhost 重定向 URI,以及通过 WithUseEmbeddedWebView 进行切换。 

  11. Microsoft Learn, Protected web API: Verify scopes and app roles。关于仅靠 [Authorize] 属性不够、需要校验 scp 声明(作用域),以及通过 Microsoft.Identity.Web 的 RequiredScope 属性进行声明式校验。  2

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

在 WinForms/WPF 应用中集成 Entra ID 认证有什么好处?
密码的保管、重置支持、离职人员账号停用、登录审计,都会变成租户一侧的工作,应用一侧的责任范围会大幅缩小。员工离职时,只要停用其 Entra ID 账号,就能立刻让其在所有应用上无法登录,多因素认证和条件访问也能直接作用于内部应用。实现上也只需要 MSAL.NET 这个库以及几十行代码。对于已经引入 Microsoft 365 的组织来说,几乎没有理由继续维护自建认证。
用自建界面接收用户名和密码进行认证的方式(ROPC)还能用吗?
应当视为禁止用于新开发。面向公共客户端的 ROPC 已被官方明确标注为「出于安全风险而弃用(deprecated)」,并公开了迁移指南。由于与 MFA、条件访问不兼容,租户中被强制要求 MFA 的用户会被直接拦截而无法登录。MFA 的强制启用是租户一侧随时可能发生的设置变更,即使现在能正常运行,也存在某天所有用户突然全部无法登录的风险。
应该使用 WAM Broker 吗?
在 Windows 上推荐使用。只需一行 WithBroker,就能获得与已登录 Windows 账号的单点登录(SSO)、对条件访问 / Windows Hello / FIDO 密钥的支持,以及刷新令牌与设备绑定的能力。只要公司电脑已加入 Entra,用户在启动应用后只需选择自己的 Windows 账号,无需输入密码即可完成登录。不过它只支持 Entra ID,在 Windows 服务、任务计划程序等没有交互式用户会话的场景下,会按设计报错,这一点需要注意。
为什么每次重新启动应用都会弹出登录界面?
这是因为没有对令牌缓存做持久化处理。MSAL.NET 的令牌缓存默认只存在于内存中,在桌面应用里持久化的实现是应用一侧的责任。官方推荐使用 Microsoft.Identity.Client.Extensions.Msal 包,在 Windows 上缓存会以加密形式保存。即使使用了 WAM,缓存持久化仍然是必要的,因为 ID 令牌和账号元数据仍需要保存下来。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表