如何确保Blazor WebAssembly应用程序的安全

362 阅读15分钟

Blazor允许你利用.NET平台来构建你的WebAssembly(也称为WASM)应用程序。由于有了Auth0,你还可以通过添加对认证和授权的支持来轻松地保护它们,正如本文所显示的。让我们开始吧!

构建一个Blazor WebAssembly应用程序

在前一篇文章中,你通过使用服务器托管模式建立了一个Blazor应用程序。那是一个简单的测验应用程序,它显示了一个有多个答案的问题列表,并根据你提供的正确答案给你打分。

现在你要通过使用WebAssembly托管模型来实现同样的应用程序。要了解更多关于Blazor托管模型的信息,请查看那篇提到的文章中的具体章节。

正如那篇文章所解释的,WebAssembly托管模式使你的应用程序被编译成WebAssembly并在浏览器中运行。然而,根据你项目的结构,你有两个选择来创建你的应用程序。

  • 你可能只有客户端应用程序,它将调用一个现有的Web API
  • 你可以同时拥有客户端应用程序和Web API应用程序。在这种情况下,Web API应用程序也会向浏览器提供Blazor WebAssembly应用程序。这个选项被称为ASP.NET Core托管

对于这个项目,你将选择第二个选项。事实上,你将有一个客户端应用程序,它将负责显示用户界面和管理用户互动,而Web API应用程序将为客户端提供测验。

要建立这个应用程序,你需要在你的机器上安装.NET 6.0 SDK或以上版本。

你通过输入以下命令创建一个新的Blazor WebAssembly项目。

dotnet new blazorwasm -o QuizManagerClientHosted --hosted

注意:如果你想只创建客户端的应用程序,你必须省略前面命令中的 --hosted标志。

如果你看一下QuizManagerClientHosted 文件夹,你会发现如下所示的文件夹结构。

QuizManagerClientHosted
│   .gitignore
│   QuizManagerClientHosted.sln    
├── Client
├── Server
└── Shared

这些文件夹中的每一个都包含一个.NET项目。虽然ClientServer 文件夹是直接的,但你可能想知道Shared 文件夹包含什么。它包含一个类库项目,其代码由客户端和服务器端应用程序共享。在你要重新实现的应用程序的情况下,它将包含数据模型。

因此,移动到Shared 文件夹中,删除 WeatherForecasts.cs文件。在Shared 文件夹中创建一个新文件,名为 QuizItem.cs的新文件,内容如下。

// Shared/QuizItem.cs

namespace QuizManagerClientHosted.Shared;
public class QuizItem
{
  public string Question { get; set; }
  public List<string> Choices { get; set; }
  public int AnswerIndex { get; set; }
  public int Score { get; set; }

  public QuizItem()
  {
    Choices = new List<string>();
  }
}

这个类为测验的每个项目实现了模型。它提供了一个问题,一个可能的答案列表,正确答案的零基索引,以及用户给出正确答案时分配的分数。

创建服务器

移动到 Server/Controllers文件夹,并删除该 WeatherForecastController.cs文件。然后,在同一文件夹中添加一个新文件,名为 QuizController.cs的新文件,并在其中放入以下代码。

// Server/Controllers/QuizController.cs

using QuizManagerClientHosted.Shared;
using Microsoft.AspNetCore.Mvc;

namespace QuizManagerClientHosted.Server.Controllers;

[ApiController]
[Route("[controller]")]
public class QuizController : ControllerBase
{
  private static readonly List<QuizItem> Quiz = new List<QuizItem> {
    new QuizItem
      {
        Question = "Which of the following is the name of a Leonardo da Vinci's masterpiece?",
        Choices = new List<string> {"Sunflowers", "Mona Lisa", "The Kiss"},
        AnswerIndex = 1,
        Score = 3
      },
    new QuizItem
      {
        Question = "Which of the following novels was written by Miguel de Cervantes?",
        Choices = new List<string> {"The Ingenious Gentleman Don Quixote of La Mancia", "The Life of Gargantua and of Pantagruel", "One Hundred Years of Solitude"},
        AnswerIndex = 0,
        Score = 5
      }
    };

  [HttpGet]
  public List<QuizItem> Get()
  {
    return Quiz;
  }
}

正如你所看到的,这是你在Blazor服务器应用程序中创建的QuizService 类的Web API版本。你注意到用几个QuizItem 实例初始化了Quiz 静态变量,并定义了返回该变量的 Get()定义了返回该变量的动作。

关于如何在ASP.NET Core中创建Web API的更多信息,请参阅本教程

创建客户端

为了创建Blazor客户端应用程序,请移至 Client/Pages文件夹,并删除 Counter.razorFetchData.razor文件。然后,在该文件夹中添加一个名为 QuizViewer.razor的文件,内容如下。

// Client/Pages/QuizViewer.cs

@page "/quizViewer"
@using QuizManagerClientHosted.Shared
@inject HttpClient Http

<h1>Take your quiz!</h1>
<p>Your current score is @currentScore</p>

@if (quiz == null)
{
  <p><em>Loading...</em></p>
}
else
{
  int quizIndex = 0;
  @foreach (var quizItem in quiz)
  {
    <section>
    <h3>@quizItem.Question</h3>
    <div class="form-check">
      @{
        int choiceIndex = 0;
        quizScores.Add(0);
      }
      @foreach (var choice in quizItem.Choices)
      {
        int currentQuizIndex = quizIndex;
        <input class="form-check-input"
          type="radio" 
          name="@quizIndex" 
          value="@choiceIndex"
          @onchange="@((eventArgs) => UpdateScore(Convert.ToInt32(eventArgs.Value), currentQuizIndex))" />@choice
        <br>

        choiceIndex++;
      }
    </div>
    </section>

    quizIndex++;
  }
}

@code {
  List<QuizItem> quiz;
  List<int> quizScores = new List<int>();
  int currentScore = 0;

  protected override async Task OnInitializedAsync()
  {
    quiz = await Http.GetFromJsonAsync<List<QuizItem>>("Quiz");
  }

  void UpdateScore(int chosenAnswerIndex, int quizIndex)
  {
    var quizItem = quiz[quizIndex];

    if (chosenAnswerIndex == quizItem.AnswerIndex)
    {
      quizScores[quizIndex] = quizItem.Score;
    }
    else
    {
      quizScores[quizIndex] = 0;
    }
    currentScore = quizScores.Sum();
  }
}

@page 指令将这个Razor组件定义为一个页面,它是一个UI元素,可以通过Blazor的路由系统中的一个地址(/quizViewer在本例中)直接到达的UI元素。然后,你有@using 指令,它提供了对上面创建的共享数据模型的访问(QuizItem.cs).@inject 指令要求依赖性注入系统获取HttpClient 类的实例。

在这些初始化之后,你会发现定义了用户界面的标记。正如你所看到的,这部分是HTML和C#代码的混合体,其目的是建立问题列表,并以单选按钮表示各自可能的答案。

该组件的最后一个块被包围在@code 指令中。这是你放置组件的逻辑的地方。在QuizViewer 组件的例子中,你有 OnInitializedAsync()UpdateScore()方法。第一个方法在组件初始化时被调用,它基本上是通过调用你之前创建的Web API的Quiz 端点来获得测验数据。第二个方法 UpdateScore()方法在用户点击其中一个建议的答案时被调用,它根据用户选择的答案更新分配的分数列表。在同一个方法中,当前分数的值被计算出来并分配给currentScore 。这个变量的值显示在问题列表的上方,你可以在标记中看到。

为了完成你的应用程序,将 NavMenu.razor文件夹中的 Client/Shared文件夹中的内容,替换为以下代码。

// Shared/NavMenu.razor

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">QuizManager</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="quizViewer">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Quiz
            </NavLink>
        </div>
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

你可能已经注意到,这段代码重新定义了导航菜单,删除了指向CounterFetchData 组件的默认项目,并包括了一个到达QuizViewer 组件的项目。

运行你的Blazor WebAssembly应用程序

你的Blazor WebAssembly应用程序已经完成,可以运行了。

⚠️如果你使用的是Mac,请阅读!⚠️

在撰写本文时,Mac用户在通过.NET CLI运行ASP.NET Core应用程序时受到一个问题的影响。在运行.NET CLI时,你可能会得到以下对话窗口。

Keychain message issue in Mac

这是由于macOS上.NET CLI的一个已知问题。目前的解决方法是,你打开 QuizManagerClientHosted.Server.csproj文件并添加 <UseAppHost>false</UseAppHost>元素,如下图所示。

<!-- QuizManagerClientHosted.Server.csproj -->

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <Nullable>enable</Nullable>
  <ImplicitUsings>enable</ImplicitUsings>
  <!--👇 new element --->
  <UseAppHost>false</UseAppHost>
</PropertyGroup>

<!-- ...existing elements... -->

</Project>

如果你使用Visual Studio,你不会受到这个问题的影响。

在项目的根文件夹中,键入以下命令。

dotnet run --project Server

看一下你的终端窗口,以获得你的应用程序正在监听的地址。它的形式是 https://localhost:<YOUR_PORT_NUMBER>.在我的例子中,我得到的地址是 https://localhost:7291,我将在整个文章中提到它。

从.NET 6.0开始,任何通过模板创建的ASP.NET项目都会为HTTP分配一个5000到5300之间的随机端口,为HTTPS分配7000到7300之间的随机端口。更多信息请参见本文档

将浏览器指向你的应用程序的地址,你应该看到以下页面。

Blazor App Home page

点击导航栏上的测验项目,你应该可以进行一个简单的测验,如下面的截图所示。

Blazor Quiz Page

即使这个应用程序的外观和感觉与Blazor服务器的实现基本相同,但应用程序的架构却大不相同。在这种情况下,你的客户端被编译成WebAssembly并在浏览器中运行,而服务器端则在内置的Web服务器中运行。此外,在这种架构下,客户端和服务器用经典的HTTP请求进行交互。你可以通过使用浏览器的开发工具分析网络流量来检查这一点。

用Auth0注册Blazor WASM应用程序

现在你有了WebAssembly版本的Quiz Manager应用程序,学习如何保护它。你将使用Auth0,因为它提供了一种简单的方法来整合认证和授权,而不必处理底层技术的复杂性。要使用Auth0,你需要提供一些信息,并配置你的应用程序,使双方相互沟通。如果你还没有Auth0账户,你可以现在就注册一个免费账户

免费试用最强大的认证平台。开始使用→

访问Auth0仪表板后,移到应用程序部分,并按照以下步骤操作。

  1. 单击 "创建应用程序"按钮。
  2. 为您的应用程序提供一个友好的名称(例如,Quiz Blazor WASM客户端),并选择单页Web应用程序作为应用程序类型。
  3. 最后,单击 "创建"按钮。

在你注册应用程序后,移动到设置选项卡,注意你的Auth0和你的客户ID。然后,把这个值分配给 https://localhost:<YOUR_PORT_NUMBER>/authentication/login-callback允许的回调URLs字段,以及 https://localhost:<YOUR_PORT_NUMBER>允许注销的URLs字段。将 <YOUR_PORT_NUMBER>占位符替换为分配给你的应用程序的实际端口号。在我的例子中,这些值是 https://localhost:7291/authentication/login-callbackhttps://localhost:7291.

第一个值告诉Auth0在用户验证后要回调哪个URL。第二个值告诉Auth0,用户注销后应该被重定向到哪个URL。

最后,单击 "保存更改"按钮来应用它们。

添加对认证的支持

现在,你需要配置你的Blazor项目,应用一些变化,使其了解Auth0。

配置您的Blazor应用程序

因此,移动到 Client/wwwroot文件夹,并创建一个 appsettings.json文件,内容如下。

{
  "Auth0": {
    "Authority": "https://<YOUR_AUTH0_DOMAIN>",
    "ClientId": "<YOUR_CLIENT_ID>"
  }
}

替换占位符 <YOUR_AUTH0_DOMAIN><YOUR_CLIENT_ID>用从Auth0仪表板上获取的相应数值替换。

添加对认证的支持

现在,通过在Client 文件夹中运行以下命令,将认证包添加到Blazor客户端项目中。

dotnet add package Microsoft.AspNetCore.Components.WebAssembly.Authentication

添加软件包后,仍在Client 文件夹中,编辑 Program.cs文件,修改其内容如下。

// Client/Program.cs

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using QuizManagerClientHosted.Client;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

// 👇 new code
builder.Services.AddOidcAuthentication(options =>
{
  builder.Configuration.Bind("Auth0", options.ProviderOptions);
  options.ProviderOptions.ResponseType = "code";
});
// 👆 new code

await builder.Build().RunAsync();

你添加了对 AddOidcAuthentication()的特定选项。特别是,你指定了使用Auth0 部分的参数。 appsettings.json配置文件中的参数。另外,你还指定了你要使用的认证和授权流程的类型;在这种特定情况下,推荐使用授权代码流程

要在你的应用程序中完成认证支持的实施,请打开 index.html文件夹下的 Client/wwwroot文件夹下的文件,并将引用添加到 AuthenticationService.js脚本,如下图所示。

<!-- Client/wwwroot/index.html -->
<!DOCTYPE html>
<html>
  <!-- existing markup -->
  <body>
    <!-- existing markup -->
    <script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
    <!--👆 new addition -->
    <script src="appsettings.jsons_framework/blazor.webassembly.js"></script>
  </body>
</html>

这个脚本负责在WebAssembly客户端执行认证操作。

调整你的Blazor应用程序的用户界面

在这一点上,你为你的Blazor应用准备了支持认证的基础设施。现在你需要对用户界面做一些改变。

第一步是启用对授权Razor组件的支持。所以,打开 _Imports.razor文件,并在Client 文件夹中添加一个引用到 Microsoft.AspNetCore.Components.AuthorizationMicrosoft.AspNetCore.Authorization命名空间的引用。该文件的内容将如下所示。

@* Client/_Imports.razor *@

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization  //👈 new addition
@using Microsoft.AspNetCore.Authorization             //👈 new addition
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using QuizManagerClientHosted.Client
@using QuizManagerClientHosted.Client.Shared

然后,打开同一文件夹中的 App.razor文件,并将其内容替换为以下内容。

<!-- Client/App.razor -->

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <Authorizing>
                    <p>Determining session state, please wait...</p>
                </Authorizing>
                <NotAuthorized>
                    <h1>Sorry</h1>
                    <p>You're not authorized to reach this page. You need to log in.</p>
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

你使用AuthorizeRouteView Blazor组件,根据用户的认证状态定制内容。CascadingAuthenticationState 组件将把当前的认证状态传播给内部组件,以便它们能够一致地进行工作。

下一步是创建一个新的Razor组件,允许用户登录并在认证时看到他们的名字。因此,创建一个新的文件,名为 AccessControl.razor的新文件,内容如下 Client/Shared文件夹中创建一个新文件,内容如下。

@* Client/Shared/AccessControl.razor *@

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <a href="#" @onclick="BeginSignOut">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

该组件使用AuthorizeView 组件,根据用户的认证状态显示不同的内容。基本上,当用户没有被认证时,它会显示登录链接。当用户被认证时,它显示用户的名字和注销链接。

注意用户点击注销链接时被重定向到的URL (authentication/logout).你将在稍后了解该URL的情况。

现在,打开 MainLayout.razor``Shared 文件,并在 "*关于 "*链接之前添加AccessControl 组件。最后的代码应该如下所示。

@* Client/Shared/MainLayout.razor *@

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <AccessControl />    //👈 new code
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

当你在Auth0注册你的Blazor应用时,你指定了几个URL作为登录回调和注销的允许URL。为了管理这些URL,你需要实现一个页面,负责处理不同的认证阶段。为了这个目的,请在Auth0中创建一个新的 Authentication.razor文件夹中的 Client/Pages文件夹中创建一个新文件,代码如下。

@* Client/Pages/Authentication.razor *@

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Configuration

@inject NavigationManager Navigation
@inject IConfiguration Configuration

<RemoteAuthenticatorView Action="@Action">
    <LogOut>
        @{
            var authority = (string)Configuration["Auth0:Authority"];
            var clientId = (string)Configuration["Auth0:ClientId"];

             Navigation.NavigateTo($"{authority}/v2/logout?client_id={clientId}");
        }
    </LogOut>
</RemoteAuthenticatorView>

@code{
    [Parameter] public string Action { get; set; }
}

正如你所看到的,这个组件实现了一个包含RemoteAuthenticatorView 组件的页面。这个组件管理用户的认证状态,并在Auth0端与授权服务器进行交互。虽然登录的交互不需要任何特定的代码,但你需要管理注销的事务。事实上,根据设计,Blazor在客户端清除了你的认证状态,但并没有断开你与Auth0的连接。要在Auth0端关闭您的会话,您需要明确地调用注销端点,如上面的代码所示。

免责声明:在撰写本文时,由于一个明显的Blazor问题,注销功能似乎并不稳定。请查看Blazor项目资源库中的这个问题,以了解更多信息

最后,你需要将Authorize 属性添加到 QuizViewer.razor来保护它免受未经授权的访问。打开 QuizViewer.razor文件夹中的Pages ,并添加属性,如下图所示。

@* Client/Pages/QuizViewer.razor *@

@page "/quizViewer"
@attribute [Authorize]            //👈 new addition

@using QuizManagerClientHosted.Shared

// ... exisiting code ...

请注意,页面上的Authorize 属性的存在并不能阻止客户端调用服务器上的API。你也需要在服务器端保护API。

在这一点上,如果你的Blazor应用程序仍在运行,你可以停止它,并重新启动它,以测试认证整合。一旦应用程序运行,通过点击Quiz菜单项,您应该看到以下屏幕。

Blazor app and the unauthenticated user

注意右上角的 "登录"。点击它,就会显示Auth0通用登录页面,并进行认证过程。认证后,你就可以访问QuizViewer页面了。

用Auth0保证API的安全

QuizViewer页面上显示的数据是从服务器项目中实现的API加载的。 /quiz在服务器项目中实现的API。这个API没有被保护,所以任何客户端都可以访问它。事实上,Blazor的WASM客户端能够毫无问题地访问它。然而,在生产就绪的情况下,你需要保护API以防止未经授权的访问。虽然API的安全实现不在本教程的范围内,但你需要对服务器项目中的API进行一些修改,以确保其安全。

如果你想了解更多关于保护.NET中的Web API的信息,请查看这篇文章

注册API

就像你对Blazor WASM应用程序所做的那样,你需要向Auth0注册API。因此,请将您的浏览器指向Auth0仪表板,移至API部分,并按照以下步骤操作。

  1. 单击 "创建API"按钮。
  2. 为你的API提供一个友好的名称(例如,Quiz API)和一个独特的标识符(也称为受众),以URL格式(例如,quizapi.com)
  3. 把签名算法留给RS256,然后点击创建按钮。

这样,Auth0就知道了你的网络API,并将允许你控制访问。

保护API

Server 文件夹下的服务器项目中,打开 appsettings.json并修改其内容如下。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Auth0": {
    "Domain": "<YOUR_AUTH0_DOMAIN>",
    "Audience": "<YOUR_API_IDENTIFIER>"
  }
}

<YOUR_AUTH0_DOMAIN>占位符替换为你在Blazor WASM客户端中使用的Auth0域值。同时,将 <YOUR_API_IDENTIFIER>占位符替换为您在Auth0仪表板中为您的API定义的唯一标识符:它应该是 https://quizapi.com,如果你保留了建议的值。

还是在Server 文件夹中,运行以下命令来安装处理授权过程的库。

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

然后,打开 Program.cs文件并应用如下所示的更改。

// Server/Startup.cs

// ... exisiting code ...
using Microsoft.AspNetCore.Authentication.JwtBearer;
//👆 new code

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
//👇 new code
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, c =>
    {
        c.Authority = $"https://{builder.Configuration["Auth0:Domain"]}";
        c.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
        {
            ValidAudience = builder.Configuration["Auth0:Audience"],
            ValidIssuer = builder.Configuration["Auth0:Domain"]
        };
    });
//👆 new code

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

// ... exisiting code ...

app.UseRouting();

//👇 new code
app.UseAuthentication();
app.UseAuthorization();
//👆 new code

app.MapRazorPages();

// ... exisiting code ...

你添加了对 Microsoft.AspNetCore.Authentication.JwtBearer命名空间的引用,并添加了配置服务器通过Auth0处理授权过程的语句。最后,你配置了中间件来处理认证和授权。

现在,打开 QuizController.cs文件夹中的 Server/Controllers文件夹中的文件,并应用以下更改。

// Server/Controllers/QuizController.cs

using QuizManagerClientHosted.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;  //👈 new addition

namespace QuizManagerClientHosted.Server.Controllers;

[ApiController]
[Route("[controller]")]
[Authorize]            //👈 new addition
public class QuizController : ControllerBase
{
  // ... existing code ...
}

你添加了对 Microsoft.AspNetCore.Authorization命名空间,并用Authorize 属性装饰了QuizController 类。

记住:如果你想深入了解如何用Auth0保护你的API,请阅读这篇文章

现在你的API被保护了。要检查一切是否按预期工作,请移动到项目的根部并重新启动它。然后,登录到应用程序,点击 "测验"菜单项。这一次你不应该再显示测验数据了。你的屏幕应该是下面的样子。

Blazor app unauthorized to access the API

如果你看一下你的浏览器的开发工具的网络部分,你会发现,调用到 /quiz端点的调用会得到一个HTTP 401状态代码,如下面的例子。

Unauthorized error when calling an API

这证实了服务器阻止了对API的未授权访问。

"了解如何用Blazor WebAssembly调用受保护的API。"

(twitter.com/intent/twee…)

调用受保护的API

为了使您的Blazor WASM应用程序能够访问受保护的API,您需要从Auth0获得一个访问令牌,并在调用API时提供该令牌。你可能会想写一些代码,在你向服务器发出HTTP请求时附加上这个令牌。然而,你可以以一种直接的方式将访问令牌的附件集中到你的API调用中。

创建HTTP客户端

首先,移动到Client 文件夹,用以下命令安装 Microsoft.Extensions.Http包,并使用以下命令。

dotnet add package Microsoft.Extensions.Http

这个包允许你创建命名的HTTP客户端并自定义其行为。在你的案例中,你将创建一个HTTP客户端,在每个HTTP请求中自动附加一个访问令牌。

打开 Program.cs文件夹中的Client ,并添加一个对 Microsoft.AspNetCore.Components.WebAssembly.Authentication如下图所示。

// Client/Program.cs

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using QuizManagerClientHosted.Client;
//👇 new addition
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

// ... existing code ...

在同一个文件中,应用下面代码片断中指出的变化。

// Client/Program.cs

// ... existing code ...

builder.RootComponents.Add<HeadOutlet>("head::after");

//👇 old code
//builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

//👇 new code
builder.Services.AddHttpClient("ServerAPI", 
      client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
  .CreateClient("ServerAPI"));
//👆 new code      

// ... existing code ...

你用两行代码替换了现有的创建HTTP客户端的那行代码。该 AddHttpClient()方法定义了一个名为HttpClient 的实例(ServerAPI),将当前服务器的地址作为请求资源时使用的基本地址。同时,BaseAddressAuthorizationMessageHandler 类被添加到HttpClient 实例中作为 HTTP 消息处理程序。这个类是由 Microsoft.AspNetCore.Components.WebAssembly.Authentication命名空间提供的,负责将访问令牌附加到任何HTTP请求的应用程序的基本URI。

实际的HttpClient 实例是由 CreateClient()``IHttpClientFactory 方法创建的。

指定API受众

现在,打开 appsettings.json文件夹中的 Client/wwwroot文件夹,添加Audience 元素,如下图所示。

{
  "Auth0": {
    "Authority": "https://<YOUR_AUTH0_DOMAIN>",
    "ClientId": "<YOUR_CLIENT_ID>",
    "Audience": "<YOUR_API_IDENTIFIER>"
  }
}

<YOUR_API_IDENTIFIER>占位符替换为你在Auth0仪表板中为你的API定义的唯一标识符(例如。 https://quizapi.com).

现在,回到 Client/Program.cs文件中,应用下面强调的变化。

// Client/Program.cs

// ... existing code ...

builder.Services.AddOidcAuthentication(options =>
{
  builder.Configuration.Bind("Auth0", options.ProviderOptions);
  options.ProviderOptions.ResponseType = "code";
  //👇 new code
  options.ProviderOptions.AdditionalProviderParameters.Add("audience", builder.Configuration["Auth0:Audience"]);
});

await builder.Build().RunAsync();

你添加了一个额外的audience 参数,让Auth0知道你想调用由Audience 设置值确定的API。

进行调用

在这个全局配置之后,你可以调用你的Web API的quiz 端点。所以,打开 QuizViewer.razor``Client\Pages 文件,并对其内容做如下修改。

@* Client/Pages/QuizViewer.razor *@

@page "/quizViewer"
@attribute [Authorize]

@using QuizManagerClientHosted.Shared
//👇 new code
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
//👆 new code
@inject HttpClient Http


// ... existing code ...
  
@code {
  
    // ... existing code ...
  
    protected override async Task OnInitializedAsync()
    {
        //👇 changed code
        try
        {
            quiz = await Http.GetFromJsonAsync<List<QuizItem>>("quiz");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
        //👆 changed code
    }
  
  // ... existing code ...
}

你导入了 Microsoft.AspNetCore.Components.WebAssembly.Authentication命名空间。然后,你简单地安排了 OnInitializedAsync()方法,用一个try-catch语句来包装它。

应用这些变化后,重新启动你的应用程序,登录,并尝试移动到测验页面。这一次你应该能够访问你的受保护的API,并显示测验页面。

回顾总结

本教程指导你通过使用Auth0来创建和保护一个Blazor WebAssembly应用程序。你学会了如何建立一个简单的Blazor WebAssembly应用程序和一些Razor组件。你经历了向Auth0注册你的应用程序并使其支持认证的过程。最后,你保护了由你的应用程序的服务器端托管的API,并通过访问令牌调用该API。

本教程中保护的应用程序的完整源代码可以从GitHub仓库下载。