Share via


使用 ASP.NET Core Identity 來保護 ASP.NET Core Blazor WebAssembly

您可以遵循本文中的指導,以使用 ASP.NET Core Identity 來保護獨立 Blazor WebAssembly 應用程式。

用於註冊、登入和登出的端點

與其使用 ASP.NET Core Identity 為 SPA 和 Blazor 應用程式提供的預設 UI (以 Razor Pages 為基礎),不妨在後端 API 中呼叫 MapIdentityApi,以新增使用 ASP.NET Core Identity 來註冊和登入使用者的 JSON API 端點。 Identity API 端點也支援進階功能,例如雙因素驗證和電子郵件驗證。

在用戶端上,呼叫 /register 端點即可使用使用者的電子郵件地址和密碼來註冊使用者:

var result = await _httpClient.PostAsJsonAsync(
    "register", new
    {
        email,
        password
    });

在用戶端上,使用 /login 端點並將 useCookies 查詢字串設定為 true,即可透過 cookie 驗證來登入使用者:

var result = await _httpClient.PostAsJsonAsync(
    "login?useCookies=true", new
    {
        email,
        password
    });

後端伺服器 API 會在驗證產生器上呼叫 AddIdentityCookies 來建立 cookie 驗證:

builder.Services
    .AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddIdentityCookies();

權杖驗證

對於某些用戶端不支援 cookie 的原生案例和行動裝置案例,登入 API 會提供參數來要求權杖。 系統會發出可用來驗證後續要求的自訂權杖 (ASP.NET Core Identity 平台專屬的權杖)。 權杖應以持有人權杖的形式傳入 Authorization 標頭。 系統也會提供重新整理權杖。 此權杖可讓應用程式在舊權杖到期時要求新的權杖,而不需要強制使用者重新登入。

權杖不是標準的 JSON Web 權杖 (JWT)。 使用自訂權杖是刻意的,因為內建的 Identity API 主要用於簡單案例。 權杖選項不是用來作為功能完整的識別服務提供者或權杖伺服器,而是作為無法使用 cookie 用戶端的 cookie 選項的替代方案。

下列指導會開始進行使用登入 API 來實作權杖型驗證的程序。 必須有自動程式碼才能完成這項實作。 如需詳細資訊,請參閱使用 Identity 來保護 SPA 的 Web API 後端

會由伺服器 API 使用 AddBearerToken 擴充方法來設定持有人權杖驗證,而不是由後端伺服器 API 透過在驗證產生器上呼叫 AddIdentityCookies 來建立 cookie 驗證。 請使用 IdentityConstants.BearerScheme 指定持有人驗證權杖的配置。

Backend/Program.cs 中,將驗證服務和設定變更為下列內容:

builder.Services
    .AddAuthentication()
    .AddBearerToken(IdentityConstants.BearerScheme);

BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs 中,移除 CookieAuthenticationStateProviderLoginAsync 方法中的 useCookies 查詢字串參數:

- login?useCookies=true
+ login

此時,您必須提供自訂程式碼來剖析用戶端上的 AccessTokenResponse,並管理存取權杖和重新整理權杖。 如需詳細資訊,請參閱使用 Identity 來保護 SPA 的 Web API 後端

其他 Identity 案例

如需 API 所提供的其他 Identity 案例,請參閱使用 Identity 來保護 SPA 的 Web API 後端

  • 保護選取的端點
  • 權杖驗證
  • 雙重要素驗證 (2FA)
  • 復原程式碼
  • 使用者資訊管理

範例應用程式

在本文中,範例應用程式可作為透過後端 Web API 存取 ASP.NET Core Identity 的獨立 Blazor WebAssembly 應用程式的參考。 示範中包含兩個應用程式:

  • Backend:後端 Web API 應用程式,可維護 ASP.NET Core Identity 的使用者身分識別存放區。
  • BlazorWasmAuth:具有使用者驗證的獨立 Blazor WebAssembly 前端應用程式。

請使用下列連結,透過存放庫根目錄中的最新版本資料夾來存取範例應用程式。 這些範例是針對 .NET 8 或更新版本來提供的。 如需如何執行範例應用程式的步驟,請參閱 BlazorWebAssemblyStandaloneWithIdentity 資料夾中的 README 檔案。

檢視或下載範例程式碼 \(英文\) (如何下載)

後端 Web API 應用程式套件和程式碼

後端 Web API 應用程式會維護 ASP.NET Core Identity 的使用者身分識別存放區。

套件

應用程式會使用下列 NuGet 套件:

如果您的應用程式使用與記憶體內部提供者不同的 EF Core 資料庫提供者,請勿在應用程式中建立 Microsoft.EntityFrameworkCore.InMemory 的套件參考。

在應用程式的專案檔 (.csproj) 中,會設定不因全球化而異

範例應用程式的程式碼

應用程式設定會設定後端和前端 URL:

  • Backend 應用程式 (BackendUrl):https://localhost:7211
  • BlazorWasmAuth 應用程式 (FrontendUrl):https://localhost:7171

Backend.http 檔案可用於測試天氣資料要求。 請注意,BlazorWasmAuth 應用程式必須執行才能測試端點,而且端點會硬式編碼到檔案中。 如需詳細資訊,請參閱在 Visual Studio 2022 中使用 .http 檔案

在應用程式的 Program 檔案中會找到下列設定和組態。

藉由呼叫 AddAuthenticationAddIdentityCookies,即可新增具有 cookie 驗證的使用者身分識別。 呼叫 AddAuthorizationBuilder 即可新增授權檢查服務。

(僅建議用於示範) 應用程式會使用 EF Core 記憶體內部資料庫提供者 來註冊資料庫內容 (AddDbContext)。 記憶體內部資料庫提供者可讓您輕鬆地重新啟動應用程式,並測試註冊和登入的使用者流程。 每次執行都會以全新的資料庫開始,但應用程式會包含測試使用者植入示範程式碼,本文稍後會有說明。 如果資料庫變更為 SQLite,使用者會跨工作階段儲存,但必須透過移轉建立資料庫,如 EF Core 快速入門教學課程所示。 您可以針對生產程式碼使用其他關聯式提供者,例如 SQL Server。

透過呼叫 AddIdentityCoreAddEntityFrameworkStoresAddApiEndpoints,將 Identity 設定為使用 EF Core 資料庫並公開 Identity 端點。

系統會建立跨原始來源資源共用 (CORS) 原則,以允許來自前端和後端應用程式的要求。 如果應用程式設定未提供後援 URL,則系統會為 CORS 原則設定後援 URL:

  • Backend 應用程式 (BackendUrl):https://localhost:5001
  • BlazorWasmAuth 應用程式 (FrontendUrl):https://localhost:5002

會包含 Swagger/OpenAPI 的服務和端點,以測試 Web API 的文件和開發。 如需 NSwag 的詳細資訊,請參閱開始使用 NSwag 和 ASP.NET Core

使用者角色宣告會從 /roles 端點的最小 API 傳送。

會藉由呼叫 MapIdentityApi<AppUser>() 來為 Identity 端點對應路由。

會在中介軟體管線中設定登出端點 (/Logout) 以登出使用者。

若要保護端點,請將 RequireAuthorization 擴充方法新增至路由定義。 若為控制器,請將 [Authorize] 屬性新增至控制器或動作。

針對 DbContext 執行個體的初始化和設定,如需其基本模式的詳細資訊,請參閱 EF Core 文件中的 DbContext 存留期、設定和初始化

前端獨立 Blazor WebAssembly 應用程式的套件和程式碼

獨立 Blazor WebAssembly 前端應用程式會示範用來存取私人網頁的使用者驗證和授權。

套件

應用程式會使用下列 NuGet 套件:

範例應用程式的程式碼

Models 資料夾包含應用程式的模型:

IAccountManagement 介面 (Identity/CookieHandler.cs) 會提供帳戶管理服務。

CookieAuthenticationStateProvider 類別 (Identity/CookieAuthenticationStateProvider.cs) 會處理 cookie 型驗證的狀態,並提供 IAccountManagement 介面所描述的帳戶管理服務實作。 LoginAsync 方法會透過 useCookies 查詢字串值 true 明確啟用 cookie 驗證。 此類別也會管理為已驗證的使用者建立角色宣告。

CookieHandler 類別 (Identity/CookieHandler.cs) 可確保 cookie 認證會隨著每個要求傳送至後端 Web API,以處理 Identity 和維護 Identity 資料存放區。

wwwroot/appsettings.file 會提供後端和前端 URL 端點。

App 元件 會將驗證狀態公開為串聯參數。 如需詳細資訊,請參閱 ASP.NET Core Blazor 驗證和授權

MainLayout 元件NavMenu 元件會使用 AuthorizeView元件,根據使用者的驗證狀態選擇性地顯示內容。

下列元件會處理常見的使用者驗證工作,並使用 IAccountManagement 服務:

PrivatePage 元件 (Components/Pages/PrivatePage.razor) 需要驗證,並且會顯示使用者的宣告。

服務和設定會在 Program 檔案 (Program.cs) 中提供:

  • cookie 處理常式會註冊為具範圍服務。
  • 授權服務已註冊好。
  • 自訂驗證狀態提供者會註冊為具範圍服務。
  • 帳戶管理介面 (IAccountManagement) 已註冊好。
  • 會為已註冊的 HTTP 用戶端執行個體設定基底主機 URL。
  • 會為用於後端 Web API 驗證的已註冊 HTTP 用戶端執行個體設定基底後端 URL。 HTTP 用戶端會使用 cookie 處理常式來確保 cookie 認證會隨著每個要求來傳送。

當使用者的驗證狀態變更時,請呼叫 AuthenticationStateProvider.NotifyAuthenticationStateChanged。 如需範例,請參閱 CookieAuthenticationStateProvider 類別 (Identity/CookieAuthenticationStateProvider.cs)LoginAsyncLogoutAsync 方法。

警告

AuthorizeView 元件會根據使用者是否獲得授權來選擇性地顯示 UI 內容。 放在 AuthorizeView 元件中的 Blazor WebAssembly 應用程式內的所有內容都可以不經驗證就進行探索,因此,在驗證成功後,應該就會從後端伺服器型 Web API 取得敏感性內容。 如需詳細資訊,請參閱以下資源:

測試使用者植入示範

SeedData 類別 (SeedData.cs) 會示範如何建立測試使用者以進行開發。 名為 Leela 的測試使用者會使用電子郵件地址 leela@contoso.com 登入應用程式。 使用者的密碼會設定為 Passw0rd!。 為了授權,已授與 Leela AdministratorManager 角色,這可讓使用者存取位於 /private-manager-page 的管理員頁面,但無法存取位於 /private-editor-page 的編輯器頁面。

警告

絕對不要讓測試使用者程式碼在生產環境中執行。 SeedData.InitializeAsync 只會在 Program 檔案的 Development 環境中呼叫:

if (builder.Environment.IsDevelopment())
{
    await using var scope = app.Services.CreateAsyncScope();
    await SeedData.InitializeAsync(scope.ServiceProvider);
}

角色

由於架構設計問題 (dotnet/aspnetcore #50037),系統不會從 manage/info 端點傳回角色宣告以建立 BlazorWasmAuth 應用程式使用者的使用者宣告。 在 Backend 專案中驗證使用者之後,會透過 CookieAuthenticationStateProvider 類別 (Identity/CookieAuthenticationStateProvider.cs)GetAuthenticationStateAsync 方法中的個別要求來獨立管理角色宣告。

CookieAuthenticationStateProvider 中,會向 Backend 伺服器 API 專案的 /roles 端點提出角色要求。 系統會藉由呼叫 ReadAsStringAsync() 將回應讀入字串中。 JsonSerializer.Deserialize 會將字串還原序列化為自訂 RoleClaim 陣列。 最後,宣告會新增至使用者的宣告集合。

Backend 伺服器 API 的 Program 檔案中,最小 API 會管理 /roles 端點。 系統會將 RoleClaimType 的宣告選取匿名型別,並加以序列化以使用 TypedResults.Json 傳回給 BlazorWasmAuth 專案。

角色端點需要藉由呼叫 RequireAuthorization 來進行授權。 如果您決定不要使用最小 API,而是使用控制器來獲得安全的伺服器 API 端點,請務必在控制器或動作上設定 [Authorize] 屬性

跨網域裝載 (相同網站設定)

範例應用程式已設定為在相同網域裝載這兩個應用程式。 如果您在與 BlazorWasmAuth 應用程式不同的網域裝載 Backend 應用程式,請將會在 Backend 應用程式的 Program 檔案中設定 cookie (ConfigureApplicationCookie) 的程式碼取消註解。 預設值是:

將值變更為:

- options.Cookie.SameSite = SameSiteMode.Lax;
- options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
+ options.Cookie.SameSite = SameSiteMode.None;
+ options.Cookie.SecurePolicy = CookieSecurePolicy.Always;

如需相同網站 cookie 設定的詳細資訊,請參閱下列資源:

防偽造支援

只有 Backend 應用程式中的登出端點 (/logout) 需要注意減輕跨網站偽造要求 (CSRF) 的威脅。

登出端點會檢查空白本文,以防止 CSRF 攻擊。 藉由要求本文,要求必須從 JavaScript 提出,這是存取驗證 cookie 的唯一方式。 表單型 POST 無法存取登出端點。 這可防止惡意網站將使用者登出。

此外,會透過授權 (RequireAuthorization) 來保護端點以防止匿名存取。

BlazorWasmAuth 用戶端應用程式只需要在要求本文中傳遞空白物件 {} 即可。

在登出端點之外,則只有在將表單資料提交至編碼為 application/x-www-form-urlencodedmultipart/form-datatext/plain 的伺服器時,才需要防偽風險降低。 大部分情況下,Blazor 會管理表單的 CSRF 風險降低。 如需詳細資訊,請參閱 ASP.NET Core Blazor 驗證和授權ASP.NET Core Blazor 表單概觀

對其他已啟用 application/json 編碼內容和 CORS 的伺服器 API 端點 (Web API) 所提出的要求不需要 CSRF 保護。 這就是 Backend 應用程式的資料處理 (/data-processing) 端點不需要 CSRF 保護的原因。 角色 (/roles) 端點不需要 CSRF 保護,因為它是不會修改任何狀態的 GET 端點。

疑難排解

記錄

若要針對 Blazor WebAssembly 驗證啟用偵錯或追蹤記錄,請參閱 ASP.NET Core Blazor 記錄

常見錯誤

請檢查每個專案的設定。 確認 URL 正確無誤:

  • Backend 專案
    • appsettings.json
      • BackendUrl
      • FrontendUrl
    • Backend.http: Backend_HostAddress
  • BlazorWasmAuth 專案:wwwroot/appsettings.json
    • BackendUrl
    • FrontendUrl

如果設定顯示正確:

  • 分析應用程式記錄檔。

  • 使用瀏覽器的開發人員工具,檢查 BlazorWasmAuth 應用程式與 Backend 應用程式之間的網路流量。 通常,在提出要求之後,後端應用程式會傳回錯誤訊息或有導致問題的線索訊息給用戶端。 下列文章中可找到開發人員工具指導:

  • Google Chrome (Google 文件)

  • Microsoft Edge

  • Mozilla Firefox (Mozilla 文件)

文件小組會回應文章中的文件意見反應和錯誤。 請使用文章底部的 [開啟文件問題] 連結來開啟問題。 該小組無法提供產品支援。 有數個公用支援論壇可用來協助針對應用程式進行疑難排解。 我們建議下列事項:

上述論壇並非由 Microsoft 擁有或控制。

針對非安全性、非敏感性和非機密可重現架構 BUG 報告,向 ASP.NET Core 產品單位提出問題。 在您徹底調查問題的原因而且無法自行解決,並取得公用支援論壇上社群的協助之前,請勿向產品單位提出問題。 產品單位無法針對因簡單設定錯誤或涉及第三方服務的使用案例而中斷的個別應用程式進行疑難排解。 如果報告本質上是敏感性或機密的,或描述了產品中可能被攻擊者利用的潛在安全性缺陷,請參閱報告安全性問題和錯誤 (dotnet/aspnetcoreGitHub 存放庫)

Cookie 和網站資料

Cookie 和網站資料可以在應用程式更新之間保存,並且干擾測試和疑難排解。 進行應用程式程式碼變更、使用者帳戶變更或應用程式設定變更時,請清除下列內容:

  • 使用者登入 cookie
  • 應用程式 cookie
  • 快取和儲存的網站資料

防止殘留 cookie 和網站資料干擾測試和疑難排解的其中一種方法是:

  • 設定瀏覽器
    • 使用瀏覽器進行測試,您可以設定在每次關閉瀏覽器時刪除所有 cookie 和網站資料。
    • 請確定瀏覽器已手動關閉或由 IDE 關閉,以便對應用程式、測試使用者或提供者設定進行任何變更。
  • 使用自訂命令,在 Visual Studio 中以私人模式或無痕模式開啟瀏覽器:
    • 從 Visual Studio 的 [執行] 按鈕開啟 [瀏覽方式] 對話方塊。
    • 選取新增按鈕。
    • 在 [程式] 欄位中提供瀏覽器的路徑。 下列可執行檔路徑是 Windows 10 的一般安裝位置。 如果您的瀏覽器安裝在不同的位置,或您不是使用 Windows 10,請提供瀏覽器可執行檔的路徑。
      • Microsoft Edge:C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
      • Google Chrome:C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
      • Mozilla Firefox:C:\Program Files\Mozilla Firefox\firefox.exe
    • 在 [引數] 欄位中,提供瀏覽器用來在私人模式或無痕模式中開啟的命令列選項。 某些瀏覽器需要應用程式的 URL。
      • Microsoft Edge:使用 -inprivate
      • Google Chrome:使用 --incognito --new-window {URL},其中預留位置 {URL} 是要開啟的 URL (例如 https://localhost:5001)。
      • Mozilla Firefox:使用 -private -url {URL},其中預留位置 {URL} 是要開啟的 URL (例如 https://localhost:5001)。
    • 在 [自訂名稱] 欄位中提供名稱。 例如: Firefox Auth Testing
    • 選取確定按鈕。
    • 若要避免針對使用應用程式測試的每個反覆項目選取瀏覽器設定檔,請使用 [設為預設值] 按鈕,將設定檔設定為預設值。
    • 請確定瀏覽器已由 IDE 關閉,以便對應用程式、測試使用者或提供者設定進行任何變更。

應用程式升級

在升級開發電腦上的 .NET Core SDK 或變更應用程式內的套件版本之後,正常運作的應用程式便立即發生失敗。 在某些情況下,執行主要升級時,不一致的套件可能會中斷應用程式。 大多數這些問題都可依照下列指示來進行修正:

  1. 從命令殼層執行 dotnet nuget locals all --clear,以清除本機系統的 NuGet 套件快取。
  2. 刪除專案的 binobj 資料夾。
  3. 還原並重建專案。
  4. 在重新部署應用程式之前,請先刪除伺服器上部署資料夾中的所有檔案。

注意

不支援使用與應用程式目標框架不相容的套件版本。 如需套件的詳細資訊,請使用 NuGet 資源庫FuGet 套件總管

檢查使用者的宣告

若要針對使用者宣告的問題進行疑難排解,下列 UserClaims 元件可以直接在應用程式中使用,或作為進一步自訂的基礎。

UserClaims.razor

@page "/user-claims"
@using System.Security.Claims
@attribute [Authorize]

<PageTitle>User Claims</PageTitle>

<h1>User Claims</h1>

**Name**: @AuthenticatedUser?.Identity?.Name

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthenticationState { get; set; }

    public ClaimsPrincipal? AuthenticatedUser { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (AuthenticationState is not null)
        {
            var state = await AuthenticationState;
            AuthenticatedUser = state.User;
        }
    }
}

其他資源