.NET Maui中GetAccountsAsync返回null的MSAL B2B认证问题求助
我开发的.NET Maui应用采用MSAL B2B认证,用户名密码登录功能正常。但在应用启动时,我尝试检查缓存中是否存储了有效访问令牌,发现代码行var accounts = await _application.GetAccountsAsync();返回null,导致AcquireTokenSilent方法无法正常执行。_application.GetAccountsAsync().IsFaulted为false。以下是我的认证流程代码:
private static IPublicClientApplication _application; private static AuthenticationResult _authenticationResult; private static void BuildApplication() { _application = PublicClientApplicationBuilder.Create(B2BConstants.ClientId) .WithTenantId(B2BConstants.TenantId) .WithAuthority(B2BConstants.Authority) .WithRedirectUri("http://localhost") .Build(); } /// <summary> /// MSAL Authenticate Silent with token cache. /// </summary> /// <returns>AccessToken as String</returns> public static async Task<string> AuthenticateSilentAsync() { if (_application == null) BuildApplication(); var accounts = await _application.GetAccountsAsync(); _authenticationResult = await _application.AcquireTokenSilent(B2BConstants.Scopes, accounts.FirstOrDefault()) .ExecuteAsync(); return _authenticationResult.AccessToken; } public static async Task<string> AuthenticateAsync(string username, string password) { BuildApplication(); _authenticationResult = null; try { return await AuthenticateSilentAsync(); } catch (MsalUiRequiredException ex) { try { _authenticationResult = await _application.AcquireTokenByUsernamePassword(B2BConstants.Scopes, username, password) .WithTenantId(B2BConstants.TenantId) .ExecuteAsync(); return _authenticationResult.AccessToken; } catch (MsalException msalex) { return null; } } catch (Exception ex) { return null; } }
我尝试通过GetAccountsAsync获取账户以传入AcquireTokenSilent,但返回的账户始终为null。请问这是什么原因?是否需要手动将令牌写入缓存,还是会自动完成?有没有替代方案?
原因分析
- 默认缓存仅存于内存:MSAL的默认令牌缓存是内存级别的,仅在当前应用进程存活期间有效。应用重启后,内存缓存被清空,自然获取不到之前的账户信息。
- 无持久化缓存配置:MSAL不会自动处理跨进程/重启的缓存持久化,需要手动实现缓存的序列化与本地存储绑定,才能在应用重启后恢复账户和令牌数据。
解决步骤
1. 实现持久化令牌缓存
针对.NET Maui,我们可以通过ITokenCacheSerializer相关事件,将缓存数据序列化到本地存储(比如Preferences或SecureStorage,敏感场景推荐后者)。
添加缓存序列化工具类:
public class TokenCachePersistence { private const string CacheStorageKey = "MSAL_B2B_TokenCache"; public static void AttachToClientApp(IPublicClientApplication app) { app.UserTokenCache.SetBeforeAccess(LoadCacheFromStorage); app.UserTokenCache.SetAfterAccess(SaveCacheToStorage); } private static void LoadCacheFromStorage(TokenCacheNotificationArgs args) { // 从本地存储读取缓存并反序列化 var cachedData = SecureStorage.GetAsync(CacheStorageKey).Result; if (!string.IsNullOrEmpty(cachedData)) { args.TokenCache.DeserializeMsalV3(Convert.FromBase64String(cachedData)); } } private static void SaveCacheToStorage(TokenCacheNotificationArgs args) { // 缓存状态变更时,序列化并写入本地存储 if (args.HasStateChanged) { var serializedCache = Convert.ToBase64String(args.TokenCache.SerializeMsalV3()); SecureStorage.SetAsync(CacheStorageKey, serializedCache).Wait(); } } }
2. 修改应用初始化逻辑
在BuildApplication中绑定缓存持久化逻辑:
private static void BuildApplication() { _application = PublicClientApplicationBuilder.Create(B2BConstants.ClientId) .WithTenantId(B2BConstants.TenantId) .WithAuthority(B2BConstants.Authority) .WithRedirectUri("http://localhost") .Build(); // 绑定持久化缓存 TokenCachePersistence.AttachToClientApp(_application); }
3. 修复静默登录的空引用问题
在AuthenticateSilentAsync中先判断账户是否存在,避免空引用并正确触发交互式登录:
public static async Task<string> AuthenticateSilentAsync() { if (_application == null) BuildApplication(); var accounts = await _application.GetAccountsAsync(); var targetAccount = accounts.FirstOrDefault(); if (targetAccount == null) { // 无缓存账户,抛出异常触发用户名密码登录流程 throw new MsalUiRequiredException(MsalError.NoAccountsFound); } _authenticationResult = await _application.AcquireTokenSilent(B2BConstants.Scopes, targetAccount) .ExecuteAsync(); return _authenticationResult.AccessToken; }
替代方案
如果暂时不想实现持久化缓存,可以在应用启动时读取本地存储的用户名(比如用Preferences保存上次登录的用户名),直接调用AcquireTokenByUsernamePassword完成登录。这种方式本质是重新获取令牌,效率不如缓存持久化,但能绕过GetAccountsAsync为空的问题。
内容的提问来源于stack exchange,提问作者Andreas

