ASP.NET Core 伺服器端和其他 Blazor Web App 安全性案例
注意
這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。
此文章說明如何針對其他安全性情節設定伺服器端 Blazor,包括如何將權杖傳遞至 Blazor 應用程式。
注意
本文中的程式碼範例採用 可為 Null 的參考型別 (NRT) 和 .NET 編譯器 Null 狀態靜態分析,這在 .NET 6 或更新版本的 ASP.NET Core 中受到支援。 以 ASP.NET Core 5.0 或更早版本為目標時,請從此文章範例的 string?
、TodoItem[]?
、WeatherForecast[]?
和 IEnumerable<GitHubBranch>?
類型中移除 Null 類型指定 (?
)。
將權杖傳遞至伺服器端 Blazor 應用程式
本節有關於 Blazor Web App 即將更新在 Blazor Web App 中傳遞令牌的小節 (dotnet/AspNetCore.Docs
#31691)。 如需詳細資訊,請參閱在互動式伺服器模式中為 HttpClient 提供存取權杖時發生問題 (dotnet/aspnetcore
#52390)。
針對 Blazor Server,請檢視本文有關 7.0 版的小節。
在伺服器端 Razor 應用程式中 Blazor 元件外部可用的權杖,可以使用此節中所述的方法傳遞至元件。 此節中的範例主要介紹如何將存取權杖、重新整理權杖和防止偽造要求 (XSRF) 權杖傳遞至 Blazor 應用程式,但此方法適用於其他 HTTP 內容狀態。
注意
在元件 POST 至 Identity 或其他需要驗證之端點的情節下,將 XSRF 權杖傳遞至 Razor 元件會很有用。 如果您的應用程式只需要存取權杖和重新整理權杖,您可以從下列範例中移除 XSRF 權杖程式碼。
使用一般 Razor Pages 或 MVC 應用程式來驗證應用程式。 將權杖佈建並儲存至驗證 cookie。
在 Program
檔案中:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
builder.Services.Configure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
在 Startup.cs
中:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
services.Configure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
在 Startup.cs
中:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
您可以選擇性地使用 options.Scope.Add("{SCOPE}");
來新增其他範圍,其中 {SCOPE}
預留位置是要新增的其他範圍。
定義可在 Blazor 應用程式中用來解析來自相依性插入 (DI) 之權杖的限定範圍權杖提供者服務。
TokenProvider.cs
:
public class TokenProvider
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}
在 Program
檔案中,新增下列項目的服務:
- IHttpClientFactory:用於使用存取權杖從伺服器 API 取得天氣資料的
WeatherForecastService
類別。 TokenProvider
:保留存取權杖和重新整理權杖。
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();
在 Startup.cs
的 Startup.ConfigureServices
中,新增下列項目的服務:
- IHttpClientFactory:用於使用存取權杖從伺服器 API 取得天氣資料的
WeatherForecastService
類別。 TokenProvider
:保留存取權杖和重新整理權杖。
services.AddHttpClient();
services.AddScoped<TokenProvider>();
定義類別,以使用存取權杖和重新整理權杖來傳入初始應用程式狀態。
InitialApplicationState.cs
:
public class InitialApplicationState
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}
在 Pages/_Host.cshtml
檔案中,建立 InitialApplicationState
的執行個體,並將它當做參數傳遞給應用程式:
在 Pages/_Layout.cshtml
檔案中,建立 InitialApplicationState
的執行個體,並將它當做參數傳遞給應用程式:
在 Pages/_Host.cshtml
檔案中,建立 InitialApplicationState
的執行個體,並將它當做參數傳遞給應用程式:
@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
...
@{
var tokens = new InitialApplicationState
{
AccessToken = await HttpContext.GetTokenAsync("access_token"),
RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
};
}
<component ... param-InitialState="tokens" ... />
在 App
元件 (App.razor
) 中,解析服務並使用參數的資料將其初始化:
@inject TokenProvider TokenProvider
...
@code {
[Parameter]
public InitialApplicationState? InitialState { get; set; }
protected override Task OnInitializedAsync()
{
TokenProvider.AccessToken = InitialState?.AccessToken;
TokenProvider.RefreshToken = InitialState?.RefreshToken;
TokenProvider.XsrfToken = InitialState?.XsrfToken;
return base.OnInitializedAsync();
}
}
注意
將初始狀態指派給上述範例中之 TokenProvider
的替代方法是將資料複製到 OnInitializedAsync 內的限定範圍服務,以便在整個應用程式中使用。
為 Microsoft.AspNet.WebApi.Client
NuGet 封裝的應用程式新增封裝參考。
注意
如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件) 的安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。
在提出安全 API 要求的服務中,插入權杖提供者並擷取 API 要求的權杖:
WeatherForecastService.cs
:
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class WeatherForecastService
{
private readonly HttpClient http;
private readonly TokenProvider tokenProvider;
public WeatherForecastService(IHttpClientFactory clientFactory,
TokenProvider tokenProvider)
{
http = clientFactory.CreateClient();
this.tokenProvider = tokenProvider;
}
public async Task<WeatherForecast[]> GetForecastAsync()
{
var token = tokenProvider.AccessToken;
var request = new HttpRequestMessage(HttpMethod.Get,
"https://localhost:5003/WeatherForecast");
request.Headers.Add("Authorization", $"Bearer {token}");
var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
Array.Empty<WeatherForecast>();
}
}
針對傳遞至元件的 XSRF 權杖,請插入 TokenProvider
並將 XSRF 權杖新增至 POST 要求。 下列範例會將權杖新增至登出端點 POST。 下列範例的情節是登出端點 (Areas/Identity/Pages/Account/Logout.cshtml
,已在應用程式中產生架構) 不會指定 IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]
),因為它除了執行必須保護的正常登出作業之外,還會執行一些動作。 端點需要有效的 XSRF 權杖才能成功處理要求。
在向已授權的使用者顯示 [登出] 按鈕的元件中:
@inject TokenProvider TokenProvider
...
<AuthorizeView>
<Authorized>
<form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
<button class="nav-link btn btn-link" type="submit">Logout</button>
<input name="__RequestVerificationToken" type="hidden"
value="@TokenProvider.XsrfToken">
</form>
</Authorized>
<NotAuthorized>
...
</NotAuthorized>
</AuthorizeView>
設定驗證配置
對於使用多個驗證中介軟體因而具有多個驗證配置的應用程式,可以在 Program
檔案的端點設定中明確設定 Blazor 所使用的配置。 下列範例會設定 OpenID Connect (OIDC) 配置:
對於使用多個驗證中介軟體因而具有多個驗證配置的應用程式,可以在 Startup.cs
的端點設定中明確設定 Blazor 所使用的配置。 下列範例會設定 OpenID Connect (OIDC) 配置:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
...
app.MapRazorComponents<App>().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
})
.AddInteractiveServerRenderMode();
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
...
app.MapBlazorHub().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
});
對於使用多個驗證中介軟體因而具有多個驗證配置的應用程式,可以在 Startup.Configure
的端點設定中明確設定 Blazor 所使用的配置。 下列範例會設定 Microsoft Entra ID 配置:
endpoints.MapBlazorHub().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
});
使用 OpenID Connect (OIDC) v2.0 端點
在 ASP.NET Core 5.0 之前的版本中,驗證程式庫和 Blazor 範本會使用 OpenID Connect (OIDC) v1.0 端點。 若要在 ASP.NET Core 5.0 之前的版本中使用 v2.0 端點,請在 OpenIdConnectOptions 中設定 OpenIdConnectOptions.Authority 選項:
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme,
options =>
{
options.Authority += "/v2.0";
}
或者,您可以在應用程式設定 (appsettings.json
) 檔案中進行設定:
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/",
...
}
}
如果將區段附加到授權單位不適合應用程式的 OIDC 提供者 (例如使用非 ME 識別碼的提供者),請直接設定 Authority 屬性。 使用 Authority 索引鍵在 OpenIdConnectOptions 或應用程式設定檔案中設定屬性。
程式碼變更
識別碼權杖中的宣告清單會針對 v2.0 端點有所變更。 變更的 Microsoft 文件已淘汰,但識別碼權杖參考中提供了與識別碼權杖中的宣告有關的指引。
由於資源已在 v2.0 端點的範圍 URI 中指定,因此請移除 OpenIdConnectOptions 中的 OpenIdConnectOptions.Resource 屬性設定:
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => { ... options.Resource = "..."; // REMOVE THIS LINE ... }
應用程式識別碼 URI
- 使用 v2.0 端點時,API 會定義
App ID URI
,用來代表 API 的唯一識別碼。 - 所有範圍都包含 App ID URI 作為前置詞,而且 v2.0 端點會以 App ID URI 作為對象發出存取權杖。
- 使用 V2.0 端點時,伺服器 API 中所設定的用戶端識別碼會從 API 應用程式識別碼 (用戶端識別碼) 變更為 App ID URI。
appsettings.json
:
{
"AzureAd": {
...
"ClientId": "https://{TENANT}.onmicrosoft.com/{PROJECT NAME}"
...
}
}
您可以在 OIDC 提供者應用程式註冊描述中,找到要使用的 App ID URI。
用來為自訂服務擷取使用者的線路處理常式
使用 CircuitHandler 從 AuthenticationStateProvider 擷取使用者,並在服務中設定使用者。 如果您要更新使用者,請註冊 AuthenticationStateChanged 的回撥,並將 Task 加入佇列,以取得新的使用者並更新服務。 下列範例會示範此方法。
在以下範例中:
- 每次線路重新連線時都會呼叫 OnConnectionUpAsync,為連線的存留期設定使用者。 除非您透過處理常式來實作驗證變更的更新 (下列範例中的
AuthenticationChanged
),否則只需要 OnConnectionUpAsync 方法。 - 呼叫 OnCircuitOpenedAsync 以附加驗證變更的處理常式
AuthenticationChanged
,以更新使用者。 UpdateAuthentication
工作的catch
區塊不會對例外狀況採取任何動作,因為目前無法在執行程式碼時報告例外狀況。 如果有例外狀況從工作擲回,則會在應用程式中的其他位置回報例外狀況。
UserService.cs
:
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;
public class UserService
{
private ClaimsPrincipal currentUser = new(new ClaimsIdentity());
public ClaimsPrincipal GetUser() => currentUser;
internal void SetUser(ClaimsPrincipal user)
{
if (currentUser != user)
{
currentUser = user;
}
}
}
internal sealed class UserCircuitHandler(
AuthenticationStateProvider authenticationStateProvider,
UserService userService)
: CircuitHandler, IDisposable
{
public override Task OnCircuitOpenedAsync(Circuit circuit,
CancellationToken cancellationToken)
{
authenticationStateProvider.AuthenticationStateChanged +=
AuthenticationChanged;
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
private void AuthenticationChanged(Task<AuthenticationState> task)
{
_ = UpdateAuthentication(task);
async Task UpdateAuthentication(Task<AuthenticationState> task)
{
try
{
var state = await task;
userService.SetUser(state.User);
}
catch
{
}
}
}
public override async Task OnConnectionUpAsync(Circuit circuit,
CancellationToken cancellationToken)
{
var state = await authenticationStateProvider.GetAuthenticationStateAsync();
userService.SetUser(state.User);
}
public void Dispose()
{
authenticationStateProvider.AuthenticationStateChanged -=
AuthenticationChanged;
}
}
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;
public class UserService
{
private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());
public ClaimsPrincipal GetUser()
{
return currentUser;
}
internal void SetUser(ClaimsPrincipal user)
{
if (currentUser != user)
{
currentUser = user;
}
}
}
internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
private readonly AuthenticationStateProvider authenticationStateProvider;
private readonly UserService userService;
public UserCircuitHandler(
AuthenticationStateProvider authenticationStateProvider,
UserService userService)
{
this.authenticationStateProvider = authenticationStateProvider;
this.userService = userService;
}
public override Task OnCircuitOpenedAsync(Circuit circuit,
CancellationToken cancellationToken)
{
authenticationStateProvider.AuthenticationStateChanged +=
AuthenticationChanged;
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
private void AuthenticationChanged(Task<AuthenticationState> task)
{
_ = UpdateAuthentication(task);
async Task UpdateAuthentication(Task<AuthenticationState> task)
{
try
{
var state = await task;
userService.SetUser(state.User);
}
catch
{
}
}
}
public override async Task OnConnectionUpAsync(Circuit circuit,
CancellationToken cancellationToken)
{
var state = await authenticationStateProvider.GetAuthenticationStateAsync();
userService.SetUser(state.User);
}
public void Dispose()
{
authenticationStateProvider.AuthenticationStateChanged -=
AuthenticationChanged;
}
}
在 Program
檔案中:
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
...
builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
在 Startup.cs
的 Startup.ConfigureServices
中:
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
...
services.AddScoped<UserService>();
services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
使用元件中的服務來取得使用者:
@inject UserService UserService
<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>
若要在 MVC、Razor Pages 和其他 ASP.NET Core 情節的中介軟體中設定使用者,請在驗證中介軟體執行之後呼叫自訂中介軟體中 UserService
上的 SetUser
,或以 IClaimsTransformation 實作來設定使用者。 下列範例採用中介軟體方法。
UserServiceMiddleware.cs
:
public class UserServiceMiddleware
{
private readonly RequestDelegate next;
public UserServiceMiddleware(RequestDelegate next)
{
this.next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task InvokeAsync(HttpContext context, UserService service)
{
service.SetUser(context.User);
await next(context);
}
}
在即將呼叫 Program
檔案中的 app.MapRazorComponents<App>()
之前,呼叫中介軟體:
在即將呼叫 Program
檔案中的 app.MapBlazorHub()
之前,呼叫中介軟體:
在即將呼叫 Startup.cs
的 Startup.Configure
中的 app.MapBlazorHub()
之前,呼叫中介軟體:
app.UseMiddleware<UserServiceMiddleware>();
在傳出要求中介軟體中存取 AuthenticationStateProvider
以 IHttpClientFactory 建立之 HttpClientDelegatingHandler 中的 AuthenticationStateProvider,可以使用線路活動處理常式在傳出要求中介軟體中存取。
注意
如需取得一般指引,了解如何在 ASP.NET Core 應用程式中使用 IHttpClientFactory 建立的 HttpClient 執行個體來定義 HTTP 要求的委派處理常式,請參閱在 ASP.NET Core 中使用 IHttpClientFactory 提出 HTTP 要求 (部分機器翻譯) 的下列各節內容:
- 傳出要求中介軟體 (部分機器翻譯)
- 在傳出要求中介軟體中使用 DI (部分機器翻譯)
下列範例使用 AuthenticationStateProvider 將已驗證使用者的自訂使用者名稱標頭附加至傳出要求。
首先,實作 Blazor 相依性插入 (DI) 文章下列部分中的 CircuitServicesAccessor
類別:
從不同的 DI 範圍存取伺服器端 Blazor 服務 (部分機器翻譯)
使用 CircuitServicesAccessor
存取 DelegatingHandler 實作中的 AuthenticationStateProvider。
AuthenticationStateHandler.cs
:
public class AuthenticationStateHandler(
CircuitServicesAccessor circuitServicesAccessor)
: DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var authStateProvider = circuitServicesAccessor.Services
.GetRequiredService<AuthenticationStateProvider>();
var authState = await authStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity is not null && user.Identity.IsAuthenticated)
{
request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
}
return await base.SendAsync(request, cancellationToken);
}
}
在 Program
檔案中,註冊 AuthenticationStateHandler
並將處理常式新增至建立 HttpClient 執行個體的 IHttpClientFactory:
builder.Services.AddTransient<AuthenticationStateHandler>();
builder.Services.AddHttpClient("HttpMessageHandler")
.AddHttpMessageHandler<AuthenticationStateHandler>();