共用方式為


使用 Identity Server 來保護託管 ASP.NET Core Blazor WebAssembly 應用程式的安全

本文說明如何建立一個託管的 Blazor WebAssembly 解決方案 (該解決方案使用 Duende Identity Server 來對使用者和 API 呼叫進行驗證)。

重要

Duende Software 可能會要求您支付授權費用才能在生產環境中使用 Duende Identity 伺服器。 如需詳細資訊,請參閱從 ASP.NET Core 5.0 移轉至 6.0

注意

若要將一個獨立或託管的 Blazor WebAssembly 應用程式設定為使用現有的外部 Identity Server 實例,請遵循「使用驗證程式庫保護 ASP.NET Core Blazor WebAssembly 獨立應用程式」中的指引。

在閱讀本文之後,如需其他安全性案例涵蓋範圍,請參閱 ASP.NET Core Blazor WebAssembly 其他安全性案例

逐步解說

逐步解說的子區段說明如何:

  • 建立 Blazor 應用程式
  • 執行應用程式

建立 Blazor 應用程式

若要建立一個含驗證機制的新 Blazor WebAssembly 專案:

  1. 建立新專案。

  2. 選擇 Blazor WebAssembly 應用程式範本。 選取 [下一步] 。

  3. 提供一個不使用破折號的專案名稱。 確認位置正確無誤。 選取 [下一步] 。

    避免在專案名稱中使用破折號 (-),這會破壞 OIDC 應用程式識別碼的形成。 Blazor WebAssembly 專案範本中的邏輯會使用解決方案組態中 OIDC 應用程式識別碼的專案名稱,而且 OIDC 應用程式識別碼中不允許破折號。 Pascal 命名法的大小寫 (BlazorSample) 或底線 (Blazor_Sample) 是可接受的替代項。

  4. [其他資訊] 對話方塊中,選取 [個人帳戶] 作為 [驗證類型],以使用 ASP.NET Core Identity 系統將使用者儲存在應用程式內。

  5. 選取 [託管的 ASP.NET Core] 核取方塊。

  6. 選取 [建立] 按鈕以建立應用程式。

執行應用程式

Server 專案執行應用程式。 使用 Visual Studio 時,請執行下列其中一項:

  • 選取 [執行] 按鈕旁的下拉式箭號。 從下拉式清單中開啟 [設定啟始專案]。 選取 [單一啟始專案] 選項。 確認或變更啟始專案的專案為 Server 專案。

  • 在使用下列任一方法啟動應用程式之前,請確認 Server 專案已在 [方案總管] 中醒目顯示:

    • 選取 [執行] 按鈕。
    • 從功能表使用 [偵錯]>[開始偵錯]
    • 請按 F5
  • 在命令殼層中,瀏覽至方案的 Server 專案資料夾。 執行 dotnet watch (或 dotnet run) 命令。

方案的組件

本節描述從 Blazor WebAssembly 專案範本產生的方案組件,並描述如何設定方案的 ClientServer 專案以供參考。 如果您使用逐步解說一節中的指導來建立應用程式,則本節中沒有針對基本工作應用程式可遵循的任何特定指導。 本節中的指導有助於更新應用程式以驗證和授權使用者。 不過,更新應用程式的替代方法是從逐步解說一節中的指導建立新的應用程式,並將應用程式的元件、類別和資源移至新的應用程式。

Server 應用程式服務

本節與解決方案的 Server 應用程式有關。

會註冊下列的服務。

  • Program 檔案中:

    • Entity Framework Core 和 ASP.NET Core Identity:

      builder.Services.AddDbContext<ApplicationDbContext>(options =>
          options.UseSqlite( ... ));
      builder.Services.AddDatabaseDeveloperPageExceptionFilter();
      
      builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
              options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<ApplicationDbContext>();
      
    • 搭配附加的 AddApiAuthorization 協助程式方法 (它在 Identity Server 之上設定了預設的 ASP.NET Core 慣例) 的 Identity 伺服器:

      builder.Services.AddIdentityServer()
          .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
      
    • 搭配附加的 AddIdentityServerJwt 協助程式方法 (它會設定應用程式以驗證 Identity Server 所產生的 JWT 權杖) 的驗證:

      builder.Services.AddAuthentication()
          .AddIdentityServerJwt();
      
  • Startup.csStartup.ConfigureServices 中:

    • Entity Framework Core 和 ASP.NET Core Identity:

      services.AddDbContext<ApplicationDbContext>(options =>
          options.UseSqlite(
              Configuration.GetConnectionString("DefaultConnection")));
      
      services.AddDefaultIdentity<ApplicationUser>(options => 
              options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<ApplicationDbContext>();
      

      警告

      請勿在用戶端程式代碼中儲存應用程式密碼、連接字串、認證、密碼、個人標識元(PIN)、私人 C#/.NET 程式代碼或私鑰/令牌,這一律不安全。 在測試/預備和生產環境中,伺服器端 Blazor 程序代碼和 Web API 應該使用安全驗證流程,以避免在專案程式代碼或組態檔內維護認證。 在本機開發測試之外,建議您避免使用環境變數來儲存敏感數據,因為環境變數不是最安全的方法。 針對本機開發測試, 建議使用秘密管理員工具 來保護敏感數據。 如需詳細資訊,請參閱 安全地維護敏感數據和認證

    • 搭配附加的 AddApiAuthorization 協助程式方法 (它在 Identity Server 之上設定了預設的 ASP.NET Core 慣例) 的 Identity 伺服器:

      services.AddIdentityServer()
          .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
      
    • 搭配附加的 AddIdentityServerJwt 協助程式方法 (它會設定應用程式以驗證 Identity Server 所產生的 JWT 權杖) 的驗證:

      services.AddAuthentication()
          .AddIdentityServerJwt();
      

注意

註冊單一驗證配置時,驗證配置會自動用作應用程式的預設配置,而且不需要向 AddAuthentication 或透過 AuthenticationOptions 陳述該配置。 如需詳細資訊,請參閱 ASP.NET Core 驗證概觀ASP.NET Core 公告 (aspnet/Announcements #490)

  • Program 檔案中:
  • Startup.csStartup.Configure 中:
  • Identity Identity Server 中介軟體會公開 OpenID Connect (OIDC) 端點:

    app.UseIdentityServer();
    
  • 「驗證中介軟體」負責驗證要求認證,並在要求內容上設定使用者:

    app.UseAuthentication();
    
  • 「授權中介軟體」可啟用授權功能:

    app.UseAuthorization();
    

API 授權

本節與解決方案的 Server 應用程式有關。

AddApiAuthorization 協助程式方法可為 ASP.NET Core 案例設定 Identity Server。 Identity Server 是一個強大且可擴充的架構,用於處理應用程式的安全性問題。 Identity Server 會對最常見的案例帶來不必要的複雜性。 因此,會提供一組慣例和組態選項,我們認為這是一個很好的起點。 一旦您的驗證需求改變了,Identity Server 的完整功能就可以用來自訂驗證以符合應用程式的需求。

為與 Identity Server 並存的 API 新增驗證處理常式

本節與解決方案的 Server 應用程式有關。

AddIdentityServerJwt 協助程式方法可將應用程式的原則配置設定為預設的驗證處理常式。 此原則會設定為允許 Identity 處理路由至 /Identity 下 Identity URL 空間中任何子路徑的所有要求。 JwtBearerHandler 會處理所有其他要求。 此外,這個方法還會:

  • 向 Identity Server 註冊 API 資源,其預設範圍為 {PROJECT NAME}API (其中 {PROJECT NAME} 預留位置是應用程式建立時的專案名稱)。
  • 設定「JWT 持有人權杖中介軟體」,以驗證 Identity Server 為應用程式所發出的權杖。

氣象預報控制站

本節與解決方案的 Server 應用程式有關。

WeatherForecastController (Controllers/WeatherForecastController.cs) 中,[Authorize] 屬性會套用至類別。 此屬性工作表示使用者必須根據預設原則來獲得授權才能存取資源。 預設的授權原則會設定為使用預設的驗證配置 (其由 AddIdentityServerJwt 設定)。 此協助程式方法會設定 JwtBearerHandler 作為對應用程式要求的預設處理常式。

應用程式資料庫內容

本節與解決方案的 Server 應用程式有關。

ApplicationDbContext (Data/ApplicationDbContext.cs) 中,DbContext 會擴充 ApiAuthorizationDbContext<TUser> 以包含 Identity Server 的結構描述。 ApiAuthorizationDbContext<TUser> 衍生自 IdentityDbContext

若要取得資料庫結構描述的完全控制權,請從其中一個可用的 IdentityDbContext 繼承,並透過在 OnModelCreating 方法中呼叫 builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value) 來將內容設定為包含 Identity 結構描述。

OIDC 設定控制器

本節與解決方案的 Server 應用程式有關。

OidcConfigurationController (Controllers/OidcConfigurationController.cs) 中,會佈建用戶端端點來提供 OIDC 參數。

應用程式設定

本節與解決方案的 Server 應用程式有關。

在專案根目錄的應用程式設定檔 (appsettings.json) 中,IdentityServer 區段描述了已設定用戶端的清單。 在下列的範例中,有一個單一的用戶端。 用戶端名稱對應於 Client 應用程式的組件名稱,並依慣例對應至 OAuth ClientId 參數。 設定檔指出正在設定的應用程式類型。 設定檔會在內部用來驅動可簡化伺服器設定過程的慣例。

"IdentityServer": {
  "Clients": {
    "{ASSEMBLY NAME}": {
      "Profile": "IdentityServerSPA"
    }
  }
}

{ASSEMBLY NAME} 預留位置是 Client 應用程式的組件名稱 (例如,BlazorSample.Client)。

驗證套件

本節與解決方案的 Client 應用程式有關。

在建立應用程式以使用個別使用者帳戶 (Individual) 時,該應用程式會自動接收 Microsoft.AspNetCore.Components.WebAssembly.Authentication 套件的套件參考。 套件提供一組基本類型,可協助應用程式驗證使用者,並取得權杖來呼叫受保護的 API。

如果將驗證新增至應用程式,請手動將 Microsoft.AspNetCore.Components.WebAssembly.Authentication 套件新增至應用程式。

注意

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

HttpClient 設定

本節與解決方案的 Client 應用程式有關。

Program 檔案中,會設定一個具名的 HttpClient,以在向伺服器 API 發出要求時提供包含存取權杖的 HttpClient 實例。 在建立解決方案時,該具名的 HttpClient{PROJECT NAME}.ServerAPI(其中 {PROJECT NAME} 預留位置是專案的名稱)。

builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI", 
        client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("{PROJECT NAME}.ServerAPI"));

{PROJECT NAME} 預留位置是方案建立時的專案名稱。 例如,提供 BlazorSample 的專案名稱會產生一個名為 BlazorSample.ServerAPIHttpClient

注意

如果您要設定 Blazor WebAssembly 應用程式以使用不屬於託管 Blazor 解決方案的現有 Identity Server 實例,請將 HttpClient 基底位址註冊從 IWebAssemblyHostEnvironment.BaseAddress (builder.HostEnvironment.BaseAddress) 變更為伺服器應用程式的 API 授權端點 URL。

API 授權支援

本節與解決方案的 Client 應用程式有關。

對使用者驗證的支援會透過 Microsoft.AspNetCore.Components.WebAssembly.Authentication 套件內所提供的擴充方法來插入到服務容器中。 此方法會設定應用程式與現有授權系統互動所需的服務。

builder.Services.AddApiAuthorization();

應用程式的設定會依照慣例從 _configuration/{client-id} 載入。 依照慣例,用戶端識別碼會設定為應用程式的組件名稱。 您可以藉由使用選項呼叫多載,將此 URL 變更為指向個別的端點。

Imports 檔案

本節與解決方案的 Client 應用程式有關。

Microsoft.AspNetCore.Components.Authorization 命名空間可透過 _Imports.razor 檔案在整個應用程式中使用:

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@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 {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index 頁面

本節與解決方案的 Client 應用程式有關。

[索引] 頁面 (wwwroot/index.html) 頁面包含一個指令碼,定義 JavaScript 中的 AuthenticationServiceAuthenticationService 會處理 OIDC 通訊協定的低階詳細資料。 應用程式會在內部呼叫指令碼中定義的方法,來執行驗證作業。

<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>

App 元件

本節與解決方案的 Client 應用程式有關。

App 元件 (App.razor) 類似於在 Blazor Server 應用程式中找到的 App 元件:

由於 ASP.NET Core 版本之間的架構變更,所以本節不會顯示 App 元件的 Razor 標記 (App.razor)。 若要檢查指定版本的元件標記,請使用下列任一方法:

  • 針對您想要使用的 ASP.NET Core 版本,從預設 Blazor WebAssembly 專案範本建立要佈建以進行驗證的應用程式。 在產生的應用程式中檢查 App 元件 (App.razor)。

  • 參考來源中檢查 App 元件 (App.razor)。 從分支選取器中選擇版本,然後在存放庫的 ProjectTemplates 資料夾中搜尋該元件,因為 App 元件的位置多年來已經移動。

    注意

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

RedirectToLogin 元件

本節與解決方案的 Client 應用程式有關。

RedirectToLogin 元件 (RedirectToLogin.razor):

  • 管理將未經授權的使用者重新導向至登入頁面。
  • 使用者嘗試存取的目前 URL 會如此進行維護,以便在驗證成功時返回該頁面:
    • .NET 7 或更新版本中 ASP.NET Core 中的瀏覽歷程記錄狀態
    • .NET 6 或更早版本中 ASP.NET Core 中的查詢字串。

參考來源中檢查 RedirectToLogin 元件。 該元件的位置已隨著時間而變更,因此請使用 GitHub 搜尋工具來找出該元件。

注意

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

LoginDisplay 元件

本節與解決方案的 Client 應用程式有關。

LoginDisplay 元件 (LoginDisplay.razor) 是在 MainLayout 元件 (MainLayout.razor) 中進行轉譯,並且管理下列行為:

  • 針對已驗證的使用者:
    • 顯示目前的使用者名稱。
    • 提供 ASP.NET Core Identity 中使用者設定檔頁面的連結。
    • 提供登出應用程式的按鈕。
  • 針對匿名使用者:
    • 提供註冊的選項。
    • 提供登入的選項。

由於 ASP.NET Core 版本之間的架構變更,所以本節不會顯示 LoginDisplay 元件的 Razor 標記。 若要檢查指定版本的元件標記,請使用下列任一方法:

  • 針對您想要使用的 ASP.NET Core 版本,從預設 Blazor WebAssembly 專案範本建立要佈建以進行驗證的應用程式。 在產生的應用程式中檢查 LoginDisplay 元件。

  • 參考來源中檢查 LoginDisplay 元件。 該元件的位置已隨著時間而變更,因此請使用 GitHub 搜尋工具來找出該元件。 使用等於 trueHosted 範本化內容。

    注意

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

Authentication 元件

本節與解決方案的 Client 應用程式有關。

Authentication 元件 (Pages/Authentication.razor) 所產生的頁面會定義處理不同驗證階段所需的路由。

RemoteAuthenticatorView 元件:

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

<RemoteAuthenticatorView Action="@Action" />

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

注意

.NET 6 或更新版本中的 ASP.NET Core 中支援可為 Null 參考型別 (NRT) 和 .NET 編譯器 Null 狀態靜態分析。 在 .NET 6 中的 ASP.NET Core 發行之前,string 類型並沒有 Null 類型指定 (?)。

FetchData 元件

本節與解決方案的 Client 應用程式有關。

FetchData 元件會示範如何:

  • 佈建存取權杖。
  • 使用該存取權杖來呼叫 Server 應用程式中的受保護資源 API。

@attribute [Authorize] 指示詞會向 Blazor WebAssembly 授權系統指示使用者必須獲得授權才能存取此元件。 Client 應用程式中該屬性的存在不會阻止在沒有適當認證的情況下呼叫伺服器上的 API。 Server 應用程式也必須在適當的端點上使用 [Authorize] 才能正確地提供保護。

IAccessTokenProvider.RequestAccessToken 負責要求索取可新增至要求以呼叫 API 的存取權杖。 如果權杖已快取或服務能夠在無需使用者互動的情況下提供新的存取權杖,則要求索取權杖會成功。 否則,要求索取權杖會失敗並出現 AccessTokenNotAvailableException (它會在 try-catch 陳述式中攔截)。

為了取得要包含在要求中的實際權杖,應用程式必須透過呼叫 tokenResult.TryGetToken(out var token) 來檢查要求索取是否成功。

如果要求索取成功,則會使用存取權杖來填入 token 變數。 權杖的 AccessToken.Value 屬性會公開要包含在 Authorization 要求標頭中的常值字串。

如果因在沒有使用者互動的情況下無法提供權杖而要求索取失敗,則:

  • .NET 7 或更新版本中的 ASP.NET Core:應用程式會使用指定的 AccessTokenResult.InteractionOptions 瀏覽至 AccessTokenResult.InteractiveRequestUrl 以允許重新整理存取權杖。
  • .NET 6 或更早版本中的 ASP.NET Core:權杖結果會包含重新導向 URL。 瀏覽至此 URL 會將使用者帶到登入頁面,並在驗證成功之後回到目前的頁面。
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

Linux 上的 Azure App Service

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

具有 API 授權的名稱和角色宣告

自訂使用者 Factory

Client 應用程式中,建立一個自訂使用者 Factory。 Identity Server 會在單一 role 宣告中以 JSON 陣列的形式傳送多個角色。 單一角色會在宣告中以字串值的形式傳送。 Factory 會為每個使用者的角色建立個別的 role 宣告。

CustomUserFactory.cs

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

public class CustomUserFactory(IAccessTokenProviderAccessor accessor)
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var user = await base.CreateUserAsync(account, options);

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            var identity = (ClaimsIdentity)user.Identity;
            var roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();

            if (roleClaims.Any())
            {
                foreach (var existingClaim in roleClaims)
                {
                    identity.RemoveClaim(existingClaim);
                }

                var rolesElem = 
                    account.AdditionalProperties[identity.RoleClaimType];

                if (options.RoleClaim is not null && rolesElem is JsonElement roles)
                {
                    if (roles.ValueKind == JsonValueKind.Array)
                    {
                        foreach (var role in roles.EnumerateArray())
                        {
                            var roleValue = role.GetString();

                            if (!string.IsNullOrEmpty(roleValue))
                            {
                                identity.AddClaim(
                                  new Claim(options.RoleClaim, roleValue));
                            }
        
                        }
                    }
                    else
                    {
                        var roleValue = roles.GetString();

                        if (!string.IsNullOrEmpty(roleValue))
                        {
                            identity.AddClaim(
                              new Claim(options.RoleClaim, roleValue));
                        }
                    }
                }
            }
        }

        return user;
    }
}

Client 應用程式中,在 Program 檔案中註冊 Factory:

builder.Services.AddApiAuthorization()
    .AddAccountClaimsPrincipalFactory<CustomUserFactory>();

Server 應用程式中,呼叫 Identity 建立器上的 AddRoles (這會新增與角色相關的服務)。

Program 檔案中:

using Microsoft.AspNetCore.Identity;

...

builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

Startup.cs 中:

using Microsoft.AspNetCore.Identity;

...

services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

設定 Identity 伺服器

使用下列其中一個方法:

API 授權選項

Server 應用程式中:

  • 設定 Identity Server 以將 namerole 宣告放入識別碼權杖和存取權杖中。
  • 防止 JWT 權杖處理常式中角色的預設對應。

Program 檔案中:

using System.IdentityModel.Tokens.Jwt;

...

builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
        options.IdentityResources["openid"].UserClaims.Add("name");
        options.ApiResources.Single().UserClaims.Add("name");
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
    });

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

Startup.cs 中:

using System.IdentityModel.Tokens.Jwt;
using System.Linq;

...

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
        options.IdentityResources["openid"].UserClaims.Add("name");
        options.ApiResources.Single().UserClaims.Add("name");
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
    });

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

設定檔服務

Server 應用程式中,建立 ProfileService 作實。

ProfileService.cs

using IdentityModel;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;

public class ProfileService : IProfileService
{
    public ProfileService()
    {
    }

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
        context.IssuedClaims.AddRange(nameClaim);

        var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
        context.IssuedClaims.AddRange(roleClaims);

        await Task.CompletedTask;
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        await Task.CompletedTask;
    }
}

Server 應用程式中,在 Program 檔案中註冊設定檔服務:

using Duende.IdentityServer.Services;

...

builder.Services.AddTransient<IProfileService, ProfileService>();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

Server 應用程式中,在 Startup.csStartup.ConfigureServices 中註冊設定檔服務:

using IdentityServer4.Services;

...

services.AddTransient<IProfileService, ProfileService>();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

使用授權機制

Client 應用程式中,元件授權方法目前正常運作。 元件中的任何授權機制都可以使用角色來授權使用者:

User.Identity.Name 會在 Client 應用程式中填入使用者的使用者名稱 (通常是其登入電子郵件地址)。

UserManagerSignInManager

當 Server 應用程式需要以下項目時,請設定使用者識別碼宣告類型:

在 .NET 6 或更高版本的 ASP.NET Core 的 Program.cs 中:

using System.Security.Claims;

...

builder.Services.Configure<IdentityOptions>(options => 
    options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

Startup.ConfigureServices (對於 ASP.NET Core 6.0 之前的版本) 中:

using System.Security.Claims;

...

services.Configure<IdentityOptions>(options => 
    options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

下列的 WeatherForecastController 會在呼叫 Get 方法時記錄 UserName

注意

下列範例使用了一個以檔案為範圍的命名空間,這是 C# 10 或更新版本(.NET 6 或更高版本)的一個功能。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using BlazorSample.Server.Models;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly UserManager<ApplicationUser> userManager;

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", 
        "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, 
        UserManager<ApplicationUser> userManager)
    {
        this.logger = logger;
        this.userManager = userManager;
    }

    [HttpGet]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        var rng = new Random();

        var user = await userManager.GetUserAsync(User);

        if (user != null)
        {
            logger.LogInformation("User.Identity.Name: {UserIdentityName}", user.UserName);
        }

        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

在前述範例中:

  • Server 專案的命名空間是 BlazorSample.Server
  • Shared 專案的命名空間是 BlazorSample.Shared

使用自訂網域和憑證以在 Azure App Service 中進行託管

下列指引說明:

  • 如何使用自訂網域將具有 Identity Server 的受託管 Blazor WebAssembly 應用程式部署到 Azure App Service 中。
  • 如何建立和使用 TLS 憑證來與瀏覽器進行 HTTPS 通訊協定的通訊。 儘管本指引重點示範如何搭配自訂網域使用憑證,但本指引同樣適用於使用預設的 Azure App 網域 (例如 contoso.azurewebsites.net)。

針對此裝載案例,請勿Duende Identity Server 的令牌簽署密鑰和網站的 HTTPS 安全與瀏覽器通訊使用相同的憑證:

  • 針對這兩個需求使用不同的憑證是一種很好的安全性做法,因為它會針對每種用途隔離私密金鑰。
  • 用於與瀏覽器通訊的 TLS 憑證是獨立受管理的,不會影響 Identity Server 的權杖簽署。
  • Azure Key Vault 向 App Service 應用程式提供憑證以進行自訂網域繫結時,Identity Server 無法從 Azure Key Vault 取得相同的憑證以進行權杖簽署。 儘管可以將 Identity Server 設定為使用實體路徑中的相同 TLS 憑證,但將安全性憑證放入原始檔控制中是一種不好的做法,在大多數情況下都應該避免

在下列指引中,會在 Azure Key Vault 中只針對 Identity Server 權杖簽署建立一個自我簽署憑證。 Identity Server 設定會透過應用程式的 CurrentUser>My 憑證存放區來使用金鑰保存庫憑證。 用於含自訂網域的 HTTPS 流量的其他憑證是與 Identity Server 簽署憑證分開建立和設定的。

若要將應用程式、Azure App Service 和 Azure Key Vault 設定為使用自訂網域和 HTTPS 進行託管,請執行下列動作:

  1. 建立一個方案層級為 Basic B1 或更高的 App Service 方案。 App Service 需要 Basic B1 或更高的服務層級才能使用自訂網域。

  2. 使用您的組織控制的網站的完整網域名稱 (FQDN) 的通用名稱 (例如 www.contoso.com),為網站的安全瀏覽器通訊 (HTTPS 通訊協定) 建立一個 PFX 憑證。 透過下列項目來建立憑證:

    • 金鑰使用
      • 數位簽章驗證 (digitalSignature)
      • 金鑰編密 (keyEncipherment)
    • 增強/擴充金鑰使用
      • 用戶端驗證 (1.3.6.1.5.5.7.3.2)
      • 伺服器驗證 (1.3.6.1.5.5.7.3.1)

    若要建立憑證,請使用下列其中一種方法或任何其他適當的工具或線上服務:

    記下密碼 (稍後會用來將憑證匯入 Azure Key Vault 中)。

    如需 Azure Key Vault 憑證的詳細資訊,請參閱 Azure Key Vault:憑證

  3. 在您的 Azure 訂用帳戶中建立一個新的 Azure Key Vault 或使用現有的金鑰保存庫。

  4. 在金鑰保存庫的憑證區域中,匯入 PFX 網站憑證。 記錄憑證的指紋 (稍後會在應用程式的設定中使用)。

  5. 在 Azure Key Vault 中,為 Identity Server 權杖簽署產生一個新的自我簽署憑證。 為憑證提供憑證名稱主體主體會指定為 CN={COMMON NAME} (其中 {COMMON NAME} 預留位置是憑證的一般名稱)。 一般名稱可以是任何的英數字串。 例如,CN=IdentityServerSigning 是有效的憑證主體。 在 [核發原則>進階原則設定]中,使用預設的設定。 記錄憑證的指紋 (稍後會在應用程式的設定中使用)。

  6. 瀏覽至 Azure 入口網站中的 Azure App Service,並使用下列設定來建立新的 App Service:

    • [發佈] 設定為 Code
    • [執行階段堆疊] 設定為應用程式的執行階段。
    • 針對 [Sku 和大小],確認 App Service 層級為 Basic B1 或更高。 App Service 需要 Basic B1 或更高的服務層級才能使用自訂網域。
  7. 在 Azure 建立 App Service 之後,開啟應用程式的 [設定],並新增新的應用程式設定,以指定先前記錄的憑證指紋。 應用程式設定索引碼為 WEBSITE_LOAD_CERTIFICATES。 使用逗號分隔應用程式設定值中的憑證指紋,如下列範例所示:

    • 索引鍵:WEBSITE_LOAD_CERTIFICATES
    • 值:57443A552A46DB...D55E28D412B943565,29F43A772CB6AF...1D04F0C67F85FB0B1

    在 Azure 入口網站中,儲存應用程式設定分為兩個步驟:儲存 WEBSITE_LOAD_CERTIFICATES 索引鍵-值設定,然後選取刀鋒視窗頂部的 [儲存] 按鈕。

  8. 選取應用程式的 [TLS/SSL 設定]。 選取 [私密金鑰憑證 (.pfx)]。 使用 [匯入金鑰保存庫憑證] 程序。 使用該程序兩次 以匯入網站的 HTTPS 通訊憑證和網站的自我簽署 Identity Server 權杖簽署憑證。

  9. 瀏覽至 [自訂網域] 刀鋒視窗。 在您的網域註冊機構網站上,使用 [IP 位址][自訂網域驗證識別碼] 來設定網域。 一般的網域設定包括:

    • 一個 A 記錄,其主機@,而其值為來自 Azure 入口網站的 IP 位址。
    • 一個 TXT 記錄,其主機asuid,而其值為由 Azure 產生並由 Azure 入口網站所提供的驗證 ID。

    確定您在網域註冊機構的網站上正確儲存變更。 某些註冊機構網站需要兩個步驟的程式來儲存網域記錄:個別儲存一或多個記錄,然後再使用個別的按鈕更新網域的註冊資料。

  10. 返回 Azure 入口網站中的 [自訂網域] 刀鋒視窗。 選取 [新增自訂網域]。 選取 [A 記錄] 選項。 提供網域,然後選取 [驗證]。 如果網域記錄正確並在網際網路上傳播,則入口網站可讓您選取 [新增自訂網域] 按鈕。

    在網域註冊機構處理完網域註冊變更後,可能需要幾天的時間才能在網際網路網域名稱伺服器 (DNS) 上傳播。 如果網域記錄在三個工作天內未更新,請與網域註冊機構確認記錄是否正確設定,並聯絡其客戶支援中心。

  11. [自訂網域] 刀鋒視窗中,網域的 [SSL 狀態] 會標示為 Not Secure。 選取 [新增繫結] 連結。 從金鑰保管庫中選取網站 HTTPS 憑證以進行自訂網域繫結。

  12. 在 Visual Studio 中,開啟 Server 專案的應用程式設定檔案 (appsettings.jsonappsettings.Production.json)。 在 Identity Server 設定中,新增下列 Key 區段。 為 Name 索引碼指定自我簽署憑證主體。 在下列範例中,金鑰保存庫中所指派的憑證通用名稱是 IdentityServerSigning (這會產生 主體CN=IdentityServerSigning):

    "IdentityServer": {
    
      ...
    
      "Key": {
        "Type": "Store",
        "StoreName": "My",
        "StoreLocation": "CurrentUser",
        "Name": "CN=IdentityServerSigning"
      }
    },
    
  13. 在 Visual Studio 中,為 Server 專案建立一個 Azure App Service 發佈設定檔。 從功能表列中選取:[建置>發佈>新建>Azure>Azure App Service (Windows 或 Linux)]。 當 Visual Studio 連線到 Azure 訂用帳戶時,您可以依資源類型設定 Azure 資源的檢視。 在 [Web 應用程式] 清單中瀏覽,以找到應用程式的 App Service 並加以選取。 選取完成

  14. 當 Visual Studio 返回 [發佈] 視窗時,會自動偵測金鑰保存庫和 SQL Server 資料庫服務相依性。

    金鑰保存庫服務不需要對預設的設定進行組態變更。

    出於測試目的,應用程式的本機 SQLite 資料庫(由 Blazor 範本設定)可以隨應用程式一起部署,無需額外的設定。 在生產環境中為 Identity Server 設定不同的資料庫超出了本文的範圍。 如需詳細資訊,請參閱下列文件集中的資料庫資源:

  15. 選取視窗頂端部署設定檔名稱下的 [編輯] 連結。 將目的地 URL 變更為網站的自訂網域 URL (例如,https://www.contoso.com)。 儲存設定。

  16. 發行應用程式。 Visual Studio 會開啟一個瀏覽器視窗,並要求其自訂網域中的網站。

Azure 文件包含有關在 App Service 中使用 Azure 服務和自訂網域搭配 TLS 繫結的其他詳細資料,包括使用 CNAME 記錄 (而非 A 記錄) 的相關資訊。 如需詳細資訊,請參閱以下資源:

我們建議在 Azure 入口網站中變更應用程式、應用程式設定或 Azure 服務之後,對每個應用程式測試執行使用新的私人模式瀏覽器視窗 (例如,Microsoft Edge InPrivate 模式或 Google Chrome 無痕模式)。 即使網站的設定正確,先前測試執行中的殘留 cookie 也可能會在測試網站時導致驗證或授權失敗。 有關如何設定 Visual Studio 來為每個測試執行開啟新的私人瀏覽器視窗的詳細資訊,請參閱 Cookie 和網站資料一節。

當在 Azure 入口網站中變更 App Service 設定時,更新通常很快便生效,但不會立即生效。 有時,您必須等待一小段時間讓 App Service 重新啟動才能使設定變更生效。

如果對 Identity Server 金鑰簽署憑證載入問題進行疑難排解,請在 Azure 入口網站 Kudu PowerShell 命令殼層中執行下列命令。 此命令提供一個應用程式可從 CurrentUser>My 憑證存放區存取的憑證清單。 輸出會包含偵錯應用程式時有用的憑證主體和指紋:

Get-ChildItem -path Cert:\CurrentUser\My -Recurse | Format-List DnsNameList, Subject, Thumbprint, EnhancedKeyUsageList

疑難排解

記錄

若要啟用 Blazor WebAssembly 驗證的偵錯或追蹤記錄,請參閱 ASP.NET Core Blazor 記錄用戶端驗證記錄一節,並將發行項版本選取器設定為 ASP.NET Core 7.0 或更新版本。

常見錯誤

  • 應用程式或 Identity 提供者 (IP) 的設定錯誤

    最常見的錯誤是由不正確的設定所造成。 以下是一些範例:

    • 視案例的需求而定,遺漏或不正確的授權單位、執行個體、租用戶識別碼、租用戶網域、用戶端識別碼或重新導向 URI 會防止應用程式驗證用戶端。
    • 不正確的要求範圍會防止用戶端存取伺服器 Web API 端點。
    • 不正確或遺漏伺服器 API 權限會防止用戶端存取伺服器 Web API 端點。
    • 在與 IP 應用程式註冊的重新導 URI 中設定的連接埠不同的連接埠上執行應用程式。 請注意,Microsoft Entra ID 和在 localhost 開發測試位址執行的應用程式不需要連接埠,但應用程式的連接埠設定和應用程式執行的連接埠必須與非 localhost 位址相符。

    本文指導的設定區段顯示正確設定的範例。 仔細查看文章中有關尋找應用程式和 IP 設定錯誤的每個區段。

    如果設定顯示正確:

    • 分析應用程式記錄檔。

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

    • 對於使用 JSON Web 權杖 (JWT) 的 Blazor 版本,根據問題發生的位置,對用於驗證用戶端或存取伺服器 Web API 的權杖內容進行解碼。 如需詳細資訊,請參閱 檢查 JSON Web 權杖 (JWT) 的內容

    文件小組會回應文章中的文件意見反應和 BUG (從此頁面意見反應區段開啟問題),但是無法提供產品支援。 有數個公用支援論壇可用來協助針對應用程式進行疑難排解。 我們建議下列事項:

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

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

  • ME-ID 未經授權的用戶端

    info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] Authorization failed. 不符合以下需求: DenyAnonymousAuthorizationRequirement:需要已驗證的使用者。

    ME-ID 的登入回呼錯誤:

    • 錯誤: unauthorized_client
    • 描述:AADB2C90058: The provided application is not configured to allow public clients.

    若要解決此錯誤:

    1. 在 Azure 入口網站中,存取應用程式的資訊清單
    2. allowPublicClient 屬性設定為 nulltrue

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 套件總管

執行 Server 應用程式

測試裝載 Blazor WebAssembly方案並且進行疑難排解時,請確定您是從 Server 專案執行應用程式。

檢查使用者

下列 User 元件可以直接在應用程式中使用,或作為進一步自訂的基礎。

User.razor

@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

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

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

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

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}

檢查 JSON Web 權杖 (JWT) 的內容

若要解碼 JSON Web 權杖 (JWT),請使用 Microsoft 的 jwt.ms 工具。 UI 中的值永遠不會離開瀏覽器。

範例編碼 JWT (已縮短顯示):

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2_nXA25Q

針對 Azure AAD B2C 進行驗證之應用程式的工具所解碼的範例 JWT:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]

其他資源