You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 03:09:41