ASP.NET Core Blazor Hybrid 身份验证和授权

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

本文介绍 ASP.NET Core 对 Blazor Hybrid 应用中的安全配置和管理及 ASP.NET Core Identity 的支持。

Blazor Hybrid 应用中的身份验证由本机平台库处理,因为后者提供了浏览器沙盒无法给予的经过增强的安全保证。 本机应用的身份验证使用特定于操作系统的机制或通过联合协议,如 OpenID Connect (OIDC)。 按照针对应用选择的 identity 提供程序指南进行操作,然后使用本文中的指南进一步将 identity 与 Blazor 集成。

集成身份验证必须为 Razor 组件和服务实现以下目标:

将身份验证添加到 .NET MAUI 后,WPF 或 Windows 窗体应用和用户能够成功登录和注销,将身份验证与 Blazor 集成,以使经过身份验证的用户用使用 Razor 组件和服务。 执行以下步骤:

.NET MAUI 应用使用 Xamarin.Essentials:Web 验证器WebAuthenticator 类允许应用启动基于浏览器的身份验证流,这些流侦听注册到应用的特定 URL 的回调。

要获得更多指南,请查看下面的资源:

Windows 窗体应用使用 Microsoft identity 平台与 Microsoft Entra (ME-ID) 和 AAD B2C 集成。 有关更多信息,请参阅 Microsoft 身份验证库 (MSAL) 概述

在不使用用户更改更新的情况下创建自定义 AuthenticationStateProvider

如果应用在应用启动后立即对用户进行身份验证,并且经过身份验证的用户在整个应用生存期内保持不变,则无需发送用户更改通知,并且该应用仅提供有关经过身份验证的用户的信息。 在此方案中,用户在打开应用时登录到应用,并在用户注销后再次显示登录屏幕。下面的 ExternalAuthStateProvider 是此身份验证方案的自定义 AuthenticationStateProvider 的示例实现。

注意

以下自定义 AuthenticationStateProvider 未声明命名空间,以便使代码示例适用于任何 Blazor Hybrid 应用。 但是,最佳做法是在生产应用中实现示例时提供应用的命名空间。

ExternalAuthStateProvider.cs:

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private readonly Task<AuthenticationState> authenticationState;

    public ExternalAuthStateProvider(AuthenticatedUser user) => 
        authenticationState = Task.FromResult(new AuthenticationState(user.Principal));

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        authenticationState;
}

public class AuthenticatedUser
{
    public ClaimsPrincipal Principal { get; set; } = new();
}

以下步骤介绍了如何:

  • 添加所需的命名空间。
  • 将授权服务和 Blazor 抽象添加到服务集合。
  • 生成服务集合。
  • 解析 AuthenticatedUser 服务以设置经过身份验证的用户的声明主体。 有关详细信息,请参阅 identity 提供程序的文档。
  • 返回生成的主机。

MauiProgram.csMauiProgram.CreateMauiApp 方法中,为 Microsoft.AspNetCore.Components.AuthorizationSystem.Security.Claims 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;

删除以下返回生成的 Microsoft.Maui.Hosting.MauiApp 的代码行:

- return builder.Build();

用下面的代码替换前面的代码行。 添加 OpenID/MSAL 代码以对用户进行身份验证。 有关详细信息,请参阅 identity 提供程序的文档。

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();

var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

return host;

以下步骤介绍了如何:

  • 添加所需的命名空间。
  • 将授权服务和 Blazor 抽象添加到服务集合。
  • 生成服务集合,并将生成的服务集合作为资源添加到应用的 ResourceDictionary 中。
  • 解析 AuthenticatedUser 服务以设置经过身份验证的用户的声明主体。 有关详细信息,请参阅 identity 提供程序的文档。
  • 返回生成的主机。

MainWindow 的构造函数 (MainWindow.xaml.cs) 中,为 Microsoft.AspNetCore.Components.AuthorizationSystem.Security.Claims 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;

删除以下用于将生成的服务集合作为资源添加到应用的 ResourceDictionary 的代码行:

- Resources.Add("services", serviceCollection.BuildServiceProvider());

用下面的代码替换前面的代码行。 添加 OpenID/MSAL 代码以对用户进行身份验证。 有关详细信息,请参阅 identity 提供程序的文档。

serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<AuthenticatedUser>();
var services = serviceCollection.BuildServiceProvider();
Resources.Add("services", services);

var authenticatedUser = services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

以下步骤介绍了如何:

  • 添加所需的命名空间。
  • 将授权服务和 Blazor 抽象添加到服务集合。
  • 生成服务集合,并将生成的服务集合添加到应用的服务提供商。
  • 解析 AuthenticatedUser 服务以设置经过身份验证的用户的声明主体。 有关详细信息,请参阅 identity 提供程序的文档。

Form1 的构造函数 (Form1.cs) 中,为 Microsoft.AspNetCore.Components.AuthorizationSystem.Security.Claims 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;

删除以下用于将生成的服务集合设置为应用的服务提供商的代码行:

- blazorWebView1.Services = services.BuildServiceProvider();

用下面的代码替换前面的代码行。 添加 OpenID/MSAL 代码以对用户进行身份验证。 有关详细信息,请参阅 identity 提供程序的文档。

services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<AuthenticatedUser>();
var serviceCollection = services.BuildServiceProvider();
blazorWebView1.Services = serviceCollection;

var authenticatedUser = serviceCollection.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

在使用用户更改更新的情况下创建自定义 AuthenticationStateProvider

若要在 Blazor 应用运行时更新用户,请使用以下任一方法在 AuthenticationStateProvider 实现中调用 NotifyAuthenticationStateChanged

BlazorWebView 外发出身份验证更新信号(选项 1)

自定义 AuthenticationStateProvider 可以使用全局服务来发出身份验证更新的信号。 我们建议此服务提供一个 AuthenticationStateProvider 可以订阅的事件,该事件会调用 NotifyAuthenticationStateChanged

注意

以下自定义 AuthenticationStateProvider 未声明命名空间,以便使代码示例适用于任何 Blazor Hybrid 应用。 但是,最佳做法是在生产应用中实现示例时提供应用的命名空间。

ExternalAuthStateProvider.cs:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState currentUser;

    public ExternalAuthStateProvider(ExternalAuthService service)
    {
        currentUser = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            currentUser = new AuthenticationState(newUser);
            NotifyAuthenticationStateChanged(Task.FromResult(currentUser));
        };
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(currentUser);
}

public class ExternalAuthService
{
    public event Action<ClaimsPrincipal>? UserChanged;
    private ClaimsPrincipal? currentUser;

    public ClaimsPrincipal CurrentUser
    {
        get { return currentUser ?? new(); }
        set
        {
            currentUser = value;

            if (UserChanged is not null)
            {
                UserChanged(currentUser);
            }
        }
    }
}

MauiProgram.csMauiProgram.CreateMauiApp 方法中,为 Microsoft.AspNetCore.Components.Authorization 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;

将授权服务和 Blazor 抽象添加到服务集合:

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<ExternalAuthService>();

MainWindow 的构造函数 (MainWindow.xaml.cs) 中,为 Microsoft.AspNetCore.Components.Authorization 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;

将授权服务和 Blazor 抽象添加到服务集合:

serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<ExternalAuthService>();

Form1 的构造函数 (Form1.cs) 中,为 Microsoft.AspNetCore.Components.Authorization 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;

将授权服务和 Blazor 抽象添加到服务集合:

services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<ExternalAuthService>();

无论应用在何处对用户进行身份验证,请解析 ExternalAuthService 服务:

var authService = host.Services.GetRequiredService<ExternalAuthService>();

执行自定义 OpenID/MSAL 代码以对用户进行身份验证。 有关详细信息,请参阅 identity 提供程序的文档。 经过身份验证的用户(以下示例中的 authenticatedUser)是基于新 ClaimsIdentity 的新 ClaimsPrincipal

将当前用户设置为经过身份验证的用户:

authService.CurrentUser = authenticatedUser;

上述方法的替代方法是基于 System.Threading.Thread.CurrentPrincipal 设置用户的主体,而不是通过服务进行设置,从而避免使用依赖项注入容器:

public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(
            new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? 
                new ClaimsPrincipal(new ClaimsIdentity())));
}

使用替代方法,仅向服务集合中添加授权服务 (AddAuthorizationCore) 和 CurrentThreadUserAuthenticationStateProvider (.TryAddScoped<AuthenticationStateProvider, CurrentThreadUserAuthenticationStateProvider>())。

BlazorWebView 中处理身份验证(选项 2)

自定义 AuthenticationStateProvider 可以包含用于触发登录和注销及更新用户的其他方法。

注意

以下自定义 AuthenticationStateProvider 未声明命名空间,以便使代码示例适用于任何 Blazor Hybrid 应用。 但是,最佳做法是在生产应用中实现示例时提供应用的命名空间。

ExternalAuthStateProvider.cs:

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(new AuthenticationState(currentUser));

    public Task LogInAsync()
    {
        var loginTask = LogInAsyncCore();
        NotifyAuthenticationStateChanged(loginTask);

        return loginTask;

        async Task<AuthenticationState> LogInAsyncCore()
        {
            var user = await LoginWithExternalProviderAsync();
            currentUser = user;

            return new AuthenticationState(currentUser);
        }
    }

    private Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
    {
        /*
            Provide OpenID/MSAL code to authenticate the user. See your identity 
            provider's documentation for details.

            Return a new ClaimsPrincipal based on a new ClaimsIdentity.
        */
        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity());

        return Task.FromResult(authenticatedUser);
    }

    public void Logout()
    {
        currentUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(currentUser)));
    }
}

在上面的示例中:

  • 调用 LogInAsyncCore 触发登录过程。
  • 调用 NotifyAuthenticationStateChanged 通知更新正在进行中,这允许应用在登录或注销过程中提供临时 UI。
  • 返回 loginTask 会返回任务,以便触发登录的组件可以等待并在任务完成后做出回应。
  • LoginWithExternalProviderAsync 方法由开发人员实现,以使用 identity 提供程序的 SDK 登录用户。 有关更多信息,请参阅 identity 提供程序的文档。 经过身份验证的用户 (authenticatedUser) 是基于新 ClaimsIdentity 的新 ClaimsPrincipal

MauiProgram.csMauiProgram.CreateMauiApp 方法中,将授权服务和 Blazor 抽象添加到服务集合:

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

MainWindow 的构造函数 (MainWindow.xaml.cs) 中,将授权服务和 Blazor 抽象添加到服务集合:

serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

Form1 的构造函数 (Form1.cs) 中,将授权服务和 Blazor 抽象添加到服务集合:

services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

以下 LoginComponent 组件演示了如何实现用户登录。 在典型应用中,仅当用户未登录到应用时,LoginComponent 组件才会显示在父组件中。

Shared/LoginComponent.razor:

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Login">Log in</button>

@code
{
    public async Task Login()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .LogInAsync();
    }
}

以下 LogoutComponent 组件演示了如何实现用户注销。 在典型应用中,仅当用户登录到应用时,LogoutComponent 组件才会显示在父组件中。

Shared/LogoutComponent.razor:

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Logout">Log out</button>

@code
{
    public async Task Logout()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .Logout();
    }
}

访问其他身份验证信息

Blazor 未定义处理其他凭据的抽象,例如用于对 Web API 发送 HTTP 请求的访问令牌。 我们建议遵循 identity 提供者的指导,使用 identity 提供者的 SDK 提供的基元管理用户的凭证。

identity 提供者 SDK 通常会对存储在设备中的用户凭据使用令牌存储。 如果将 SDK 的令牌存储基元添加到服务容器,请在应用中使用 SDK 的基元。

框架 Blazor 既不知晓用户的身份验证凭据,也不会以任何方式与凭据进行交互,因此应用的代码可随意遵循你认为最方便的任何方法。 但是,在应用中执行身份验证代码时,请遵循下一部分其他身份验证安全注意事项中的一般安全指南要求。

其他身份验证安全注意事项

身份验证过程在 Blazor 的外部,我们建议开发人员访问 identity 提供者指南以了解其他安全指导意见。

进行身份验证时:

  • 避免在 Web View 的上下文中进行身份验证。 例如,避免使用 JavaScript OAuth 库来执行身份验证流。 在单页应用中,身份验证令牌不是隐藏在 JavaScript 中的,可被恶意用户轻松发现并用于恶意目的。 本机应用不会遭受此风险,因为本机应用只能在浏览器上下文之外获取令牌,这意味着第三方恶意脚本无法窃取令牌并危及应用安全。
  • 避免自行执行身份验证工作流。 在大多数情况下,平台库将使用系统的浏览器(而不是使用容易遭到劫持的自定义 Web View)安全地处理身份验证工作流。
  • 避免使用平台的 Web View 控件来执行身份验证。 相反,请尽可能使用系统的浏览器。
  • 避免将令牌传递到文档上下文 (JavaScript)。 在某些情况下,文档中的 JavaScript 库需要执行对外部服务的授权调用。 无需通过 JS 互操作向 JavaScript 提供令牌:
    • 向库和 Web View 内部提供生成的临时令牌。
    • 截获代码中的传出网络请求。
    • 将临时令牌替换为实际令牌,并确认请求的目标有效。

其他资源

Blazor Hybrid 应用中的身份验证由本机平台库处理,因为后者提供了浏览器沙盒无法给予的经过增强的安全保证。 本机应用的身份验证使用特定于操作系统的机制或通过联合协议,如 OpenID Connect (OIDC)。 按照针对应用选择的 identity 提供程序指南进行操作,然后使用本文中的指南进一步将 identity 与 Blazor 集成。

集成身份验证必须为 Razor 组件和服务实现以下目标:

将身份验证添加到 .NET MAUI 后,WPF 或 Windows 窗体应用和用户能够成功登录和注销,将身份验证与 Blazor 集成,以使经过身份验证的用户用使用 Razor 组件和服务。 执行以下步骤:

.NET MAUI 应用使用 Xamarin.Essentials:Web 验证器WebAuthenticator 类允许应用启动基于浏览器的身份验证流,这些流侦听注册到应用的特定 URL 的回调。

要获得更多指南,请查看下面的资源:

Windows 窗体应用使用 Microsoft identity 平台与 Microsoft Entra (ME-ID) 和 AAD B2C 集成。 有关更多信息,请参阅 Microsoft 身份验证库 (MSAL) 概述

在不使用用户更改更新的情况下创建自定义 AuthenticationStateProvider

如果应用在应用启动后立即对用户进行身份验证,并且经过身份验证的用户在整个应用生存期内保持不变,则无需发送用户更改通知,并且该应用仅提供有关经过身份验证的用户的信息。 在此方案中,用户在打开应用时登录到应用,并在用户注销后再次显示登录屏幕。下面的 ExternalAuthStateProvider 是此身份验证方案的自定义 AuthenticationStateProvider 的示例实现。

注意

以下自定义 AuthenticationStateProvider 未声明命名空间,以便使代码示例适用于任何 Blazor Hybrid 应用。 但是,最佳做法是在生产应用中实现示例时提供应用的命名空间。

ExternalAuthStateProvider.cs:

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private readonly Task<AuthenticationState> authenticationState;

    public ExternalAuthStateProvider(AuthenticatedUser user) => 
        authenticationState = Task.FromResult(new AuthenticationState(user.Principal));

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        authenticationState;
}

public class AuthenticatedUser
{
    public ClaimsPrincipal Principal { get; set; } = new();
}

以下步骤介绍了如何:

  • 添加所需的命名空间。
  • 将授权服务和 Blazor 抽象添加到服务集合。
  • 生成服务集合。
  • 解析 AuthenticatedUser 服务以设置经过身份验证的用户的声明主体。 有关详细信息,请参阅 identity 提供程序的文档。
  • 返回生成的主机。

MauiProgram.csMauiProgram.CreateMauiApp 方法中,为 Microsoft.AspNetCore.Components.AuthorizationSystem.Security.Claims 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;

删除以下返回生成的 Microsoft.Maui.Hosting.MauiApp 的代码行:

- return builder.Build();

用下面的代码替换前面的代码行。 添加 OpenID/MSAL 代码以对用户进行身份验证。 有关详细信息,请参阅 identity 提供程序的文档。

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();

var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

return host;

以下步骤介绍了如何:

  • 添加所需的命名空间。
  • 将授权服务和 Blazor 抽象添加到服务集合。
  • 生成服务集合,并将生成的服务集合作为资源添加到应用的 ResourceDictionary 中。
  • 解析 AuthenticatedUser 服务以设置经过身份验证的用户的声明主体。 有关详细信息,请参阅 identity 提供程序的文档。
  • 返回生成的主机。

MainWindow 的构造函数 (MainWindow.xaml.cs) 中,为 Microsoft.AspNetCore.Components.AuthorizationSystem.Security.Claims 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;

删除以下用于将生成的服务集合作为资源添加到应用的 ResourceDictionary 的代码行:

- Resources.Add("services", serviceCollection.BuildServiceProvider());

用下面的代码替换前面的代码行。 添加 OpenID/MSAL 代码以对用户进行身份验证。 有关详细信息,请参阅 identity 提供程序的文档。

serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<AuthenticatedUser>();
var services = serviceCollection.BuildServiceProvider();
Resources.Add("services", services);

var authenticatedUser = services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

以下步骤介绍了如何:

  • 添加所需的命名空间。
  • 将授权服务和 Blazor 抽象添加到服务集合。
  • 生成服务集合,并将生成的服务集合添加到应用的服务提供商。
  • 解析 AuthenticatedUser 服务以设置经过身份验证的用户的声明主体。 有关详细信息,请参阅 identity 提供程序的文档。

Form1 的构造函数 (Form1.cs) 中,为 Microsoft.AspNetCore.Components.AuthorizationSystem.Security.Claims 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;

删除以下用于将生成的服务集合设置为应用的服务提供商的代码行:

- blazorWebView1.Services = services.BuildServiceProvider();

用下面的代码替换前面的代码行。 添加 OpenID/MSAL 代码以对用户进行身份验证。 有关详细信息,请参阅 identity 提供程序的文档。

services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<AuthenticatedUser>();
var serviceCollection = services.BuildServiceProvider();
blazorWebView1.Services = serviceCollection;

var authenticatedUser = serviceCollection.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

在使用用户更改更新的情况下创建自定义 AuthenticationStateProvider

若要在 Blazor 应用运行时更新用户,请使用以下任一方法在 AuthenticationStateProvider 实现中调用 NotifyAuthenticationStateChanged

BlazorWebView 外发出身份验证更新信号(选项 1)

自定义 AuthenticationStateProvider 可以使用全局服务来发出身份验证更新的信号。 我们建议此服务提供一个 AuthenticationStateProvider 可以订阅的事件,该事件会调用 NotifyAuthenticationStateChanged

注意

以下自定义 AuthenticationStateProvider 未声明命名空间,以便使代码示例适用于任何 Blazor Hybrid 应用。 但是,最佳做法是在生产应用中实现示例时提供应用的命名空间。

ExternalAuthStateProvider.cs:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState currentUser;

    public ExternalAuthStateProvider(ExternalAuthService service)
    {
        currentUser = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            currentUser = new AuthenticationState(newUser);
            NotifyAuthenticationStateChanged(Task.FromResult(currentUser));
        };
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(currentUser);
}

public class ExternalAuthService
{
    public event Action<ClaimsPrincipal>? UserChanged;
    private ClaimsPrincipal? currentUser;

    public ClaimsPrincipal CurrentUser
    {
        get { return currentUser ?? new(); }
        set
        {
            currentUser = value;

            if (UserChanged is not null)
            {
                UserChanged(currentUser);
            }
        }
    }
}

MauiProgram.csMauiProgram.CreateMauiApp 方法中,为 Microsoft.AspNetCore.Components.Authorization 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;

将授权服务和 Blazor 抽象添加到服务集合:

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<ExternalAuthService>();

MainWindow 的构造函数 (MainWindow.xaml.cs) 中,为 Microsoft.AspNetCore.Components.Authorization 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;

将授权服务和 Blazor 抽象添加到服务集合:

serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<ExternalAuthService>();

Form1 的构造函数 (Form1.cs) 中,为 Microsoft.AspNetCore.Components.Authorization 添加命名空间:

using Microsoft.AspNetCore.Components.Authorization;

将授权服务和 Blazor 抽象添加到服务集合:

services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<ExternalAuthService>();

无论应用在何处对用户进行身份验证,请解析 ExternalAuthService 服务:

var authService = host.Services.GetRequiredService<ExternalAuthService>();

执行自定义 OpenID/MSAL 代码以对用户进行身份验证。 有关详细信息,请参阅 identity 提供程序的文档。 经过身份验证的用户(以下示例中的 authenticatedUser)是基于新 ClaimsIdentity 的新 ClaimsPrincipal

将当前用户设置为经过身份验证的用户:

authService.CurrentUser = authenticatedUser;

上述方法的替代方法是基于 System.Threading.Thread.CurrentPrincipal 设置用户的主体,而不是通过服务进行设置,从而避免使用依赖项注入容器:

public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(
            new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? 
                new ClaimsPrincipal(new ClaimsIdentity())));
}

使用替代方法,仅向服务集合中添加授权服务 (AddAuthorizationCore) 和 CurrentThreadUserAuthenticationStateProvider (.AddScoped<AuthenticationStateProvider, CurrentThreadUserAuthenticationStateProvider>())。

BlazorWebView 中处理身份验证(选项 2)

自定义 AuthenticationStateProvider 可以包含用于触发登录和注销及更新用户的其他方法。

注意

以下自定义 AuthenticationStateProvider 未声明命名空间,以便使代码示例适用于任何 Blazor Hybrid 应用。 但是,最佳做法是在生产应用中实现示例时提供应用的命名空间。

ExternalAuthStateProvider.cs:

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(new AuthenticationState(currentUser));

    public Task LogInAsync()
    {
        var loginTask = LogInAsyncCore();
        NotifyAuthenticationStateChanged(loginTask);

        return loginTask;

        async Task<AuthenticationState> LogInAsyncCore()
        {
            var user = await LoginWithExternalProviderAsync();
            currentUser = user;

            return new AuthenticationState(currentUser);
        }
    }

    private Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
    {
        /*
            Provide OpenID/MSAL code to authenticate the user. See your identity 
            provider's documentation for details.

            Return a new ClaimsPrincipal based on a new ClaimsIdentity.
        */
        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity());

        return Task.FromResult(authenticatedUser);
    }

    public void Logout()
    {
        currentUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(currentUser)));
    }
}

在上面的示例中:

  • 调用 LogInAsyncCore 触发登录过程。
  • 调用 NotifyAuthenticationStateChanged 通知更新正在进行中,这允许应用在登录或注销过程中提供临时 UI。
  • 返回 loginTask 会返回任务,以便触发登录的组件可以等待并在任务完成后做出回应。
  • LoginWithExternalProviderAsync 方法由开发人员实现,以使用 identity 提供程序的 SDK 登录用户。 有关更多信息,请参阅 identity 提供程序的文档。 经过身份验证的用户 (authenticatedUser) 是基于新 ClaimsIdentity 的新 ClaimsPrincipal

MauiProgram.csMauiProgram.CreateMauiApp 方法中,将授权服务和 Blazor 抽象添加到服务集合:

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

MainWindow 的构造函数 (MainWindow.xaml.cs) 中,将授权服务和 Blazor 抽象添加到服务集合:

serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

Form1 的构造函数 (Form1.cs) 中,将授权服务和 Blazor 抽象添加到服务集合:

services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

以下 LoginComponent 组件演示了如何实现用户登录。 在典型应用中,仅当用户未登录到应用时,LoginComponent 组件才会显示在父组件中。

Shared/LoginComponent.razor:

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Login">Log in</button>

@code
{
    public async Task Login()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .LogInAsync();
    }
}

以下 LogoutComponent 组件演示了如何实现用户注销。 在典型应用中,仅当用户登录到应用时,LogoutComponent 组件才会显示在父组件中。

Shared/LogoutComponent.razor:

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Logout">Log out</button>

@code
{
    public async Task Logout()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .Logout();
    }
}

访问其他身份验证信息

Blazor 未定义处理其他凭据的抽象,例如用于对 Web API 发送 HTTP 请求的访问令牌。 我们建议遵循 identity 提供者的指导,使用 identity 提供者的 SDK 提供的基元管理用户的凭证。

identity 提供者 SDK 通常会对存储在设备中的用户凭据使用令牌存储。 如果将 SDK 的令牌存储基元添加到服务容器,请在应用中使用 SDK 的基元。

框架 Blazor 既不知晓用户的身份验证凭据,也不会以任何方式与凭据进行交互,因此应用的代码可随意遵循你认为最方便的任何方法。 但是,在应用中执行身份验证代码时,请遵循下一部分其他身份验证安全注意事项中的一般安全指南要求。

其他身份验证安全注意事项

身份验证过程在 Blazor 的外部,我们建议开发人员访问 identity 提供者指南以了解其他安全指导意见。

进行身份验证时:

  • 避免在 Web View 的上下文中进行身份验证。 例如,避免使用 JavaScript OAuth 库来执行身份验证流。 在单页应用中,身份验证令牌不是隐藏在 JavaScript 中的,可被恶意用户轻松发现并用于恶意目的。 本机应用不会遭受此风险,因为本机应用只能在浏览器上下文之外获取令牌,这意味着第三方恶意脚本无法窃取令牌并危及应用安全。
  • 避免自行执行身份验证工作流。 在大多数情况下,平台库将使用系统的浏览器(而不是使用容易遭到劫持的自定义 Web View)安全地处理身份验证工作流。
  • 避免使用平台的 Web View 控件来执行身份验证。 相反,请尽可能使用系统的浏览器。
  • 避免将令牌传递到文档上下文 (JavaScript)。 在某些情况下,文档中的 JavaScript 库需要执行对外部服务的授权调用。 无需通过 JS 互操作向 JavaScript 提供令牌:
    • 向库和 Web View 内部提供生成的临时令牌。
    • 截获代码中的传出网络请求。
    • 将临时令牌替换为实际令牌,并确认请求的目标有效。

其他资源