ASP.NET Core Blazor WebAssembly 的其他安全性案例

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

本文說明 Blazor WebAssembly 應用程式的其他安全性案例。

將權杖連結至傳出要求

AuthorizationMessageHandler 是用來處理存取權杖的 DelegatingHandler。 權杖可使用架構所註冊的 IAccessTokenProvider 服務來取得。 如果無法取得權杖,則會擲回 AccessTokenNotAvailableExceptionAccessTokenNotAvailableException 具有 Redirect 方法,可使用指定的 AccessTokenResult.InteractionOptions 瀏覽至 AccessTokenResult.InteractiveRequestUrl 以允許重新整理存取權杖。

為了方便起見,架構提供預先設定了應用程式基底位址的 BaseAddressAuthorizationMessageHandler 作為授權的 URL。 只有在要求 URI 位於應用程式基底 URI 內時,才會新增存取權杖。 若傳出要求 URI 不在應用程式的基底 URI 內,請使用 自訂 AuthorizationMessageHandler 類別 (建議)設定 AuthorizationMessageHandler

注意

除了伺服器 API 存取的用戶端應用程式組態以外,當用戶端和伺服器不在相同的基底位址時,伺服器 API 還必須允許跨原始來源要求 (CORS)。 如需伺服器端 CORS 設定的詳細資訊,請參閱本文稍後的跨原始來源資源共用 (CORS) 一節。

在以下範例中:

在下列範例中,HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http 中的擴充。 將套件新增至尚未加以參考的應用程式。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient("WebAPI", 
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("WebAPI"));

對於以 Blazor WebAssembly 專案範本為基礎的託管 Blazor解決方案,要求 URI 依預設會位於應用程式的基底 URI 內。 因此,IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) 會指派給從專案範本產生的應用程式中的 HttpClient.BaseAddress

已設定的 HttpClient 用來使用 try-catch 模式提出授權要求:

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http

...

protected override async Task OnInitializedAsync()
{
    try
    {
        var examples = 
            await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

        ...
    }
    catch (AccessTokenNotAvailableException exception)
    {
        exception.Redirect();
    }
}

自訂驗證要求案例

下列案例示範如何自訂驗證要求,以及如何從驗證選項取得登入路徑。

自訂登入程序

在新的 InteractiveRequestOptions 執行個體上,使用下列方法一或多次以管理登入要求的其他參數:

在下列 LoginDisplay 元件範例中,會將其他參數新增至登入要求:

  • prompt 設定為 login:強制使用者在該要求上輸入其認證,而使單一登入失效。
  • loginHint 設定為 peter@contoso.com:為 peter@contoso.com 的使用者預先填入登入頁面的使用者名稱/電子郵件地址欄位。 應用程式通常會在重新驗證期間使用此參數,因而已使用 preferred_username 宣告從先前的登入擷取使用者名稱。

Shared/LoginDisplay.razor

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <button @onclick="BeginLogOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <button @onclick="BeginLogIn">Log in</button>
    </NotAuthorized>
</AuthorizeView>

@code{
    public void BeginLogOut()
    {
        Navigation.NavigateToLogout("authentication/logout");
    }

    public void BeginLogIn()
    {
        InteractiveRequestOptions requestOptions =
            new()
            {
                Interaction = InteractionType.SignIn,
                ReturnUrl = Navigation.Uri,
            };

        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");

        Navigation.NavigateToLogin("authentication/login", requestOptions);
    }
}

如需詳細資訊,請參閱以下資源:

使用互動方式,在取得權杖之前自訂選項

如果發生 AccessTokenNotAvailableException,請在新的 InteractiveRequestOptions 執行個體上使用下列方法一或多次,以管理新識別提供者存取權杖要求的其他參數:

在下列透過 Web API 取得 JSON 資料的範例中,如果存取權杖無法使用 (擲回 AccessTokenNotAvailableException),則會將其他參數新增至重新導向要求:

  • prompt 設定為 login:強制使用者在該要求上輸入其認證,而使單一登入失效。
  • loginHint 設定為 peter@contoso.com:為 peter@contoso.com 的使用者預先填入登入頁面的使用者名稱/電子郵件地址欄位。 應用程式通常會在重新驗證期間使用此參數,因而已使用 preferred_username 宣告從先前的登入擷取使用者名稱。
try
{
    var examples = await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

    ...
}
catch (AccessTokenNotAvailableException ex)
{
    ex.Redirect(requestOptions => {
        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");
    });
}

上述範例假設:

如需詳細資訊,請參閱以下資源:

在使用 IAccessTokenProvider 時自訂選項

如果在使用 IAccessTokenProvider 時無法取得權杖,請在新的 InteractiveRequestOptions 執行個體上使用下列方法一或多次,以管理新識別提供者存取權杖要求的其他參數:

在下列嘗試為使用者取得存取權杖的範例中,如果在呼叫 TryGetToken 時嘗試取得權杖失敗,則會將其他參數新增至登入要求:

  • prompt 設定為 login:強制使用者在該要求上輸入其認證,而使單一登入失效。
  • loginHint 設定為 peter@contoso.com:為 peter@contoso.com 的使用者預先填入登入頁面的使用者名稱/電子郵件地址欄位。 應用程式通常會在重新驗證期間使用此參數,因而已使用 preferred_username 宣告從先前的登入擷取使用者名稱。
var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { ... }
    });

if (!tokenResult.TryGetToken(out var token))
{
    tokenResult.InteractionOptions.TryAddAdditionalParameter("prompt", "login");
    tokenResult.InteractionOptions.TryAddAdditionalParameter("loginHint", 
        "peter@contoso.com");

    Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, 
        accessTokenResult.InteractionOptions);
}

上述範例假設:

如需詳細資訊,請參閱以下資源:

使用自訂傳回 URL 登出

下列範例會登出使用者,並將使用者傳回至 /goodbye 端點:

Navigation.NavigateToLogout("authentication/logout", "goodbye");

從驗證選項取得登入路徑

RemoteAuthenticationOptions 取得已設定的登入路徑:

var loginPath = 
    RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath;

上述範例假設:

自訂 AuthorizationMessageHandler 類別

用戶端應用程式對不在應用程式基底 URI 內的 URI 提出傳出要求時,建議使用本節中的這項指引。

在下列範例中,自訂類別會擴充 AuthorizationMessageHandler,以作為 HttpClientDelegatingHandlerConfigureHandler 會設定此處理常式,以使用存取權杖授權給輸出 HTTP 要求。 至少有一個已授權的 URL 是要求 URI (HttpRequestMessage.RequestUri) 的基底時,才會連結存取權杖。

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, 
        NavigationManager navigation)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
            scopes: new[] { "example.read", "example.write" });
    }
}

在上述程式碼中,範圍 example.readexample.write 是通用範例,並非用來反映任何特定提供者的有效範圍。

Program 檔案中,CustomAuthorizationMessageHandler 會註冊為暫時性服務,並設定為具名 HttpClient 發出的傳出 HttpResponseMessage 執行個體的 DelegatingHandler

在下列範例中,HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http 中的擴充。 將套件新增至尚未加以參考的應用程式。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

builder.Services.AddHttpClient("WebAPI",
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

注意

在前述範例中,CustomAuthorizationMessageHandlerDelegatingHandler 已註冊為 AddHttpMessageHandler 的暫時性服務。 建議對 IHttpClientFactory 進行暫時性註冊,以管理其本身的 DI 範圍。 如需詳細資訊,請參閱以下資源:

對於以 Blazor WebAssembly 專案範本為基礎的託管 Blazor 解決方案,依預設會將 IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) 指派給 HttpClient.BaseAddress

已設定的 HttpClient 用來使用 try-catch 模式提出授權要求。 其中,會使用 CreateClient (Microsoft.Extensions.Http 套件) 建立用戶端,而在向伺服器 API 提出要求時,會為 HttpClient 提供包含存取權杖的執行個體。 如果要求 URI 是相對 URI,如同下列範例所示 (ExampleAPIMethod),則會在用戶端應用程式提出要求時與 BaseAddress 結合:

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("WebAPI");

            var examples = 
                await client.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

            ...
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

設定 AuthorizationMessageHandler

AuthorizationMessageHandler 可透過 ConfigureHandler 方法,使用已授權的 URL、範圍和傳回 URL 來設定。 ConfigureHandler 會設定處理常式,以使用存取權杖授權給輸出 HTTP 要求。 至少有一個已授權的 URL 是要求 URI (HttpRequestMessage.RequestUri) 的基底時,才會連結存取權杖。 如果要求 URI 是相對 URI,則會與 BaseAddress 結合。

在下列範例中,AuthorizationMessageHandler 會在 Program 檔案中設定 HttpClient

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddScoped(sp => new HttpClient(
    sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }))
    {
        BaseAddress = new Uri("https://api.contoso.com/v1.0")
    });

在上述程式碼中,範圍 example.readexample.write 是通用範例,並非用來反映任何特定提供者的有效範圍。

對於以 Blazor WebAssembly 專案範本為基礎的託管 Blazor 解決方案,依預設會將 IWebAssemblyHostEnvironment.BaseAddress 指派給下列項目:

具類型的 HttpClient

您可以定義具型別用戶端,以處理單一類別內的所有 HTTP 和權杖取得考量。

WeatherForecastClient.cs

using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[]? forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[] forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}

在上述範例中,WeatherForecast 類型是保存氣象預報資料的靜態類別。 預留位置 {ASSEMBLY NAME} 是應用程式的組件名稱 (例如 using static BlazorSample.Data;)。

在下列範例中,HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http 中的擴充。 將套件新增至尚未加以參考的應用程式。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

Program 檔案中:

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

對於以 Blazor WebAssembly 專案範本為基礎的託管 Blazor 解決方案,依預設會將 IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) 指派給 HttpClient.BaseAddress

在擷取氣象資料的元件中:

@inject WeatherForecastClient Client

...

protected override async Task OnInitializedAsync()
{
    forecasts = await Client.GetForecastAsync();
}

設定 HttpClient 處理常式

處理常式可透過 ConfigureHandler 針對輸出 HTTP 要求進一步設定。

在下列範例中,HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http 中的擴充。 將套件新增至尚未加以參考的應用程式。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

Program 檔案中:

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new [] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }));

在上述程式碼中,範圍 example.readexample.write 是通用範例,並非用來反映任何特定提供者的有效範圍。

對於以 Blazor WebAssembly 專案範本為基礎的託管 Blazor 解決方案,依預設會將 IWebAssemblyHostEnvironment.BaseAddress 指派給下列項目:

在具有安全預設用戶端的應用程式中未經驗證或未經授權的 Web API 要求

通常使用安全預設 HttpClient 的應用程式,也可以藉由設定具名 HttpClient 來提出未經驗證或未經授權的 Web API 要求。

在下列範例中,HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http 中的擴充。 將套件新增至尚未加以參考的應用程式。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

Program 檔案中:

builder.Services.AddHttpClient("WebAPI.NoAuthenticationClient", 
    client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"));

對於以 Blazor WebAssembly 專案範本為基礎的託管 Blazor 解決方案,依預設會將 IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) 指派給 HttpClient.BaseAddress

前述註冊現有安全預設 HttpClient 註冊以外的註冊。

元件會從 IHttpClientFactory (Microsoft.Extensions.Http 套件) 建立 HttpClient,以提出未經驗證或未經授權的要求:

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        var client = ClientFactory.CreateClient("WebAPI.NoAuthenticationClient");

        var examples = await client.GetFromJsonAsync<ExampleType[]>(
            "ExampleNoAuthentication");

        ...
    }
}

注意

伺服器 API 中的控制器 (在上述範例中為 ExampleNoAuthenticationController) 不會標示 [Authorize] 屬性

要使用安全用戶端或不安全的用戶端作為預設 HttpClient 執行個體,由開發人員決定。 進行此決策的方法之一,是考量應用程式所聯繫的已驗證端點數目與未經驗證的端點數目。 如果應用程式的要求大多是要保護 API 端點,請使用已驗證的 HttpClient 執行個體作為預設值。 否則,請將未驗證的 HttpClient 執行個體註冊為預設值。

使用 IHttpClientFactory 的替代方法是建立具型別用戶端,以對匿名端點進行未經驗證的存取。

要求其他存取權杖

存取權杖可藉由呼叫 IAccessTokenProvider.RequestAccessToken 以手動方式取得。 在下列範例中,應用程式的預設 HttpClient 需要額外的範圍。 Microsoft 驗證程式庫 (MSAL) 範例會使用 MsalProviderOptions 來設定範圍:

Program 檔案中:

builder.Services.AddMsalAuthentication(options =>
{
    ...

    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 1}");
    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 2}");
}

上述範例中的 {CUSTOM SCOPE 1}{CUSTOM SCOPE 2} 預留位置是自訂範圍。

注意

當使用者第一次使用在 Microsoft Azure 中註冊的應用程式時,AdditionalScopesToConsent 無法透過 Microsoft Entra ID 同意 UI 來佈建 Microsoft Graph 的委派使用者權限。 如需詳細資訊,請參閱搭配 ASP.NET Core Blazor WebAssembly 使用 Graph API

IAccessTokenProvider.RequestAccessToken 方法提供可讓應用程式使用一組指定的範圍佈建存取權杖的多載。

在 Razor 元件中:

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider

...

var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" }
    });

if (tokenResult.TryGetToken(out var token))
{
    ...
}

上述範例中的 {CUSTOM SCOPE 1}{CUSTOM SCOPE 2} 預留位置是自訂範圍。

AccessTokenResult.TryGetToken 會傳回:

  • truetoken 以供使用。
  • false (若未擷取權杖)。

跨來源資源共用 (CORS)

在 CORS 要求上傳送認證 (授權 cookie/標頭) 時,CORS 原則必須允許 Authorization 標頭。

下列原則包含的設定適用於:

  • 要求來源 (http://localhost:5000https://localhost:5001)。
  • 任何方法 (動詞)。
  • Content-TypeAuthorization 標頭。 若要允許自訂標頭 (例如 x-custom-header),請在呼叫 WithHeaders 時列出標頭。
  • 用戶端 JavaScript 程式碼所設定的認證 (credentials 屬性設定為 include)。
app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
        .AllowAnyMethod()
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, 
            "x-custom-header")
        .AllowCredentials());

Blazor WebAssembly 專案範本為基礎的託管 Blazor 解決方案會對用戶端和伺服器應用程式使用相同的基底位址。 用戶端應用程式的 HttpClient.BaseAddress 會預設為 builder.HostEnvironment.BaseAddress 的 URI。 託管 Blazor 解決方案的預設設定中需要 CORS 設定。 未由伺服器專案託管、且不共用伺服器應用程式基底位址的其他用戶端應用程式,必須在伺服器專案中進行 CORS 設定。

如需詳細資訊,請參閱在 ASP.NET Core 中啟用跨原始來源要求 (CORS) 和範例應用程式的 HTTP 要求測試器元件 (Components/HTTPRequestTester.razor)。

處理權杖要求錯誤

單頁應用程式 (SPA) 使用 OpenID Connect (OIDC) 驗證使用者時,驗證狀態會以因使用者提供認證而設定的工作階段 cookie 的形式,在 SPA 內和 Identity 提供者 (IP) 中本機維護。

IP 為使用者發出的權杖通常僅在短時間內有效 (大約一小時),因此用戶端應用程式必須定期擷取新的權杖。 否則,在授與的權杖到期後,使用者將會登出。 多數情況下,OIDC 用戶端可以直接佈建新的權杖,使用者無須再次驗證,這要歸功於保留在 IP 內的驗證狀態或「工作階段」。

在某些情況下,用戶端無法在沒有使用者互動的情況下取得權杖,例如,當使用者因故明確從 IP 登出時。 如果使用者造訪 https://login.microsoftonline.com 並登出,就會發生此情況。在這類情況下,應用程式不會立即得知使用者已登出。用戶端保留的任何權杖可能不再有效。 此外,在目前的權杖到期之後,用戶端無法在沒有使用者互動的情況下佈建新的權杖。

這並非權杖型驗證特有的情況。 這都是 SPA 的本質。 如果驗證 cookie 已移除,使用 cookie 的 SPA 也將無法呼叫伺服器 API。

當應用程式對受保護的資源執行 API 呼叫時,您必須注意下列事項:

  • 若要佈建新的存取權杖以呼叫 API,使用者可能必須再次驗證。
  • 即使用戶端擁有看似有效的權杖,對伺服器的呼叫仍可能失敗,因為權杖已由使用者撤銷。

應用程式要求權杖時,會有兩種可能的結果:

  • 要求成功,應用程式獲得有效的權杖。
  • 要求失敗,且應用程式必須再次驗證使用者才能取得新權杖。

權杖要求失敗時,您必須決定是否要先儲存任何目前的狀態再執行重新導向。 有數種方法可用來儲存愈來愈複雜的狀態:

  • 將目前頁面狀態儲存在工作階段儲存體中。 在 OnInitializedAsync 生命週期方法 (OnInitializedAsync) 期間,請先檢查狀態是否可還原再繼續操作。
  • 新增查詢字串參數,並將其用來向應用程式指出需要解除凍結先前儲存的狀態。
  • 新增具有唯一識別碼的查詢字串參數,以將資料儲存在工作階段儲存體中,而不會有與其他項目發生衝突的風險。

在驗證作業之前使用工作階段儲存體來儲存應用程式狀態

下列範例顯示如何:

  • 在重新導向至登入頁面之前保留狀態。
  • 使用查詢字串參數,在驗證之後復原先前的狀態。
...
@using System.Text.Json
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject IJSRuntime JS
@inject NavigationManager Navigation

<EditForm Model="User" OnSubmit="OnSaveAsync">
    <label>
        First Name: 
        <InputText @bind-Value="User!.Name" />
    </label>
    <label>
        Last Name: 
        <InputText @bind-Value="User!.LastName" />
    </label>
    <button type="submit">Save User</button>
</EditForm>

@code {
    public Profile User { get; set; } = new Profile();

    protected override async Task OnInitializedAsync()
    {
        var currentQuery = new Uri(Navigation.Uri).Query;

        if (currentQuery.Contains("state=resumeSavingProfile"))
        {
            var user = await JS.InvokeAsync<string>("sessionStorage.getItem",
                "resumeSavingProfile");

            if (!string.IsNullOrEmpty(user))
            {
                User = JsonSerializer.Deserialize<Profile>(user);
            }
        }
    }

    public async Task OnSaveAsync()
    {
        var http = new HttpClient();
        http.BaseAddress = new Uri(Navigation.BaseUri);

        var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile";

        var tokenResult = await TokenProvider.RequestAccessToken(
            new AccessTokenRequestOptions
            {
                ReturnUrl = resumeUri
            });

        if (tokenResult.TryGetToken(out var token))
        {
            http.DefaultRequestHeaders.Add("Authorization", 
                $"Bearer {token.Value}");
            await http.PostAsJsonAsync("Save", User);
        }
        else
        {
            await JS.InvokeVoidAsync("sessionStorage.setItem", 
                "resumeSavingProfile", JsonSerializer.Serialize(User));
            Navigation.NavigateTo(tokenResult.InteractiveRequestUrl);
        }
    }

    public class Profile
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }
}

在驗證作業之前使用工作階段儲存體和和狀態容器儲存應用程式狀態

在驗證作業期間,您在某些情況下會想要在瀏覽器重新導向至 IP 之前儲存應用程式狀態。 若您使用狀態容器,且想要在驗證成功後還原狀態,就可能發生這種情況。 您可以使用自訂驗證狀態物件來保留應用程式特定狀態或其參考,並在驗證作業順利完成後還原該狀態。 下列範例會示範此方法。

在應用程式中建立狀態容器類別,並使用屬性來保存應用程式狀態值。 下列範例使用容器來維護預設 Blazor 專案範本的Counter 元件 (Counter.razor) 的計數器值。 序列化和還原序列化容器的方法以 System.Text.Json 為基礎。

using System.Text.Json;

public class StateContainer
{
    public int CounterValue { get; set; }

    public string GetStateForLocalStorage()
    {
        return JsonSerializer.Serialize(this);
    }

    public void SetStateFromLocalStorage(string locallyStoredState)
    {
        var deserializedState = 
            JsonSerializer.Deserialize<StateContainer>(locallyStoredState);

        CounterValue = deserializedState.CounterValue;
    }
}

Counter 元件會使用狀態容器來維護元件外部的 currentCount 值:

@page "/counter"
@inject StateContainer State

<h1>Counter</h1>

<p>Current count: @currentCount</p>

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

@code {
    private int currentCount = 0;

    protected override void OnInitialized()
    {
        if (State.CounterValue > 0)
        {
            currentCount = State.CounterValue;
        }
    }

    private void IncrementCount()
    {
        currentCount++;
        State.CounterValue = currentCount;
    }
}

RemoteAuthenticationState 建立 ApplicationAuthenticationState。 提供 Id 屬性,作為本機儲存狀態的識別碼。

ApplicationAuthenticationState.cs

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string? Id { get; set; }
}
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string Id { get; set; }
}

Authentication 元件 (Authentication.razor) 會使用本機工作階段儲存體與 StateContainer 序列化和還原序列化方法 GetStateForLocalStorageSetStateFromLocalStorage,來儲存及還原應用程式的狀態:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

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

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

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

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}

此範例使用 Microsoft Entra (ME-ID) 進行驗證。 在 Program 檔案中:

  • ApplicationAuthenticationState 設定為 Microsoft 驗證程式庫 (MSAL) RemoteAuthenticationState 類型。
  • 狀態容器註冊於服務容器中。
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();

自訂應用程式路由

根據預設,Microsoft.AspNetCore.Components.WebAssembly.Authentication 程式庫會使用下表顯示的路由來代表不同的驗證狀態。

路由 目的
authentication/login 觸發登入作業。
authentication/login-callback 處理任何登入作業的結果。
authentication/login-failed 在登入作業因故失敗時顯示錯誤訊息。
authentication/logout 觸發登出作業。
authentication/logout-callback 處理登出作業的結果。
authentication/logout-failed 在登出作業因故失敗時顯示錯誤訊息。
authentication/logged-out 指出使用者已成功登出。
authentication/profile 觸發編輯使用者設定檔的作業。
authentication/register 觸發註冊新使用者的作業。

上表顯示的路由可透過 RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.AuthenticationPaths 來設定。 設定提供自訂路由的選項時,請確認應用程式具有可處理每個路徑的路由。

在下列範例中,所有路徑前面都會加上 /security

Authentication 元件 (Authentication.razor):

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

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

Program 檔案中:

builder.Services.AddApiAuthorization(options => { 
    options.AuthenticationPaths.LogInPath = "security/login";
    options.AuthenticationPaths.LogInCallbackPath = "security/login-callback";
    options.AuthenticationPaths.LogInFailedPath = "security/login-failed";
    options.AuthenticationPaths.LogOutPath = "security/logout";
    options.AuthenticationPaths.LogOutCallbackPath = "security/logout-callback";
    options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed";
    options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out";
    options.AuthenticationPaths.ProfilePath = "security/profile";
    options.AuthenticationPaths.RegisterPath = "security/register";
});

如果需要完全不同的路徑,請依照先前的說明設定路由,並使用明確的動作參數轉譯 RemoteAuthenticatorView

@page "/register"

<RemoteAuthenticatorView Action="RemoteAuthenticationActions.Register" />

如果選擇這麼做,則可以將 UI 分成不同的頁面。

自訂驗證使用者介面

RemoteAuthenticatorView 為每個驗證狀態都提供一組預設的 UI 片段。 每個狀態都可自訂,只要傳入自訂 RenderFragment 即可。 若要在初始登入程序期間自訂顯示的文字,可以變更 RemoteAuthenticatorView,如下所示。

Authentication 元件 (Authentication.razor):

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

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

RemoteAuthenticatorView 有一個片段,可依據下表顯示的驗證路由使用。

路由 Fragment
authentication/login <LoggingIn>
authentication/login-callback <CompletingLoggingIn>
authentication/login-failed <LogInFailed>
authentication/logout <LogOut>
authentication/logout-callback <CompletingLogOut>
authentication/logout-failed <LogOutFailed>
authentication/logged-out <LogOutSucceeded>
authentication/profile <UserProfile>
authentication/register <Registering>

自訂使用者

繫結至應用程式的使用者可以自訂。

使用承載宣告自訂使用者

在下列範例中,應用程式的已驗證使用者會就其每個驗證方法收到一個 amr 宣告。 amr 宣告會識別權杖的主體在 Microsoft Identity Platform v1.0 承載宣告中進行驗證的方式。 此範例會根據 RemoteUserAccount 使用自訂使用者帳戶類別。

建立擴充 RemoteUserAccount 類別的類別。 下列範例會將 AuthenticationMethod 屬性設定為使用者的 amrJSON 屬性值陣列。 使用者經過驗證後,架構會自動填入 AuthenticationMethod

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[]? AuthenticationMethod { get; set; }
}
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[] AuthenticationMethod { get; set; }
}

建立擴充 AccountClaimsPrincipalFactory<TAccount> 的處理站,以從儲存在 CustomUserAccount.AuthenticationMethod 中的使用者驗證方法建立宣告:

using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            if (account.AuthenticationMethod is not null)
            {
                foreach (var value in account.AuthenticationMethod)
                {
                    userIdentity.AddClaim(new Claim("amr", value));
                }
            }
        }

        return initialUser;
    }
}
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            foreach (var value in account.AuthenticationMethod)
            {
                userIdentity.AddClaim(new Claim("amr", value));
            }
        }

        return initialUser;
    }
}

為使用中的驗證提供者註冊 CustomAccountFactory。 下列任何註冊均有效:

  • AddOidcAuthentication

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddOidcAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddMsalAuthentication

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddApiAuthorization

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddApiAuthorization<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    

具有自訂使用者帳戶類別的 ME-ID 安全性群組和角色

如需使用 ME-ID 安全性群組和 ME-ID 管理員角色和自訂使用者帳戶類別的其他範例,請參閱具有 Microsoft Entra ID 群組和角色的 ASP.NET Core Blazor WebAssembly

使用驗證預先呈現

目前不支援預先轉譯需要驗證和授權的內容。 依照其中一個 Blazor WebAssembly 安全性應用程式主題中的指引操作後,請使用下列指示建立具有以下特性的應用程式:

  • 預先轉譯不需要授權的路徑。
  • 不預先轉譯需要授權的路徑。

針對 Client 專案的 Program 檔案,將一般服務註冊納入個別方法中 (例如,在 Client 專案中建立 ConfigureCommonServices 方法)。 一般服務是指開發人員註冊供用戶端和伺服器專案使用的服務。

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

Program 檔案中:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();

Server 專案的 Program 檔案中,註冊下列其他服務並呼叫 ConfigureCommonServices

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddRazorPages();
builder.Services.TryAddScoped<AuthenticationStateProvider, 
    ServerAuthenticationStateProvider>();

Client.Program.ConfigureCommonServices(services);

Server 專案的 Startup.ConfigureServices 方法中,註冊下列其他服務並呼叫 ConfigureCommonServices

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddRazorPages();
    services.AddScoped<AuthenticationStateProvider, 
        ServerAuthenticationStateProvider>();
    services.AddScoped<SignOutSessionStateManager>();

    Client.Program.ConfigureCommonServices(services);
}

如需 Blazor 架構伺服器驗證提供者 (ServerAuthenticationStateProvider) 的詳細資訊,請參閱 ASP.NET Core Blazor 驗證和授權

Server 專案的 Pages/_Host.cshtml 檔案中,將 Component 標籤協助程式 (<component ... />) 取代為下列項目:

<div id="app">
    @if (HttpContext.Request.Path.StartsWithSegments("/authentication"))
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssembly" />
    }
    else
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssemblyPrerendered" />
    }
</div>

在前述範例中:

  • 預留位置 {CLIENT APP ASSEMBLY NAME} 是用戶端應用程式的組件名稱 (例如 BlazorSample.Client)。
  • /authentication 路徑線段的條件式檢查:
    • 避免為驗證路徑進行預先轉譯 (render-mode="WebAssembly")。
    • 為非驗證路徑進行預先轉譯 (render-mode="WebAssemblyPrerendered")。

託管應用程式和第三方登入提供者的選項

使用第三方提供者進行託管 Blazor WebAssembly 應用程式的驗證和授權時,有數個選項可用來驗證使用者。 應選擇何者視具體情況而定。

如需詳細資訊,請參閱在 ASP.NET Core 中保存來自外部提供者的其他宣告和權杖

驗證使用者而僅呼叫受保護的第三方 API

使用用戶端 OAuth 流程對第三方 API 提供者驗證使用者:

builder.services.AddOidcAuthentication(options => { ... });

在此情節中:

  • 裝載應用程式的伺服器沒有作用。
  • 伺服器上的 API 無法受到保護。
  • 應用程式只能呼叫受保護的第三方 API。

使用第三方提供者驗證使用者,並在主機伺服器和第三方呼叫受保護的 API

使用第三方登入提供者設定 Identity。 取得第三方 API 存取所需的權杖,並加以儲存。

當使用者登入時,Identity 會在驗證程序中收集存取和重新整理權杖。 此時,有幾種方法可用來對第三方 API 進行 API 呼叫。

使用伺服器存取權杖來擷取第三方存取權杖

使用在伺服器上產生的存取權杖,從伺服器 API 端點擷取第三方存取權杖。 從該處使用第三方存取權杖,直接從用戶端上的 Identity 呼叫第三方 API 資源。

不建議使用此方法。此方法需要將第三方存取權杖視同為公用用戶端產生的權杖。 根據 OAuth 條款,公用應用程式沒有用戶端密碼 (因為我們無法確信它能安全地儲存密碼),且存取權杖是針對機密用戶端產生的。 機密用戶端是具有用戶端密碼,且假定能夠安全地儲存密碼的用戶端。

  • 第三方存取權杖可能會被授與其他範圍,以根據第三方為更受信任的用戶端發出權杖的事實來執行敏感性作業。
  • 同樣地,重新整理權杖不應發行給不受信任的用戶端,因為這樣會給予用戶端無限制的存取權,除非設定了其他限制。

從用戶端對伺服器 API 進行 API 呼叫,以呼叫第三方 API

從用戶端對伺服器 API 進行 API 呼叫。 從伺服器擷取第三方 API 資源的存取權杖,並發出任何必要的呼叫。

我們建議使用此方法。雖然此方法需要額外的網路躍點透過伺服器呼叫第三方 API,但最終會產生更安全的體驗:

  • 伺服器可儲存重新整理權杖,並確保應用程式不會失去對第三方資源的存取權。
  • 應用程式無法從可能包含更敏感權限的伺服器洩漏存取權杖。

使用 OpenID Connect (OIDC) v2.0 端點

驗證程式庫和 Blazor 專案範本會使用 OpenID Connect (OIDC) v1.0 端點。 若要使用 v2.0 端點,請設定 JWT 持有人 JwtBearerOptions.Authority 選項。 下列範例藉由將 v2.0 區段附加至 Authority 屬性,為 v2.0 設定了 ME-ID:

using Microsoft.AspNetCore.Authentication.JwtBearer;

...

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    });

或者,您可以在應用程式設定 (appsettings.json) 檔案中進行設定:

{
  "Local": {
    "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/",
    ...
  }
}

如果將區段附加到授權單位不適合應用程式的 OIDC 提供者 (例如使用非 ME 識別碼的提供者),請直接設定 Authority 屬性。 使用 Authority 索引鍵在 JwtBearerOptions 或應用程式設定檔案 (appsettings.json) 中設定屬性。

識別碼權杖中的宣告清單會針對 v2.0 端點有所變更。 變更的 Microsoft 文件已淘汰,但識別碼權杖參考中提供了與識別碼權杖中的宣告有關的指引。

在元件中設定及使用 gRPC

若要將 Blazor WebAssembly 應用程式設定為使用 ASP.NET Core gRPC 架構

注意

在 Blazor Web 應用程式中依預設會啟用預先轉譯功能,因此您必須先考量從伺服器的元件轉譯,再考量用戶端的轉譯。 任何預先轉譯的狀態都應流向用戶端,以便重複使用。 如需詳細資訊,請參閱預先轉譯 ASP.NET Core Razor 元件

注意

在裝載的 Blazor WebAssembly Web 應用程式中依預設會啟用預先轉譯功能,因此您必須先考量從伺服器的元件轉譯,再考量用戶端的轉譯。 任何預先轉譯的狀態都應流向用戶端,以便重複使用。 如需詳細資訊,請參閱預先轉譯和整合 ASP.NET Core Razor 元件

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;

...

builder.Services.AddScoped(sp =>
{
    var baseAddressMessageHandler = 
        sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
    baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
    var grpcWebHandler = 
        new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
    var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, 
        new GrpcChannelOptions { HttpHandler = grpcWebHandler });

    return new Greeter.GreeterClient(channel);
});

用戶端應用程式中的元件可以使用 gRPC 用戶端 (Grpc.razor) 進行 gRPC 呼叫:

@page "/grpc"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject Greeter.GreeterClient GreeterClient

<h1>Invoke gRPC service</h1>

<p>
    <input @bind="name" placeholder="Type your name" />
    <button @onclick="GetGreeting" class="btn btn-primary">Call gRPC service</button>
</p>

Server response: <strong>@serverResponse</strong>

@code {
    private string name = "Bert";
    private string? serverResponse;

    private async Task GetGreeting()
    {
        try
        {
            var request = new HelloRequest { Name = name };
            var reply = await GreeterClient.SayHelloAsync(request);
            serverResponse = reply.Message;
        }
        catch (Grpc.Core.RpcException ex)
            when (ex.Status.DebugException is 
                AccessTokenNotAvailableException tokenEx)
        {
            tokenEx.Redirect();
        }
    }
}

若要使用 Status.DebugException 屬性,請使用 Grpc.Net.Client 2.30.0 版或更新版本。

如需詳細資訊,請參閱 ASP.NET Core gRPC 應用程式中的 gRPC-Web

取代 AuthenticationService 實作

以下幾小節說明如何取代:

  • 任何 JavaScript AuthenticationService 實作。
  • 適用於 JavaScript 的 Microsoft 驗證程式庫 (MSAL.js)。

取代任何 JavaScript AuthenticationService 實作

建立 JavaScript 程式庫以處理您的自訂驗證詳細資料。

警告

本節中的指引是預設 RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProviderOptions> 的實作詳細資料。 本節中的 TypeScript 程式碼僅適用於 .NET 7 中的 ASP.NET Core,且在即將推出的 ASP.NET Core 版本中可能會變更,恕不另行通知。

// .NET makes calls to an AuthenticationService object in the Window.
declare global {
  interface Window { AuthenticationService: AuthenticationService }
}

export interface AuthenticationService {
  // Init is called to initialize the AuthenticationService.
  public static init(settings: UserManagerSettings & AuthorizeServiceSettings, logger: any) : Promise<void>;

  // Gets the currently authenticated user.
  public static getUser() : Promise<{[key: string] : string }>;

  // Tries to get an access token silently.
  public static getAccessToken(options: AccessTokenRequestOptions) : Promise<AccessTokenResult>;

  // Tries to sign in the user or get an access token interactively.
  public static signIn(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the sign-in process when a redirect is used.
  public static async completeSignIn(url: string) : Promise<AuthenticationResult>;

  // Signs the user out.
  public static signOut(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the signout callback when a redirect is used.
  public static async completeSignOut(url: string) : Promise<AuthenticationResult>;
}

// The rest of these interfaces match their C# definitions.

export interface AccessTokenRequestOptions {
  scopes: string[];
  returnUrl: string;
}

export interface AccessTokenResult {
  status: AccessTokenResultStatus;
  token?: AccessToken;
}

export interface AccessToken {
  value: string;
  expires: Date;
  grantedScopes: string[];
}

export enum AccessTokenResultStatus {
  Success = 'Success',
  RequiresRedirect = 'RequiresRedirect'
}

export enum AuthenticationResultStatus {
  Redirect = 'Redirect',
  Success = 'Success',
  Failure = 'Failure',
  OperationCompleted = 'OperationCompleted'
};

export interface AuthenticationResult {
  status: AuthenticationResultStatus;
  state?: unknown;
  message?: string;
}

export interface AuthenticationContext {
  state?: unknown;
  interactiveRequest: InteractiveAuthenticationRequest;
}

export interface InteractiveAuthenticationRequest {
  scopes?: string[];
  additionalRequestParameters?: { [key: string]: any };
};

您可以移除原始 <script> 標籤,並新增載入自訂程式庫的 <script> 標籤,以匯入程式庫。 下列範例示範如何將預設 <script> 標籤取代為從 wwwroot/js 資料夾載入程式庫 CustomAuthenticationService.js 的標籤。

wwwroot/index.html 中的 Blazor 指令碼 (_framework/blazor.webassembly.js) 前面的結尾 </body> 標籤內:

- <script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
+ <script src="js/CustomAuthenticationService.js"></script>

如需詳細資訊,請參閱 dotnet/aspnetcore GitHub 存放庫中的 AuthenticationService.ts

注意

.NET 參考來源的文件連結通常會載入存放庫的預設分支,這表示下一版 .NET 的目前開發。 若要選取特定版本的標籤,請使用 [切換分支或標籤] 下拉式清單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤

取代適用於 JavaScript 的 Microsoft 驗證程式庫 (MSAL.js)

如果應用程式需要適用於 JavaScript 的 Microsoft 驗證程式庫 (MSAL.js) 的自訂版本,請執行下列步驟:

  1. 確認系統具有最新的開發人員 .NET SDK,或從 .NET Core SDK:安裝程式和二進位檔取得並安裝最新的開發人員 SDK。 對於此案例不需要設定內部 NuGet 摘要。
  2. 依照從來源建置 ASP.NET Core 中的文件指示設定 dotnet/aspnetcore GitHub 存放庫,以進行開發。 派生及複製或下載 dotnet/aspnetcore GitHub 存放庫的 ZIP 封存。
  3. 開啟 src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json 檔案,並設定所需的 @azure/msal-browser 版本。 如需已發行的版本清單,請瀏覽 @azure/msal-browser npm 網站,然後選取 [版本] 索引標籤。
  4. 在命令殼層中使用 yarn build 命令,在 src/Components/WebAssembly/Authentication.Msal/src 資料夾中建置 Authentication.Msal 專案。
  5. 如果應用程式使用壓縮的資產 (Brotli/Gzip),請壓縮 Interop/dist/Release/AuthenticationService.js 檔案。
  6. AuthenticationService.js 檔案和其壓縮版本 (.br/.gz) 從 Interop/dist/Release 資料夾複製到應用程式已發佈資產中的應用程式 publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal 資料夾。

傳遞自訂提供者選項

定義類別,以將資料傳至基礎 JavaScript 程式庫。

重要

使用 System.Text.Json 序列化 JSON 時,類別的結構必須與程式庫預期的相符。

下列範例示範類別 ProviderOptionsJsonPropertyName 屬性符合假設的自訂提供者程式庫的預期:

public class ProviderOptions
{
    public string? Authority { get; set; }
    public string? MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string? ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string? RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string? PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string? ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string? ResponseMode { get; set; }
}
public class ProviderOptions
{
    public string Authority { get; set; }
    public string MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string ResponseMode { get; set; }
}

在 DI 系統中註冊提供者選項,並設定適當的值:

builder.Services.AddRemoteAuthentication<RemoteAuthenticationState, RemoteUserAccount,
    ProviderOptions>(options => {
        options.Authority = "...";
        options.MetadataUrl = "...";
        options.ClientId = "...";
        options.DefaultScopes = new List<string> { "openid", "profile", "myApi" };
        options.RedirectUri = "https://localhost:5001/authentication/login-callback";
        options.PostLogoutRedirectUri = "https://localhost:5001/authentication/logout-callback";
        options.ResponseType = "...";
        options.ResponseMode = "...";
    });

上述範例使用一般字串常值來設定重新導向 URI。 以下是可用的替代方案:

  • 使用 IWebAssemblyHostEnvironment.BaseAddressTryCreate

    Uri.TryCreate(
        $"{builder.HostEnvironment.BaseAddress}authentication/login-callback", 
        UriKind.Absolute, out var redirectUri);
    options.RedirectUri = redirectUri;
    
  • 主機產生器設定

    options.RedirectUri = builder.Configuration["RedirectUri"];
    

    wwwroot/appsettings.json

    {
      "RedirectUri": "https://localhost:5001/authentication/login-callback"
    }
    

其他資源