从 Blazor Web 应用调用受保护的 API

使用 .NET 8 构建的 Blazor Web 应用程序需要额外的工作才能正确使用身份验证和授权。在将 Auth0 身份验证添加到 Blazor Web 应用程序中,您已经了解了如何向使用最动态渲染模式的 Blazor Web 应用程序添加身份验证:交互式自动渲染模式。本文将向您展示如何使用相同的渲染模式从 Blazor 应用程序调用受保护的 API。

示例项目

为了向您展示 Blazor Web 应用程序如何调用受保护的 API,您将使用上述文章《向 Blazor Web 应用程序添加 Auth0 身份验证》中的示例项目。让我们通过在终端窗口中运行以下命令来下载示例项目:

git clone -b starting-point --single-branch

您将获得一个名为的文件夹,其中包含三个子文件夹,每个子文件夹包含一个.NET 项目:blazor-interactive-auto-call-api

  • BlazorIntAuto。这是托管 Blazor 应用程序服务器端的项目。
  • BlazorIntAuto.Client。该项目包含 Blazor 应用程序的 Blazor WebAssembly 部分。
  • ExternalAPI。此文件夹包含一个最小的 Web API 项目,您将使用它来测试 API 调用。

按照上述文章中的说明,向 Auth0 注册 Blazor 应用程序并对其进行配置。按照存储在文件夹中的文件中说明,向 Auth0 注册 API 并对其进行配置。README.mdExternalAPI

这两种情况都需要一个 Auth0 账户。如果没有,可以立即注册获取免费账户。

内部和外部 API

使用 .NET 8 构建的现代 Blazor Web 应用程序的架构由 Blazor 服务器应用程序和 Blazor WebAssembly 应用程序组成。Blazor 服务器应用程序托管 WebAssembly 应用程序,在交互式自动渲染模式下,它还负责预渲染客户端 Razor 组件。

要了解有关 Blazor 渲染模式的更多信息,请阅读此介绍。

从 Auth0 的角度来看,您有一个常规 Web 应用程序(Blazor 服务器应用程序)和一个单页应用程序(Blazor WebAssembly 应用程序)。这揭示了一些关于在此体系结构上下文中调用受保护 API 的含义的要点。

如果服务器应用程序调用受保护的 API,解决方案就相当简单了。这只不过是一个调用 API 的常规 ASP.NET Core 应用程序。您可以参考本文来了解在这种情况下如何调用 API 。

更有趣(也更棘手)的情况是 Blazor WebAssembly 应用想要调用受保护的 API。我们需要区分两种子情况:

  • Blazor WebAssembly 应用想要调用 Blazor 服务器应用实现和公开的 API。我们将此案例称为内部 API案例。
  • Blazor WebAssembly 应用想要调用由单独应用程序实现的 API。我们将此案例称为外部 API案例。

您该如何处理这两种情况?

调用内部 API

我们先来分析一下调用API的第一个情况:内部API的调用。

内部 API 的问题

您可能认为您只需要一个访问令牌,这样您就可以通过在标头中嵌入令牌来创建对内部 API 的授权请求Authorization。但是您如何获取该访问令牌呢?

请记住,用户身份验证发生在 Blazor 应用程序的服务器端(请参阅本文以了解有关如何实现身份验证的更多信息)。因此,您的 Blazor 服务器应用程序可以从 Auth0 获取访问令牌,然后它应该将此令牌提供给 Blazor WebAssembly 应用程序,以便它可以对内部 API 进行授权调用。这个想法至少有几个缺点:

  • 从访问令牌安全角度来看,服务器比客户端更安全。将访问令牌发送到 Blazor WebAssembly 应用会使其面临潜在的窃取风险。有些情况下,客户端会获取访问令牌来调用 API。不过,在这些情况下,客户端直接从授权服务器而不是中介接收访问令牌。为什么要将安全级别从更安全的级别(服务器持有的令牌)更改为不太安全的级别(客户端持有的令牌)?
  • 假设您无论如何都想将访问令牌传递给客户端,那么为了使用该令牌,您需要确定 Razor 组件何时在服务器上运行以及何时在浏览器上运行。在第一种情况下,您无需发出 HTTP 请求即可调用在服务器上实现的功能。在第二种情况下,您需要使用嵌入的访问令牌发出 HTTP 请求。虽然您可以以静态方式确定这一点,但交互式自动渲染模式会使事情变得复杂,因为相同的组件最初在服务器上渲染,随后在客户端上运行。

为了方便起见,我建议不要依赖访问令牌来向内部 API 发出授权请求。在这种情况下,最简单的方法是依赖传统的基于 cookie 的身份验证。毕竟,您的 Blazor WebAssembly 和服务器应用在同一个域上运行。

构建内部 API

让我们探索如何使用基于 cookie 的方法从 Blazor WebAssembly 应用调用内部 API。当然,让我们从实现内部 API 开始。

打开文件夹中的Blazor 服务器文件并添加下面突出显示的代码:Program.csBlazorIntAuto

// BlazorIntAuto/Program.cs
// ...existing code...

// �� new code

app.MapGet("/api/internalData", () =>

{

    var data = Enumerable.Range(1, 5).Select(index =>

        Random.Shared.Next(1, 100))

        .ToArray();

    return data;

})

.RequireAuthorization();

// �� new code

app.MapRazorComponents<App>()

    .AddInteractiveServerRenderMode()

    .AddInteractiveWebAssemblyRenderMode()

    .AddAdditionalAssemblies(typeof(Counter).Assembly);

app.Run();

此处添加的代码片段实现了一个最小的 Web API 端点,它仅返回一个由 1 到 100 之间的五个随机生成的整数组成的数组。请注意,该调用通过仅允许经过身份验证的用户访问端点来保护端点访问。/api/internalDataRequireAuthorization()

然后,在同一个文件中,找到定义应用程序服务的部分并添加如下所示的语句:

// BlazorIntAuto/Program.cs
// ...existing code...

builder.Services.AddRazorComponents()

    .AddInteractiveServerComponents()

    .AddInteractiveWebAssemblyComponents();

// �� new code

builder.Services.AddHttpClient();

// �� new code

var app = builder.Build();

// ...existing code...

您只需添加服务即可创建HttpClient实例。

为什么需要添加HttpClient到服务器?客户端将发出 HTTP 请求,而不是服务器。

您说得对,但这是使用 Interactive WebAssembly 和 Interactive Auto 渲染模式时 Blazor 的要求。由于这些渲染模式中的组件是在服务器上预渲染的,因此您需要抽象API 调用以防止出现错误。

查看Stack Overflow 上的这个问题和文档以了解更多信息。

您不需要在服务器端做任何其他事情。

请注意,示例项目中使用的Auth0 ASP.NET Core 身份验证 SDK已经启用对基于 cookie 的身份验证的支持。

调用内部 API

让我们转到文件夹以允许 Blazor WebAssembly 应用调用内部 API。添加包以使服务可供客户端应用使用:BlazorIntAuto.ClientMicrosoft.Extensions.HttpHttpClient

dotnet add package Microsoft.Extensions.Http

然后,打开Blazor WebAssembly 应用程序的文件并添加以下代码:Program.cs

// BlazorIntAuto.Client/Program.cs
// ...existing code...

PersistentAuthenticationStateProvider>();

// �� new code

builder.Services.AddHttpClient("ServerAPI",

      client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()

  .CreateClient("ServerAPI"));

// �� new code

await builder.Build().RunAsync();

此代码创建绑定到应用程序基址的命名实例HttpClient。这会导致来自客户端的所有调用都定向到服务器的基址,因此我们不需要使用绝对 URL。

接下来,转到文件夹,打开文件并进行以下突出显示的更改:BlazorIntAuto.Client/PagesCounter.razor

@* BlazorIntAuto.Client/Pages/Counter.razor *
@page "/counter"

@attribute [Authorize]

@rendermode InteractiveAuto

@inject HttpClient Http  <!-- �� new code -->

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p>Hello @Username!</p>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

<!-- �� new code -->

<div>

  <p> </p>

  <button class="btn btn-primary" @onclick="CallInternalApi">Call internal API</button>

  <p>@ApiResponse</p>

</div>

<!-- �� new code -->

@code {

    private int currentCount = 0;

    [CascadingParameter]

    private Task<AuthenticationState>? authenticationState { get; set; }

    private string Username = "";

    private string ApiResponse = ""; //�� new code

    protected override async Task OnInitializedAsync()

    {

        if (authenticationState is not null)

        {

        var state = await authenticationState;

        Username = state?.User?.Identity?.Name ?? string.Empty;

        }

        await base.OnInitializedAsync();

    }

    private void IncrementCount()

    {

        currentCount++;

    }

    //�� new code

    private async Task CallInternalApi()

    {

        var data = await Http.GetFromJsonAsync<int[]>("api/internalData");

        ApiResponse = $"This data comes from an internal API: {string.Join(",", data)}";

    }

    //�� new code

}

您将HttpClient实例注入到 Razor 组件中,然后添加了一个新按钮,让用户调用内部 API。API 的响应将显示在包含变量的段落中@ApiResponse。

在组件的代码区域中,您声明了ApiResponse私有变量并实现了方法,该方法仅使用实例向端点发出 HTTP 请求。调用的响应与消息字符串组合并分配给变量。CallInternalApi()HttpClientapi/internalDataApiResponse

转到BlazorIntAuto文件夹并启动dotnet run命令来运行 Blazor 应用程序。使用您喜欢的浏览器导航到该应用程序,您应该会看到如下页面:

从 Blazor Web 应用调用受保护的 API插图

单击“登录”链接进行身份验证并访问应用程序,然后导航到计数器页面。您应该看到如下所示的新按钮:

从 Blazor Web 应用调用受保护的 API插图1

单击调用内部 API按钮,您应该会看到其下方的 API 响应,如下图所示:

从 Blazor Web 应用调用受保护的 API插图2

太棒了!您实现了对受保护的内部 API 的调用。

要验证端点是否真正受到保护,请退出应用程序并直接导航到API 端点 URL。您将被重定向到 Auth0 通用登录页面进行身份验证。

调用外部 API

现在让我们分析如何调用受保护的外部 API,即不是由您的 Blazor 服务器应用程序实现的 API,而是由您无法修改的其他应用程序实现的 API。我们假设此外部 API 需要访问令牌。

您在本旅程开始时下载的示例项目包含一个简单的受保护 Web API,它公开了/data端点,并返回了一组随机生成的整数。我们假设它已经启动并运行。

外部 API 的问题

您的 Blazor WebAssembly 应用程序如何调用/data外部 API 的端点?您可能会认为,这一次,您的 Blazor WebAssembly 应用程序需要访问令牌,因为您无法修改外部 API。我想说您是对的,但请记住在寻找内部 API 解决方案时讨论过的缺点。您的 Blazor 服务器应用程序接收访问令牌,因此同样的问题也适用于外部 API 的情况。如何解决这个问题?

我的建议是应用一种常见的设计模式:后端前端 (BFF) 模式。使用此模式,访问令牌仍将由 Blazor 服务器应用程序处理,就像内部 API 的情况一样。此外,Blazor 服务器应用程序将代理来自 Blazor WebAssembly 应用程序的请求到外部 API。一般架构如下图所示:

从 Blazor Web 应用调用受保护的 API插图3

这样,Blazor WebAssembly 应用将继续使用基于 Cookie 的身份验证方法调用内部 API。Blazor 服务器应用将负责使用访问令牌调用外部 API,并将结果返回给调用者。

获取访问令牌

让我们通过在 Blazor 服务器端获取访问令牌来开始实现此模式。

您需要对应用程序配置和身份验证握手进行一些更改。首先,您需要指定客户端密钥(请参阅此处了解原因)和 API 受众以访问外部 API。您可以从Auth0 仪表板获取这两个值:您将从 Blazor 应用程序注册页面的“设置”选项卡中找到客户端密钥;您可以从 API 注册页面的“设置”选项卡中的“标识符”字段中获取受众。

获得这些数据后,转到BlazorIntAuto文件夹并将这些值添加到文件中,如下所示:appsettings.json

// BlazorIntAuto/appsettings.json
{

  "Logging": {

    "LogLevel": {

      "Default": "Information",

      "Microsoft.AspNetCore": "Warning"

    }

  },

  "AllowedHosts": "*",

  "Auth0": {

    "Domain": "YOUR_AUTH0_DOMAIN",

    "ClientId": "YOUR_CLIENT_ID",

    "ClientSecret": "YOUR_CLIENT_SECRET", // �� new key

    "Audience": "YOUR_UNIQUE_IDENTIFIER"  // �� new key

  }

}

现在,应用下面突出显示的更改以使用这些新设置并在身份验证时获取访问令牌:

// BlazorIntAuto/Program.cs
// ...existing code...

builder.Services

    .AddAuth0WebAppAuthentication(options => {

      options.Domain = builder.Configuration["Auth0:Domain"];

      options.ClientId = builder.Configuration["Auth0:ClientId"];

      // �� new code

      options.ClientSecret = builder.Configuration["Auth0:ClientSecret"];

      // �� new code

    })

    // �� new code

    .WithAccessToken(options =>

      {

          options.Audience = builder.Configuration["Auth0:Audience"];

      });

    // �� new code

// ...existing code...

您将客户端密钥添加到 SDK 参数中,并调用传递 API 受众的方法。WithAccessToken()

处理访问令牌

您的 Blazor 服务器应用程序需要管理访问令牌,以便在需要时可以使用它。

有关此处所遵循的过程的详细说明,请阅读有关在 ASP.NET Core 中调用受保护 API 的文章。

在该BlazorIntAuto文件夹中,创建一个名为的文件,其代码如下:TokenHandler.cs

//BlazorIntAuto/TokenHandler.cs
using System.Net.Http.Headers;

using Microsoft.AspNetCore.Authentication;

public class TokenHandler : DelegatingHandler {

    private readonly IHttpContextAccessor _httpContextAccessor;

    public TokenHandler(IHttpContextAccessor httpContextAccessor)

    {

      _httpContextAccessor = httpContextAccessor;

    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {

      var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");

      request.Headers.Authorization =

          new AuthenticationHeaderValue("Bearer", accessToken);

      return await base.SendAsync(request, cancellationToken);

    }

}

此代码定义了TokenHandler负责将访问令牌注入 HTTP 请求的类。

然后,将该TokenHandler类注册为你的应用程序的消息处理程序。编辑文件并添加下面突出显示的行:Program.cs

// BlazorIntAuto/Program.cs
// ...existing code...

builder.Services.AddRazorComponents()

    .AddInteractiveServerComponents()

    .AddInteractiveWebAssemblyComponents();

// �� new code

builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<TokenHandler>();

// �� new code

builder.Services.AddHttpClient();

var app = builder.Build();

// ...existing code...

下一步是定义将提供实例来HttpClient调用外部 API 的服务。仍然在文件中,将定义通用服务的现有语句替换为特定服务,如下所示:Program.csHttpClient

// BlazorIntAuto/Program.cs
// ...existing code...

builder.Services.AddScoped<TokenHandler>();

// builder.Services.AddHttpClient();  // �� old code

// �� new code

builder.Services.AddHttpClient("ExternalAPI",

      client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiBaseUrl"]))

      .AddHttpMessageHandler<TokenHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()

  .CreateClient("ExternalAPI"));

// �� new code

var app = builder.Build();

// ...existing code...

为了使应用程序独立于外部 API 的特定基本 URL,上述代码引用了一个配置键。您还ExternalApiBaseUrl需要将此键添加到文件中:appsettings.json

// BlazorIntAuto/appsettings.json
{

  "Logging": {

    "LogLevel": {

      "Default": "Information",

      "Microsoft.AspNetCore": "Warning"

    }

  },

  "AllowedHosts": "*",

  "Auth0": {

    "Domain": "YOUR_AUTH0_DOMAIN",

    "ClientId": "YOUR_CLIENT_ID",

    "ClientSecret": "YOUR_CLIENT_SECRET",

    "Audience": "YOUR_UNIQUE_IDENTIFIER"

  },

  "ExternalApiBaseUrl": "https://localhost:7130" // �� new key

}

请注意,该ExternalApiBaseUrl密钥未包含在该Auth0部分中,因为它与 Auth0 设置无关。

映射 API 调用

现在,让我们在 Blazor 服务器应用中创建一个映射外部 API 端点的 API 端点:

// BlazorIntAuto/Program.cs
// ...existing code...

app.MapGet("/api/internalData", () =>

{

    var data = Enumerable.Range(1, 5).Select(index =>

        Random.Shared.Next(1, 100))

        .ToArray();

    return data;

})

.RequireAuthorization();

// �� new code

app.MapGet("/api/externalData", async (HttpClient httpClient) =>

{

    return await httpClient.GetFromJsonAsync<int[]>("data");

})

.RequireAuthorization();

// �� new code

// ...existing code...

如您所见,有一个新的端点,它只是调用外部 API 端点并将结果返回给调用者。/api/externalDatadata

对于更复杂的调用,您可能需要考虑使用反向代理库,例如Yarp。

调用外部 API

最后,您的 Blazor WebAssembly 应用程序可以通过 Blazor 服务器应用调用外部 API。

在该文件夹中,将以下更改应用到组件:BlazorIntAuto.Client/PagesCounter.razor

@* BlazorIntAuto.Client/Pages/Counter.razor *
@page "/counter"

@attribute [Authorize]

@rendermode InteractiveAuto

@inject HttpClient Http

<!-- ...existing code... -->

<div>

  <p> </p>

  <button class="btn btn-primary" @onclick="CallInternalApi">Call internal API</button>

  <!-- �� new code -->

  <button class="btn btn-primary" @onclick="CallExternalApi">Call external API</button>

  <!-- �� new code -->

  <p>@ApiResponse</p>

</div>

@code {

    // ...existing code...

    //�� new code

    private async Task CallExternalApi()

    {

        var data = await Http.GetFromJsonAsync<int[]>("api/externalData");

        ApiResponse = $"This data comes from an external API: {string.Join(",", data)}";

    }

    //�� new code

}

您添加了一个新按钮来调用外部 API,并实现了调用端点的新方法。CallExternalApi()api/externalData

单击新按钮时,您将看到如下图所示的消息:

从 Blazor Web 应用调用受保护的 API插图4

大功告成!您的 Blazor WebAssembly 应用程序现在可以调用内部和外部受保护的 API。

这是一个漫长的过程,需要了解从使用 .NET 8 构建的 Blazor 应用程序调用受保护 API 的最佳方法。如您所见,主要问题与新的 Blazor 渲染模式和应用程序架构本身有关。

为了克服这些问题,您使用了传统的基于 cookie 的身份验证来调用内部 API。然后,您使用相同的基础架构来实现 BFF 模式,这使您可以将 Blazor 服务器转变为调用外部 API 的反向代理。

声明:本文表达的内容和观点没有任何投资暗示,文章内容仅代表个人观点,所述内容仅供学习、阅读和参考。对购买、持有或出售任何数字资产不作为交易依据,请用户谨慎自行评估。转载需注明来源,违者必究!