Blazor WebAssembly实现Azure AD与自定义双认证及报错修复
System.InvalidCastException: Specified cast is not valid异常的触发逻辑非常明确:Blazor WebAssembly的认证管道默认仅支持单实例AuthenticationStateProvider注册。直接并行注册自定义AuthenticationStateProvider、调用AddMsalAuthentication注册MSAL自带的认证提供方时,MSAL内部逻辑会尝试将DI容器中解析到的AuthenticationStateProvider实例强制转换为MsalAuthenticationStateProvider类型,此时拿到的是你注册的自定义Provider实例,类型不匹配直接抛出强制转换失败异常。
不要直接并行注册两个独立的AuthenticationStateProvider,而是实现一个聚合路由型的AuthenticationStateProvider作为全局唯一注册的认证状态提供方,内部根据用户选择的登录方式,分流调用MSAL认证逻辑或者自定义认证逻辑。
步骤1:清理冲突的服务注册
首先修改Program.cs的服务注册代码,删除原有直接注册自定义AuthenticationStateProvider、直接调用AddMsalAuthentication的代码,仅保留基础授权核心服务注册:
// Program.cs 基础服务注册部分 builder.Services.AddAuthorizationCore();
步骤2:实现聚合认证状态提供方
这个聚合Provider内部持有两套认证逻辑的处理实例,根据用户当前选择的登录模式切换返回对应的认证状态,同时对外暴露两套认证的登录、登出操作入口:
public class HybridAuthenticationStateProvider : AuthenticationStateProvider { // 标记当前生效的认证类型 public enum AuthMode { AzureAd, Custom } private AuthMode _currentAuthMode = AuthMode.Custom; // 内部持有MSAL认证状态提供方实例 private readonly MsalAuthenticationStateProvider _msalProvider; // 内部持有自定义认证状态提供方实例 private readonly CustomAuthenticationStateProvider _customProvider; public HybridAuthenticationStateProvider(IAccessTokenProviderAccessor tokenAccessor, NavigationManager navigation, IOptionsSnapshot<RemoteAuthenticationOptions<MsalProviderOptions>> msalOptions, // 注入自定义认证需要的依赖,比如本地存储服务 ILocalStorageService localStorage) { // 初始化两个内部认证提供方,不注册到DI容器避免冲突 _msalProvider = new MsalAuthenticationStateProvider(tokenAccessor, navigation, msalOptions); _customProvider = new CustomAuthenticationStateProvider(localStorage); // 监听两个内部提供方的认证状态变化,符合当前认证模式时向外转发事件 _msalProvider.AuthenticationStateChanged += async (task) => { if (_currentAuthMode == AuthMode.AzureAd) { NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }; _customProvider.AuthenticationStateChanged += async (task) => { if (_currentAuthMode == AuthMode.Custom) { NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }; // 初始化时可从本地存储读取用户上次选择的认证模式,避免刷新后重置 var savedMode = localStorage.GetItemAsync<string>("currentAuthMode").Result; if (!string.IsNullOrEmpty(savedMode) && Enum.TryParse<AuthMode>(savedMode, out var mode)) { _currentAuthMode = mode; } } // 切换认证模式 private void SwitchAuthMode(AuthMode mode) { _currentAuthMode = mode; // 持久化用户选择的认证模式 _customProvider.SaveAuthModeToLocal(mode).Wait(); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { // 根据当前认证模式返回对应提供方的认证状态 return _currentAuthMode switch { AuthMode.AzureAd => await _msalProvider.GetAuthenticationStateAsync(), AuthMode.Custom => await _customProvider.GetAuthenticationStateAsync(), _ => await _customProvider.GetAuthenticationStateAsync() }; } // Azure AD登录入口 public async Task<RemoteAuthenticationResult> SignInAzureAd() { SwitchAuthMode(AuthMode.AzureAd); return await _msalProvider.SignInAsync(new InteractiveRequestOptions { Interaction = InteractionType.SignIn, ReturnUrl = "/" }); } // Azure AD登出入口 public async Task SignOutAzureAd() { await _msalProvider.SignOutAsync(); } // 自定义认证登录入口 public async Task SignInCustom(string username, string password) { SwitchAuthMode(AuthMode.Custom); await _customProvider.SignIn(username, password); } // 自定义认证登出入口 public async Task SignOutCustom() { await _customProvider.SignOut(); } }
注意:两个内部使用的
MsalAuthenticationStateProvider和CustomAuthenticationStateProvider不要单独注册到DI容器,所有对这两个Provider的调用都通过聚合层转发,避免DI容器解析到非预期的Provider实例再次触发类型转换错误。
步骤3:注册聚合提供方为唯一认证状态源
回到Program.cs,手动注册MSAL需要的底层依赖服务(不调用会自动注册自带Provider的AddMsalAuthentication扩展方法),再将聚合Provider注册为全局唯一的认证状态提供方:
// Program.cs 服务注册 // 注册MSAL底层依赖,不注册它自带的AuthenticationStateProvider builder.Services.AddRemoteAuthenticationServices<MsalAuthenticationStateProvider, MsalAuthenticationOptions, RemoteAuthenticationState>() .AddMsal(options => { options.ProviderOptions.Authority = "你的Azure AD Authority地址"; options.ProviderOptions.ClientId = "你的Azure AD客户端ID"; options.ProviderOptions.DefaultScopes.Add("需要申请的API权限范围"); }); // 注册聚合认证提供方为唯一的AuthenticationStateProvider builder.Services.AddScoped<AuthenticationStateProvider, HybridAuthenticationStateProvider>(); builder.Services.AddScoped(sp => (HybridAuthenticationStateProvider)sp.GetRequiredService<AuthenticationStateProvider>());
步骤4:登录页实现认证方式选择
在登录页面添加两个登录入口,分别触发Azure AD登录和自定义账号密码登录即可:
@page "/login" @inject HybridAuthenticationStateProvider AuthProvider @inject NavigationManager Navigation <div class="login-box"> <button @onclick="SignInWithAzureAd" class="btn btn-primary">使用Azure AD登录</button> <button @onclick="ShowCustomLoginForm" class="btn btn-secondary">使用账号密码登录</button> @if (showCustomForm) { <div class="custom-form"> <input @bind="username" placeholder="请输入用户名" /> <input @bind="password" type="password" placeholder="请输入密码" /> <button @onclick="SignInWithCustom" class="btn btn-success">登录</button> </div> } </div> @code { private bool showCustomForm; private string username; private string password; private async Task SignInWithAzureAd() { await AuthProvider.SignInAzureAd(); } private void ShowCustomLoginForm() { showCustomForm = true; } private async Task SignInWithCustom() { await AuthProvider.SignInCustom(username, password); Navigation.NavigateTo("/"); } }
内容的提问来源于stack exchange,提问作者Reishabh

