Share via


保護 ASP.NET Core 伺服器端 Blazor 應用程式

注意

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

重要

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

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

本文說明如何以 ASP.NET Core 應用程式的形式保護伺服器端 Blazor 應用程式。

為求安全,伺服器端 Blazor 應用程式的設定方式會與 ASP.NET Core 應用程式相同。 如需詳細資訊,請參閱 ASP.NET Core 安全性主題底下的文章。

只有在應用程式啟動時 (也就是應用程式第一次連線到 WebSocket 時) 才會建立驗證內容。 驗證內容會在線路的存留期內受到維護。 應用程式會定期重新驗證使用者的驗證狀態,目前預設每 30 分鐘一次。

如果應用程式必須擷取自訂服務的使用者,或回應使用者更新,請參閱伺服器端 ASP.NET Core Blazor 其他安全性案例

Blazor 不同於傳統伺服器轉譯的 Web 應用程式,這些應用程式會在每個頁面瀏覽上使用 cookie 提出新的 HTTP 要求。 瀏覽事件期間會檢查驗證。 不過,不涉及 cookie。 只有在對伺服器提出 HTTP 要求時,才會傳送 Cookie,這不是使用者在 Blazor 應用程式中瀏覽時發生的情況。 瀏覽期間,會在 Blazor 線路內檢查使用者的驗證狀態,您可以使用 RevalidatingAuthenticationStateProvider 抽象隨時在伺服器上更新。

重要

不建議在瀏覽期間實作自訂 NavigationManager 來實現驗證確認。 如果應用程式必須在瀏覽期間執行自訂驗證狀態邏輯,請使用自訂 AuthenticationStateProvider

注意

本文中的程式碼範例採用 可為 Null 的參考型別 (NRT) 和 .NET 編譯器 Null 狀態靜態分析,這在 .NET 6 或更新版本的 ASP.NET Core 中受到支援。 以 ASP.NET Core 5.0 或更早版本為目標時,請從本文的範例中移除 Null 型別指定 (?)。

專案範本

遵循 Tooling for ASP.NET Core Blazor 中的指引,建立新的伺服器端 Blazor 應用程式。

在選擇伺服器端應用程式範本並設定專案之後,請在 [驗證類型] 底下選取應用程式的驗證:

  • (預設值):無驗證。
  • 個別帳戶:使用者帳戶會使用 ASP.NET Core Identity 儲存在應用程式內。
  • (預設值):無驗證。
  • 個別帳戶:使用者帳戶會使用 ASP.NET Core Identity 儲存在應用程式內。
  • Microsoft 身分識別平台:如需詳細資訊,請參閱 ASP.NET Core Blazor 驗證和授權
  • Windows:使用 Windows 驗證。

BlazorIdentity UI (個人帳戶)

當您選擇 [個別帳戶] 的驗證選項時,Blazor 支援產生完整 Blazor 型 Identity UI。

Blazor Web 應用程式範本會為 SQL Server 資料庫產生 Identity 程式碼。 命令列版本預設會使用 SQLite,並包含 Identity 的 SQLite 資料庫。

此範本會處理下列動作:

  • 針對常式驗證工作新增 IdentityRazor 元件和相關邏輯,例如登入和登出使用者。
  • 新增 Identity 相關的套件和相依性。
  • 參考 _Imports.razor 中的 Identity 套件。
  • 建立自訂使用者 Identity 類別 (ApplicationUser)。
  • 建立及註冊 EF Core 資料庫內容 (ApplicationDbContext)。
  • 設定內建 Identity 端點的路由。
  • 包含 Identity 驗證和商務邏輯。

若要檢查 Blazor 架構的 Identity 元件,請在 Blazor Web 應用程式專案範本 (參考來源) 的 Account 資料夾的 PagesShared 資料夾中進行存取。

當您選擇互動式 WebAssembly 或互動式自動轉譯模式時,伺服器會處理所有驗證和授權要求,而 Identity 元件會在 Blazor Web 應用程式的主要專案中以靜態方式呈現在伺服器上。 專案範本包含 .Client 專案中的 PersistentAuthenticationStateProvider 類別 (參考來源),以同步處理伺服器與瀏覽器之間的使用者驗證狀態。 類別是 AuthenticationStateProvider 的自訂實作。 提供者會使用 PersistentComponentState 類別預先轉譯驗證狀態,並將其保存至頁面。

BlazorIdentity 取決於 DbContext 執行個體未由處理站建立,這是刻意的,因為 DbContext 足以讓專案範本的 Identity 元件以靜態方式轉譯,而不用支援互動功能。

在 Blazor Web 應用程式的主要專案中,驗證狀態供應器名為 IdentityRevalidatingAuthenticationStateProvider (參考來源) (僅限伺服器互動功能解決方案) 或 PersistingRevalidatingAuthenticationStateProvider (參考來源) (WebAssembly 或自動互動功能解決方案)。

如需在針對 Identity 元件強制執行靜態 SSR 的同時,全域互動轉譯模式如何套用至非 Identity 元件的描述,請參閱 ASP.NET Core Blazor 轉譯模式

如需保存預先轉譯狀態的詳細資訊,請參閱預先轉譯 ASP.NET Core Razor 元件

如需 BlazorIdentity UI 的詳細資訊,以及透過社交網站整合外部登入的指導,請參閱 .NET 8 中身分識別的新功能

注意

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

在 Blazor Web 應用程式中管理驗證狀態

本節適用於採用以下功能的 Blazor Web 應用程式:

  • 互動式伺服器端轉譯 (互動式 SSR) 和 CSR。
  • 用戶端轉譯 (CSR)。

用戶端驗證狀態供應器僅在 Blazor 內使用,不會與 ASP.NET Core 驗證系統整合。 在預先轉譯期間,Blazor 遵守頁面上定義的中繼資料,並且使用 ASP.NET Core 驗證系統來判斷使用者是否已驗證。 當使用者從一個頁面瀏覽到另一個頁面時,會使用用戶端驗證提供者。 當使用者重新整理頁面 (完整頁面重新載入) 時,用戶端驗證狀態供應器不會參與伺服器上的驗證決策。 由於伺服器不會保存使用者的狀態,因此維護用戶端的任何驗證狀態都會遺失。

若要解決此問題,最佳方法是在 ASP.NET Core 驗證系統中執行驗證。 用戶端驗證狀態供應器只會負責反映使用者的驗證狀態。 Blazor Web 應用程式專案範本示範如何使用驗證狀態供應器來完成這項作業的範例:

注意

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

產生Identity

如需如何將 Identity 產生至伺服器端 Blazor 應用程式的詳細資訊,請參閱在 ASP.NET Core 專案中產生 Identity

將 Identity 產生至伺服器端 Blazor 應用程式:

來自外部提供者的其他宣告和權杖

若要儲存來自外部提供者的其他宣告,請參閱在 ASP.NET Core 中保存來自外部提供者的其他宣告和權杖

Linux 上的 Azure App Service 與 Identity 伺服器

請在部署 Linux 上的 Azure App Service 與 Identity 伺服器時明確指定簽發者。 如需詳細資訊,請參閱使用 Identity 來保護 SPA 的 Web API 後端

實作自訂 AuthenticationStateProvider

如果應用程式需要自訂提供者,請實作 AuthenticationStateProvider 並覆寫 GetAuthenticationStateAsync

在下列範例中,所有使用者都會以使用者名稱 mrfibuli 進行驗證。

CustomAuthStateProvider.cs

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

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, "mrfibuli"),
        }, "Custom Authentication");

        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user));
    }
}

CustomAuthStateProvider 服務會在 Program 檔案中註冊:

using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

在呼叫 AddServerSideBlazor 之後CustomAuthStateProvider 服務便會在 Program 檔案中註冊:

using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddServerSideBlazor();

...

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

在呼叫 AddServerSideBlazor 之後CustomAuthStateProvider 服務便會在 Startup.csStartup.ConfigureServices 中註冊:

using Microsoft.AspNetCore.Components.Authorization;

...

services.AddServerSideBlazor();

...

services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

確認或新增 AuthorizeRouteViewRouter 元件。

Routes 元件 (Components/Routes.razor) 中:

<Router ...>
    <Found ...>
        <AuthorizeRouteView RouteData="routeData" 
            DefaultLayout="typeof(Layout.MainLayout)" />
        ...
    </Found>
</Router>

將串聯驗證狀態服務新增至 Program 檔案中的服務集合:

builder.Services.AddCascadingAuthenticationState();

注意

當您從其中一個已啟用驗證的 Blazor 專案範本建立 Blazor 應用程式時,此應用程式會包含 AuthorizeRouteView 和對 AddCascadingAuthenticationState 的呼叫。 如需詳細資訊,請參閱 ASP.NET Core Blazor 驗證和授權,其中附有搭配 Router 元件自訂未經授權的內容一節中呈現的其他資訊。

確認或新增 AuthorizeRouteViewCascadingAuthenticationStateRouter 元件:

<CascadingAuthenticationState>
    <Router ...>
        <Found ...>
            <AuthorizeRouteView RouteData="routeData" 
                DefaultLayout="typeof(MainLayout)" />
            ...
        </Found>
    </Router>
</CascadingAuthenticationState>

注意

當您從其中一個已啟用驗證的 Blazor 專案範本建立 Blazor 應用程式時,此應用程式會包含上述範例中顯示的 AuthorizeRouteViewCascadingAuthenticationState 元件。 如需詳細資訊,請參閱 ASP.NET Core Blazor 驗證和授權,其中附有搭配 Router 元件自訂未經授權的內容一節中呈現的其他資訊。

AuthorizeView 示範任何元件中已驗證的使用者名稱:

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

如需使用 AuthorizeView 的指引,請參閱 ASP.NET Core Blazor 驗證和授權

關於驗證狀態變更的通知

自訂 AuthenticationStateProvider 可以在 AuthenticationStateProvider 基底類別上叫用 NotifyAuthenticationStateChanged,以通知取用者要重新轉譯驗證狀態變更。

下列範例是以實作自訂 AuthenticationStateProvider 為基礎,方法是遵循實作自訂 AuthenticationStateProvider 一節中的指引。

下列 CustomAuthStateProvider 實作會公開自訂方法 AuthenticateUser,以登入使用者並通知取用者驗證狀態變更。

CustomAuthStateProvider.cs

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

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity();
        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user));
    }

    public void AuthenticateUser(string userIdentifier)
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, userIdentifier),
        }, "Custom Authentication");

        var user = new ClaimsPrincipal(identity);

        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(user)));
    }
}

在 元件中:

@inject AuthenticationStateProvider AuthenticationStateProvider

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        ((CustomAuthStateProvider)AuthenticationStateProvider)
            .AuthenticateUser(userIdentifier);
    }
}
@inject AuthenticationStateProvider AuthenticationStateProvider

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        ((CustomAuthStateProvider)AuthenticationStateProvider)
            .AuthenticateUser(userIdentifier);
    }
}

您可以增強上述方法,透過自訂服務觸發驗證狀態變更的通知。 下列 AuthenticationService 程式碼會在支援欄位 (currentUser) 中使用 AuthenticationStateProvider 可以訂閱的事件 (UserChanged),維護目前使用者的宣告主體,此事件會在該欄位中叫用 NotifyAuthenticationStateChanged。 透過本節稍後的其他設定,AuthenticationService 可以透過將 CurrentUser 設定為觸發 UserChanged 事件的邏輯插入至元件。

using System.Security.Claims;

public class AuthenticationService
{
    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);
            }
        }
    }
}

Program 檔案中,於相依性插入容器中註冊 AuthenticationService

builder.Services.AddScoped<AuthenticationService>();

Startup.csStartup.ConfigureServices 中,於相依性插入容器中註冊 AuthenticationService

services.AddScoped<AuthenticationService>();

下列 CustomAuthStateProvider 會訂閱 AuthenticationService.UserChanged 事件。 GetAuthenticationStateAsync 會傳回使用者的驗證狀態。 一開始,驗證狀態是以 AuthenticationService.CurrentUser 的值為基礎。 當使用者發生變更時,系統會以新的使用者 (new AuthenticationState(newUser)) 建立新的驗證狀態,以呼叫 GetAuthenticationStateAsync

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

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState authenticationState;

    public CustomAuthStateProvider(AuthenticationService service)
    {
        authenticationState = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            authenticationState = new AuthenticationState(newUser);

            NotifyAuthenticationStateChanged(
                Task.FromResult(new AuthenticationState(newUser)));
        };
    }

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

下列元件的 SignIn 方法會為要在 AuthenticationService.CurrentUser 上設定的使用者識別碼建立宣告主體:

@inject AuthenticationService AuthenticationService

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        var currentUser = AuthenticationService.CurrentUser;

        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, userIdentifier),
            },
            "Custom Authentication");

        var newUser = new ClaimsPrincipal(identity);

        AuthenticationService.CurrentUser = newUser;
    }
}
@inject AuthenticationService AuthenticationService

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        var currentUser = AuthenticationService.CurrentUser;

        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, userIdentifier),
            },
            "Custom Authentication");

        var newUser = new ClaimsPrincipal(identity);

        AuthenticationService.CurrentUser = newUser;
    }
}

為範圍限定為元件的服務插入 AuthenticationStateProvider

請不要嘗試在自訂範圍內解析 AuthenticationStateProvider,因為其會導致為未正確初始化的 AuthenticationStateProvider 建立新執行個體。

若要在範圍限定為元件的服務內存取 AuthenticationStateProvider,請使用 @inject 指示詞[Inject] 屬性插入 AuthenticationStateProvider,並將其以參數形式傳遞至服務。 此方法確保針對每個使用者應用程式執行個體使用正確、初始化的 AuthenticationStateProvider 執行個體。

ExampleService.cs

public class ExampleService
{
    public async Task<string> ExampleMethod(AuthenticationStateProvider authStateProvider)
    {
        var authState = await authStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            return $"{user.Identity.Name} is authenticated.";
        }
        else
        {
            return "The user is NOT authenticated.";
        }
    }
}

將服務註冊為範圍。 在伺服器端 Blazor 應用程式中,限定範圍服務的存留期等於用戶端連線線路的持續時間。

Program 檔案中:

builder.Services.AddScoped<ExampleService>();

Startup.csStartup.ConfigureServices 中:

services.AddScoped<ExampleService>();

在下列 InjectAuthStateProvider 元件中:

InjectAuthStateProvider.razor

@page "/inject-auth-state-provider"
@inherits OwningComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
    private string? message;
    private ExampleService? ExampleService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ExampleService = ScopedServices.GetRequiredService<ExampleService>();

        message = await ExampleService.ExampleMethod(AuthenticationStateProvider);
    }
}
@page "/inject-auth-state-provider"
@inject AuthenticationStateProvider AuthenticationStateProvider
@inherits OwningComponentBase

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
    private string? message;
    private ExampleService? ExampleService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ExampleService = ScopedServices.GetRequiredService<ExampleService>();

        message = await ExampleService.ExampleMethod(AuthenticationStateProvider);
    }
}

如需詳細資訊,請參閱 ASP.NET Core Blazor 相依性插入OwningComponentBase 的相關指引。

使用自訂 AuthenticationStateProvider 預先轉譯時會顯示未經授權的內容

若要避免在預先轉譯自訂 AuthenticationStateProvider 時顯示未經驗證的內容,例如 AuthorizeView 元件中的內容,請採用以下其中一個方法:

  • 停用預先轉譯:指出轉譯模式,其中 prerender 參數在應用程式元件階層的最高階元件中設定為 false,最高階元件不是根元件。

    注意

    不支援讓根元件成為互動式,例如 App 元件。 因此,App 元件無法直接停用預先轉譯。

    針對以 Blazor Web 應用程式專案範本為基礎的應用程式,預先轉譯通常會停用,其中 Routes 元件是在 App 元件 (Components/App.razor) 中使用:

    <Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
    

    此外,停用元件的 HeadOutlet 預先轉譯:

    <HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
    

    您也可以選擇性地停用預先轉譯,並精確控制套用至 Routes 元件執行個體的轉譯模式。 如需詳細資訊,請參閱 ASP.NET Core Blazor 轉譯模式

  • 停用預先轉譯:開啟 _Host.cshtml 檔案,並將元件標籤協助程式render-mode 屬性變更為 Server

    <component type="typeof(App)" render-mode="Server" />
    
  • 在應用程式啟動前驗證伺服器上的使用者:若要採用這種方法,應用程式必須以 Identity 型登入頁面或檢視回應使用者的初始要求,並防止對 Blazor 端點的任何要求,直到其驗證完畢為止。 如需詳細資訊,請參閱使用受授權保護的使用者資料建立 ASP.NET Core 應用程式。 驗證之後,只有在使用者真正未經授權檢視內容時,才會顯示預先轉譯 Razor 元件中未經授權的內容。

使用者狀態管理

儘管名稱中有 "state" 這個字,但 AuthenticationStateProvider 不是用於儲存「一般使用者狀態」AuthenticationStateProvider 只會指出使用者對應用程式的驗證狀態、其是否登入應用程式,以及其以什麼身分登入。

驗證會使用與 Razor Pages 和 MVC 應用程式相同的 ASP.NET Core Identity 驗證。 針對 ASP.NET Core Identity 儲存的使用者狀態會流向 Blazor,而無需將額外的程式碼新增至應用程式。 請遵循 ASP.NET Core Identity 文章和教學課程中的指引,讓 Identity 功能在應用程式的 Blazor 組件中生效。

如需 ASP.NET Core Identity 以外的一般狀態管理指引,請參閱 ASP.NET Core Blazor 狀態管理

其他安全性抽象

有兩個額外的抽象會參與管理驗證狀態:

注意

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

暫時重新導向 URL 有效期間

本節適用於 Blazor Web Apps。

針對 Blazor 伺服器端轉譯發出的暫時重新導向 URL,使用 RazorComponentsServiceOptions.TemporaryRedirectionUrlValidityDuration 選項來取得或設定資料保護有效性的存留期。 這些只是暫時使用,因此存留期只要足夠讓用戶端接收 URL 並開始瀏覽至該 URL 即可。 不過,也應該足以允許跨伺服器時發生的時鐘誤差。 預設值為五分鐘。

在下列範例中,該值會延長至七分鐘:

builder.Services.AddRazorComponents(options => 
    options.TemporaryRedirectionUrlValidityDuration = 
        TimeSpan.FromMinutes(7));

其他資源