Bagikan melalui


Skenario keamanan tambahan ASP.NET Core Blazor sisi server

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Peringatan

Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Penting

Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.

Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Artikel ini menjelaskan cara mengonfigurasi sisi Blazor server untuk skenario keamanan tambahan, termasuk cara meneruskan token ke Blazor aplikasi.

Catatan

Contoh kode dalam artikel ini mengadopsi jenis referensi nullable (NRTs) dan .NET compiler null-state static analysis, yang didukung di ASP.NET Core di .NET 6 atau yang lebih baru. Saat menargetkan ASP.NET Core 5.0 atau yang lebih lama, hapus penunjukan jenis null (?) dari string?TodoItem[]?, , WeatherForecast[]?, dan IEnumerable<GitHubBranch>? jenis dalam contoh artikel.

Meneruskan token ke aplikasi sisi Blazor server

Token yang tersedia di luar Razor komponen dalam aplikasi sisi Blazor server dapat diteruskan ke komponen dengan pendekatan yang dijelaskan di bagian ini. Contoh di bagian ini berfokus pada meneruskan token akses, refresh, dan pemalsuan anti-permintaan (XSRF) ke Blazor aplikasi, tetapi pendekatan ini berlaku untuk status konteks HTTP lainnya.

Catatan

Meneruskan token XSRF ke Razor komponen berguna dalam skenario di mana komponen POST ke Identity atau titik akhir lain yang memerlukan validasi. Jika aplikasi Anda hanya memerlukan token akses dan refresh, Anda dapat menghapus kode token XSRF dari contoh berikut.

Autentikasi aplikasi seperti yang Anda lakukan dengan Halaman biasa Razor atau aplikasi MVC. Provisikan dan simpan token ke autentikasi cookie.

Dalam file 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);
});

Di 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);
});

Di 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);
});

Secara opsional, cakupan tambahan ditambahkan dengan options.Scope.Add("{SCOPE}");, di mana tempat penampung {SCOPE} adalah cakupan tambahan untuk ditambahkan.

Tentukan layanan penyedia token tercakup yang dapat digunakan dalam Blazor aplikasi untuk menyelesaikan token dari injeksi dependensi (DI).

TokenProvider.cs:

public class TokenProvider
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}

Program Dalam file, tambahkan layanan untuk:

  • IHttpClientFactory: Digunakan di WeatherForecastService kelas yang mendapatkan data cuaca dari API server dengan token akses.
  • TokenProvider: Menyimpan token akses dan refresh.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

Di Startup.ConfigureServices dari Startup.cs, tambahkan layanan untuk:

  • IHttpClientFactory: Digunakan di WeatherForecastService kelas yang mendapatkan data cuaca dari API server dengan token akses.
  • TokenProvider: Menyimpan token akses dan refresh.
services.AddHttpClient();
services.AddScoped<TokenProvider>();

Tentukan kelas yang akan diteruskan dalam status aplikasi awal dengan token akses dan refresh.

InitialApplicationState.cs:

public class InitialApplicationState
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}

Pages/_Host.cshtml Dalam file, buat dan instans InitialApplicationState dan teruskan sebagai parameter ke aplikasi:

Pages/_Layout.cshtml Dalam file, buat dan instans InitialApplicationState dan teruskan sebagai parameter ke aplikasi:

Pages/_Host.cshtml Dalam file, buat dan instans InitialApplicationState dan teruskan sebagai parameter ke aplikasi:

@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 Dalam komponen (App.razor), atasi layanan dan inisialisasi dengan data dari parameter :

@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();
    }
}

Catatan

Alternatif untuk menetapkan status awal ke TokenProvider dalam contoh sebelumnya adalah menyalin data ke dalam layanan tercakup dalam OnInitializedAsync untuk digunakan di seluruh aplikasi.

Tambahkan referensi paket ke aplikasi untuk Microsoft.AspNet.WebApi.Client paket NuGet.

Catatan

Untuk panduan tentang menambahkan paket ke aplikasi .NET, lihat artikel di bagian Menginstal dan mengelola paket di Alur kerja konsumsi paket (dokumentasi NuGet). Konfirmasikan versi paket yang benar di NuGet.org.

Dalam layanan yang membuat permintaan API aman, masukkan penyedia token dan ambil token untuk permintaan 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>();
    }
}

Untuk token XSRF yang diteruskan ke komponen, masukkan TokenProvider dan tambahkan token XSRF ke permintaan POST. Contoh berikut menambahkan token ke POST titik akhir keluar. Skenario untuk contoh berikut adalah bahwa titik akhir keluar (Areas/Identity/Pages/Account/Logout.cshtml, perancah ke dalam aplikasi) tidak menentukan IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]) karena melakukan beberapa tindakan selain operasi keluar normal yang harus dilindungi. Titik akhir memerlukan token XSRF yang valid untuk berhasil memproses permintaan.

Dalam komponen yang menyajikan tombol Keluar untuk pengguna yang berwenang:

@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>

Mengatur skema autentikasi

Untuk aplikasi yang menggunakan lebih dari satu Middleware Autentikasi dan dengan demikian memiliki lebih dari satu skema autentikasi, skema yang Blazor digunakan dapat diatur secara eksplisit dalam konfigurasi Program titik akhir file. Contoh berikut mengatur skema OpenID Connect (OIDC):

Untuk aplikasi yang menggunakan lebih dari satu Middleware Autentikasi dan dengan demikian memiliki lebih dari satu skema autentikasi, skema yang Blazor menggunakan dapat diatur secara eksplisit dalam konfigurasi Startup.cstitik akhir . Contoh berikut mengatur skema 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
    });

Untuk aplikasi yang menggunakan lebih dari satu Middleware Autentikasi dan dengan demikian memiliki lebih dari satu skema autentikasi, skema yang Blazor menggunakan dapat diatur secara eksplisit dalam konfigurasi Startup.Configuretitik akhir . Contoh berikut mengatur skema ID Microsoft Entra:

endpoints.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
    });

Menggunakan titik akhir OpenID Connect (OIDC) v2.0

Dalam versi ASP.NET Core sebelum 5.0, pustaka dan Blazor templat autentikasi menggunakan titik akhir OpenID Connect (OIDC) v1.0. Untuk menggunakan titik akhir v2.0 dengan versi ASP.NET Core sebelum 5.0, konfigurasikan OpenIdConnectOptions.Authority opsi di OpenIdConnectOptions:

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    }

Atau, pengaturan dapat dibuat dalam file pengaturan aplikasi (appsettings.json):

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

Jika tacking pada segmen ke otoritas tidak sesuai untuk penyedia OIDC aplikasi, seperti dengan penyedia non-ME-ID, atur properti secara Authority langsung. Atur properti di atau OpenIdConnectOptions di file pengaturan aplikasi dengan Authority kunci .

Perubahan kode

  • Daftar klaim dalam token ID berubah untuk titik akhir v2.0. Dokumentasi Microsoft tentang perubahan telah dihentikan, tetapi panduan tentang klaim dalam token ID tersedia dalam referensi klaim token ID.

  • Karena sumber daya ditentukan dalam URI cakupan untuk titik akhir v2.0, hapus OpenIdConnectOptions.Resource pengaturan properti di OpenIdConnectOptions:

    services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => 
        {
            ...
            options.Resource = "...";    // REMOVE THIS LINE
            ...
        }
    

URI ID Aplikasi

  • Saat menggunakan titik akhir v2.0, API menentukan App ID URI, yang dimaksudkan untuk mewakili pengidentifikasi unik untuk API.
  • Semua cakupan mencakup URI ID Aplikasi sebagai awalan, dan titik akhir v2.0 memancarkan token akses dengan URI ID Aplikasi sebagai audiens.
  • Saat menggunakan titik akhir V2.0, ID klien yang dikonfigurasi di API Server berubah dari ID Aplikasi API (ID Klien) ke URI ID Aplikasi.

appsettings.json:

{
  "AzureAd": {
    ...
    "ClientId": "https://{TENANT}.onmicrosoft.com/{PROJECT NAME}"
    ...
  }
}

Anda dapat menemukan URI ID Aplikasi untuk digunakan dalam deskripsi pendaftaran aplikasi penyedia OIDC.

Penanganan sirkuit untuk menangkap pengguna untuk layanan kustom

CircuitHandler Gunakan untuk mengambil pengguna dari AuthenticationStateProvider dan mengatur pengguna dalam layanan. Jika Anda ingin memperbarui pengguna, daftarkan panggilan balik ke AuthenticationStateChanged dan antrekan Task untuk mendapatkan pengguna baru dan perbarui layanan. Contoh berikut menunjukkan pendekatan.

Dalam contoh berikut:

  • OnConnectionUpAsync dipanggil setiap kali sirkuit terhubung kembali, mengatur pengguna selama masa pakai koneksi. Hanya metode yang OnConnectionUpAsync diperlukan kecuali Anda menerapkan pembaruan melalui handler untuk perubahan autentikasi (AuthenticationChanged dalam contoh berikut).
  • OnCircuitOpenedAsync dipanggil untuk melampirkan handler yang diubah autentikasi, AuthenticationChanged, untuk memperbarui pengguna.
  • catch Blok UpdateAuthentication tugas tidak mengambil tindakan pada pengecualian karena tidak ada cara untuk melaporkannya pada saat ini dalam eksekusi kode. Jika pengecualian dilemparkan dari tugas, pengecualian dilaporkan di tempat lain di aplikasi.

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()
    {
        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;
    }
}
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;
    }
}

Dalam file Program:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

Dalam Startup.ConfigureServices dari Startup.cs:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

services.AddScoped<UserService>();
services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

Gunakan layanan dalam komponen untuk mendapatkan pengguna:

@inject UserService UserService

<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>

Untuk mengatur pengguna di middleware untuk MVC, Razor Pages, dan dalam skenario ASP.NET Core lainnya, panggil SetUserUserService di middleware kustom setelah Middleware Autentikasi berjalan, atau atur pengguna dengan IClaimsTransformation implementasi. Contoh berikut mengadopsi pendekatan middleware.

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);
    }
}

Segera sebelum panggilan ke app.MapRazorComponents<App>()Program dalam file, panggil middleware:

Segera sebelum panggilan ke app.MapBlazorHub()Program dalam file, panggil middleware:

Segera sebelum panggilan ke app.MapBlazorHub() di Startup.Configure , Startup.cspanggil middleware:

app.UseMiddleware<UserServiceMiddleware>();

Akses AuthenticationStateProvider di middleware permintaan keluar

AuthenticationStateProvider dari DelegatingHandler untuk HttpClient dibuat dengan IHttpClientFactory dapat diakses dalam middleware permintaan keluar menggunakan handler aktivitas sirkuit.

Catatan

Untuk panduan umum tentang mendefinisikan penangan untuk permintaan HTTP berdasarkan HttpClient instans yang dibuat menggunakan IHttpClientFactory di aplikasi ASP.NET Core, lihat bagian berikut membuat permintaan HTTP menggunakan IHttpClientFactory di ASP.NET Core:

Contoh berikut menggunakan AuthenticationStateProvider untuk melampirkan header nama pengguna kustom untuk pengguna terautentikasi ke permintaan keluar.

Pertama, terapkan CircuitServicesAccessor kelas di bagian berikut dari Blazor artikel injeksi dependensi (DI):

Mengakses layanan sisi Blazor server dari cakupan DI yang berbeda

CircuitServicesAccessor Gunakan untuk mengakses AuthenticationStateProvider dalam DelegatingHandler implementasi.

AuthenticationStateHandler.cs:

public class AuthenticationStateHandler : DelegatingHandler
{
    readonly CircuitServicesAccessor circuitServicesAccessor;

    public AuthenticationStateHandler(
        CircuitServicesAccessor circuitServicesAccessor)
    {
        this.circuitServicesAccessor = circuitServicesAccessor;
    }

    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 Dalam file, daftarkan AuthenticationStateHandler dan tambahkan handler ke IHttpClientFactory yang membuat instansHttpClient:

builder.Services.AddTransient<AuthenticationStateHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<AuthenticationStateHandler>();