Autentikasi dan otorisasi di ASP.NET Core SignalR

Mengautentikasi pengguna yang SignalR tersambung ke hub

SignalR dapat digunakan dengan autentikasi ASP.NET Core untuk mengaitkan pengguna dengan setiap koneksi. Di hub, data autentikasi dapat diakses dari HubConnectionContext.User properti . Autentikasi memungkinkan hub untuk memanggil metode pada semua koneksi yang terkait dengan pengguna. Untuk informasi selengkapnya, lihat Mengelola pengguna dan grup di SignalR. Beberapa koneksi dapat dikaitkan dengan satu pengguna.

Kode berikut adalah contoh yang menggunakan SignalR dan ASP.NET autentikasi Core:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chat");

app.Run();

Catatan

Jika token kedaluwarsa selama masa pakai koneksi, secara default koneksi terus berfungsi. LongPolling dan ServerSentEvent koneksi gagal pada permintaan berikutnya jika mereka tidak mengirim token akses baru. Agar koneksi ditutup saat token autentikasi kedaluwarsa, atur CloseOnAuthenticationExpiration.

Di aplikasi berbasis browser, cookie autentikasi memungkinkan kredensial pengguna yang ada mengalir secara otomatis ke SignalR koneksi. Saat menggunakan klien browser, tidak diperlukan konfigurasi tambahan. Jika pengguna masuk ke aplikasi, SignalR koneksi secara otomatis mewarisi autentikasi ini.

Cookies adalah cara khusus browser untuk mengirim token akses, tetapi klien non-browser dapat mengirimnya. Saat menggunakan Klien .NET, Cookies properti dapat dikonfigurasi dalam .WithUrl panggilan untuk menyediakan cookie. Namun, menggunakan cookie autentikasi dari klien .NET mengharuskan aplikasi menyediakan API untuk bertukar data autentikasi dengan cookie.

Autentikasi token pembawa

Klien dapat menyediakan token akses alih-alih menggunakan cookie. Server memvalidasi token dan menggunakannya untuk mengidentifikasi pengguna. Validasi ini dilakukan hanya ketika koneksi dibuat. Selama masa pakai koneksi, server tidak secara otomatis memvalidasi ulang untuk memeriksa pencabutan token.

Di klien JavaScript, token dapat disediakan menggunakan opsi accessTokenFactory .

// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();

Di klien .NET, ada properti AccessTokenProvider serupa yang dapat digunakan untuk mengonfigurasi token:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    { 
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();

Catatan

Fungsi token akses yang disediakan dipanggil sebelum setiap permintaan HTTP yang dibuat oleh SignalR. Jika token perlu diperbarui untuk menjaga koneksi tetap aktif, lakukan dari dalam fungsi ini dan kembalikan token yang diperbarui. Token mungkin perlu diperbarui sehingga tidak kedaluwarsa selama koneksi.

Di API web standar, token pembawa dikirim di header HTTP. Namun, SignalR tidak dapat mengatur header ini di browser saat menggunakan beberapa transportasi. Saat menggunakan WebSocket dan Peristiwa Yang Dikirim Server, token ditransmisikan sebagai parameter string kueri.

Autentikasi JWT bawaan

Di server, autentikasi token pembawa dikonfigurasi menggunakan middleware Pembawa JWT:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

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

builder.Services.AddAuthentication(options =>
{
    // Identity made Cookie authentication the default.
    // However, we want JWT Bearer Auth to be the default.
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
  {
      // Configure the Authority to the expected value for
      // the authentication provider. This ensures the token
      // is appropriately validated.
      options.Authority = "Authority URL"; // TODO: Update URL

      // We have to hook the OnMessageReceived event in order to
      // allow the JWT authentication handler to read the access
      // token from the query string when a WebSocket or 
      // Server-Sent Events request comes in.

      // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
      // due to a limitation in Browser APIs. We restrict it to only calls to the
      // SignalR hub in this code.
      // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
      // for more information about security considerations when using
      // the query string to transmit the access token.
      options.Events = new JwtBearerEvents
      {
          OnMessageReceived = context =>
          {
              var accessToken = context.Request.Query["access_token"];

              // If the request is for our hub...
              var path = context.HttpContext.Request.Path;
              if (!string.IsNullOrEmpty(accessToken) &&
                  (path.StartsWithSegments("/hubs/chat")))
              {
                  // Read the token out of the query string
                  context.Token = accessToken;
              }
              return Task.CompletedTask;
          }
      };
  });

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token 
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages 
// intended for a different user!
builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

// Change to use email as the user identifier for SignalR
// builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

// WARNING: use *either* the NameUserIdProvider *or* the 
// EmailBasedUserIdProvider, but do not use both. 

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();

Catatan

String kueri digunakan pada browser saat menyambungkan dengan WebSocket dan Peristiwa Terkirim Server karena keterbatasan API browser. Saat menggunakan HTTPS, nilai string kueri diamankan oleh koneksi TLS. Namun, banyak server mencatat nilai string kueri. Untuk informasi selengkapnya, lihat Pertimbangan keamanan di ASP.NET Core SignalR. SignalR menggunakan header untuk mengirimkan token di lingkungan yang mendukungnya (seperti klien .NET dan Java).

Identity Autentikasi JWT server

Saat menggunakan Duende IdentityServer, tambahkan PostConfigureOptions<TOptions> layanan ke proyek:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Daftarkan layanan setelah menambahkan layanan untuk autentikasi (AddAuthentication) dan handler autentikasi untuk Identity Server (AddIdentityServerJwt):

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
        ConfigureJwtBearerOptions>());

builder.Services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.

Cookies vs. token pembawa

Cookies khusus untuk browser. Mengirimnya dari jenis klien lain menambah kompleksitas dibandingkan dengan mengirim token pembawa. Cookie autentikasi tidak disarankan kecuali aplikasi hanya perlu mengautentikasi pengguna dari klien browser. Autentikasi token pembawa adalah pendekatan yang direkomendasikan saat menggunakan klien selain klien browser.

Autentikasi Windows

Jika autentikasi Windows dikonfigurasi di aplikasi, SignalR dapat menggunakan identitas tersebut untuk mengamankan hub. Namun, untuk mengirim pesan ke pengguna individual, tambahkan penyedia ID Pengguna kustom. Sistem autentikasi Windows tidak menyediakan klaim "Pengidentifikasi Nama". SignalR menggunakan klaim untuk menentukan nama pengguna.

Tambahkan kelas baru yang mengimplementasikan IUserIdProvider dan mengambil salah satu klaim dari pengguna untuk digunakan sebagai pengidentifikasi. Misalnya, untuk menggunakan klaim "Nama" (yang merupakan nama pengguna Windows dalam formulir [Domain]/[Username]), buat kelas berikut:

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

Daripada ClaimTypes.Name, gunakan nilai apa pun dari User, seperti pengidentifikasi Windows SID, dll.

Catatan

Nilai yang dipilih harus unik di antara semua pengguna dalam sistem. Jika tidak, pesan yang ditujukan untuk satu pengguna dapat berakhir ke pengguna yang berbeda.

Daftarkan komponen ini di Program.cs:

using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.SignalR;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages();

services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

var app = builder.Build();

// Code removed for brevity.

Di Klien .NET, Autentikasi Windows harus diaktifkan dengan mengatur UseDefaultCredentials properti :

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();

Autentikasi Windows didukung di Microsoft Edge, tetapi tidak di semua browser. Misalnya, di Chrome dan Safari, mencoba menggunakan autentikasi Windows dan WebSocket gagal. Ketika autentikasi Windows gagal, klien mencoba untuk kembali ke transportasi lain yang mungkin berfungsi.

Menggunakan klaim untuk menyesuaikan penanganan identitas

Aplikasi yang mengautentikasi pengguna dapat memperoleh SignalR ID pengguna dari klaim pengguna. Untuk menentukan cara SignalR membuat ID pengguna, terapkan IUserIdProvider dan daftarkan implementasinya.

Kode sampel menunjukkan cara menggunakan klaim untuk memilih alamat email pengguna sebagai properti identifikasi.

Catatan

Nilai yang dipilih harus unik di antara semua pengguna dalam sistem. Jika tidak, pesan yang ditujukan untuk satu pengguna dapat berakhir ke pengguna yang berbeda.

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value!;
    }
}

Pendaftaran akun menambahkan klaim dengan jenis ClaimsTypes.Email ke database identitas ASP.NET.

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
                                                                          .ToList();
    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
        var result = await _userManager.CreateAsync(user, Input.Password);

        // Add the email claim and value for this user.
        await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

        // Remaining code removed for brevity.

Daftarkan komponen ini di Program.cs:

builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Mengotorisasi pengguna untuk mengakses hub dan metode hub

Secara default, semua metode di hub dapat dipanggil oleh pengguna yang tidak diautentikasi. Untuk memerlukan autentikasi, terapkan AuthorizeAttribute atribut ke hub:

[Authorize]
public class ChatHub: Hub
{
}

Argumen konstruktor dan properti [Authorize] atribut dapat digunakan untuk membatasi akses hanya ke pengguna yang cocok dengan kebijakan otorisasi tertentu. Misalnya, dengan kebijakan otorisasi kustom yang disebut MyAuthorizationPolicy, hanya pengguna yang cocok dengan kebijakan tersebut yang dapat mengakses hub menggunakan kode berikut:

[Authorize("MyAuthorizationPolicy")]
public class ChatPolicyHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Clients.All.SendAsync("ReceiveSystemMessage", 
                                    $"{Context.UserIdentifier} joined.");
        await base.OnConnectedAsync();
    }
    // Code removed for brevity.

Atribut [Authorize] dapat diterapkan ke metode hub individual. Jika pengguna saat ini tidak cocok dengan kebijakan yang diterapkan ke metode , kesalahan dikembalikan ke pemanggil:

[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... ban a user from the chat room (something only Administrators can do) ...
    }
}

Menggunakan handler otorisasi untuk menyesuaikan otorisasi metode hub

SignalR menyediakan sumber daya kustom untuk penangan otorisasi saat metode hub memerlukan otorisasi. Sumber daya adalah instans .HubInvocationContext HubInvocationContext termasuk HubCallerContext, nama metode hub yang dipanggil, dan argumen ke metode hub.

Pertimbangkan contoh ruang obrolan yang memungkinkan beberapa organisasi masuk melalui ID Microsoft Entra. Siapa pun dengan akun Microsoft dapat masuk ke obrolan, tetapi hanya anggota organisasi pemilik yang harus dapat melarang pengguna atau melihat riwayat obrolan pengguna. Selain itu, kami mungkin ingin membatasi beberapa fungsionalitas dari pengguna tertentu. Perhatikan bagaimana DomainRestrictedRequirement berfungsi sebagai kustom IAuthorizationRequirement. Sekarang setelah HubInvocationContext parameter sumber daya diteruskan, logika internal dapat memeriksa konteks di mana Hub sedang dipanggil dan membuat keputusan untuk memungkinkan pengguna menjalankan metode Hub individual:

[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

namespace SignalRAuthenticationSample;

public class DomainRestrictedRequirement :
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement,
        HubInvocationContext resource)
    {
        if (context.User.Identity != null &&
          !string.IsNullOrEmpty(context.User.Identity.Name) && 
          IsUserAllowedToDoThis(resource.HubMethodName,
                               context.User.Identity.Name) &&
          context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
                context.Succeed(requirement);
            
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") &&
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}

Di Program.cs, tambahkan kebijakan baru, berikan persyaratan kustom DomainRestrictedRequirement sebagai parameter untuk membuat DomainRestricted kebijakan:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var services = builder.Services;

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
services.AddDatabaseDeveloperPageExceptionFilter();

services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthorization(options =>
   {
       options.AddPolicy("DomainRestricted", policy =>
       {
           policy.Requirements.Add(new DomainRestrictedRequirement());
       });
   });

services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.

Dalam contoh sebelumnya, DomainRestrictedRequirement kelas adalah dan IAuthorizationRequirement sendiri AuthorizationHandler untuk persyaratan tersebut. Dapat diterima untuk membagi kedua komponen ini menjadi kelas terpisah untuk memisahkan kekhawatiran. Manfaat dari pendekatan contoh adalah tidak perlu menyuntikkan AuthorizationHandler selama startup, karena persyaratan dan handler adalah hal yang sama.

Sumber Daya Tambahan:

Menampilkan atau mengunduh kode sampel (cara mengunduh)

Mengautentikasi pengguna yang SignalR tersambung ke hub

SignalR dapat digunakan dengan autentikasi ASP.NET Core untuk mengaitkan pengguna dengan setiap koneksi. Di hub, data autentikasi dapat diakses dari HubConnectionContext.User properti . Autentikasi memungkinkan hub untuk memanggil metode pada semua koneksi yang terkait dengan pengguna. Untuk informasi selengkapnya, lihat Mengelola pengguna dan grup di SignalR. Beberapa koneksi dapat dikaitkan dengan satu pengguna.

Berikut ini adalah contoh Startup.Configure yang menggunakan SignalR dan ASP.NET autentikasi Core:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chat");
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}

Catatan

Jika token kedaluwarsa selama masa pakai koneksi, koneksi akan terus berfungsi. LongPolling dan ServerSentEvent koneksi gagal pada permintaan berikutnya jika mereka tidak mengirim token akses baru.

Di aplikasi berbasis browser, cookie autentikasi memungkinkan kredensial pengguna yang ada mengalir secara otomatis ke SignalR koneksi. Saat menggunakan klien browser, tidak diperlukan konfigurasi tambahan. Jika pengguna masuk ke aplikasi Anda, SignalR koneksi secara otomatis mewarisi autentikasi ini.

Cookies adalah cara khusus browser untuk mengirim token akses, tetapi klien non-browser dapat mengirimnya. Saat menggunakan Klien .NET, Cookies properti dapat dikonfigurasi dalam .WithUrl panggilan untuk menyediakan cookie. Namun, menggunakan cookie autentikasi dari klien .NET mengharuskan aplikasi menyediakan API untuk bertukar data autentikasi dengan cookie.

Autentikasi token pembawa

Klien dapat menyediakan token akses alih-alih menggunakan cookie. Server memvalidasi token dan menggunakannya untuk mengidentifikasi pengguna. Validasi ini dilakukan hanya ketika koneksi dibuat. Selama masa pakai koneksi, server tidak secara otomatis memvalidasi ulang untuk memeriksa pencabutan token.

Di klien JavaScript, token dapat disediakan menggunakan opsi accessTokenFactory .

// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();

Di klien .NET, ada properti AccessTokenProvider serupa yang dapat digunakan untuk mengonfigurasi token:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    { 
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();

Catatan

Fungsi token akses yang Anda berikan dipanggil sebelum setiap permintaan HTTP yang dibuat oleh SignalR. Jika Anda perlu memperbarui token untuk menjaga koneksi tetap aktif (karena mungkin kedaluwarsa selama koneksi), lakukan dari dalam fungsi ini dan kembalikan token yang diperbarui.

Di API web standar, token pembawa dikirim di header HTTP. Namun, SignalR tidak dapat mengatur header ini di browser saat menggunakan beberapa transportasi. Saat menggunakan WebSocket dan Peristiwa Yang Dikirim Server, token ditransmisikan sebagai parameter string kueri.

Autentikasi JWT bawaan

Di server, autentikasi token pembawa dikonfigurasi menggunakan middleware Pembawa JWT:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication(options =>
        {
            // Identity made Cookie authentication the default.
            // However, we want JWT Bearer Auth to be the default.
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            // Configure the Authority to the expected value for your authentication provider
            // This ensures the token is appropriately validated
            options.Authority = /* TODO: Insert Authority URL here */;

            // We have to hook the OnMessageReceived event in order to
            // allow the JWT authentication handler to read the access
            // token from the query string when a WebSocket or 
            // Server-Sent Events request comes in.

            // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
            // due to a limitation in Browser APIs. We restrict it to only calls to the
            // SignalR hub in this code.
            // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
            // for more information about security considerations when using
            // the query string to transmit the access token.
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];

                    // If the request is for our hub...
                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrEmpty(accessToken) &&
                        (path.StartsWithSegments("/hubs/chat")))
                    {
                        // Read the token out of the query string
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddSignalR();

    // Change to use Name as the user identifier for SignalR
    // WARNING: This requires that the source of your JWT token 
    // ensures that the Name claim is unique!
    // If the Name claim isn't unique, users could receive messages 
    // intended for a different user!
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

    // Change to use email as the user identifier for SignalR
    // services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

    // WARNING: use *either* the NameUserIdProvider *or* the 
    // EmailBasedUserIdProvider, but do not use both. 
}

Jika Anda ingin melihat komentar kode yang diterjemahkan ke bahasa selain bahasa Inggris, beri tahu kami dalam masalah diskusi GitHub ini.

Catatan

String kueri digunakan pada browser saat menyambungkan dengan WebSocket dan Peristiwa Terkirim Server karena keterbatasan API browser. Saat menggunakan HTTPS, nilai string kueri diamankan oleh koneksi TLS. Namun, banyak server mencatat nilai string kueri. Untuk informasi selengkapnya, lihat Pertimbangan keamanan di ASP.NET Core SignalR. SignalR menggunakan header untuk mengirimkan token di lingkungan yang mendukungnya (seperti klien .NET dan Java).

Identity Autentikasi JWT server

Saat menggunakan Identity Server, tambahkan PostConfigureOptions<TOptions> layanan ke proyek:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) && 
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Daftarkan layanan setelah Startup.ConfigureServices menambahkan layanan untuk autentikasi (AddAuthentication) dan handler autentikasi untuk Identity Server (AddIdentityServerJwt):

services.AddAuthentication()
    .AddIdentityServerJwt();
services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, 
        ConfigureJwtBearerOptions>());

Cookies vs. token pembawa

Cookies khusus untuk browser. Mengirimnya dari jenis klien lain menambah kompleksitas dibandingkan dengan mengirim token pembawa. Akibatnya, cookie autentikasi tidak disarankan kecuali aplikasi hanya perlu mengautentikasi pengguna dari klien browser. Autentikasi token pembawa adalah pendekatan yang direkomendasikan saat menggunakan klien selain klien browser.

Autentikasi Windows

Jika autentikasi Windows dikonfigurasi di aplikasi Anda, SignalR dapat menggunakan identitas tersebut untuk mengamankan hub. Namun, untuk mengirim pesan ke pengguna individual, Anda perlu menambahkan penyedia ID Pengguna kustom. Sistem autentikasi Windows tidak menyediakan klaim "Pengidentifikasi Nama". SignalR menggunakan klaim untuk menentukan nama pengguna.

Tambahkan kelas baru yang mengimplementasikan IUserIdProvider dan mengambil salah satu klaim dari pengguna untuk digunakan sebagai pengidentifikasi. Misalnya, untuk menggunakan klaim "Nama" (yang merupakan nama pengguna Windows dalam formulir [Domain]\[Username]), buat kelas berikut:

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

Daripada ClaimTypes.Name, Anda dapat menggunakan nilai apa pun dari User (seperti pengidentifikasi Windows SID, dan sebagainya).

Catatan

Nilai yang Anda pilih harus unik di antara semua pengguna di sistem Anda. Jika tidak, pesan yang ditujukan untuk satu pengguna dapat berakhir ke pengguna yang berbeda.

Daftarkan komponen ini dalam metode Anda Startup.ConfigureServices .

public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services.AddSignalR();
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}

Di Klien .NET, Autentikasi Windows harus diaktifkan dengan mengatur UseDefaultCredentials properti :

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();

Autentikasi Windows didukung di Internet Explorer dan Microsoft Edge, tetapi tidak di semua browser. Misalnya, di Chrome dan Safari, mencoba menggunakan autentikasi Windows dan WebSocket gagal. Ketika autentikasi Windows gagal, klien mencoba untuk kembali ke transportasi lain yang mungkin berfungsi.

Menggunakan klaim untuk menyesuaikan penanganan identitas

Aplikasi yang mengautentikasi pengguna dapat memperoleh SignalR ID pengguna dari klaim pengguna. Untuk menentukan cara SignalR membuat ID pengguna, terapkan IUserIdProvider dan daftarkan implementasinya.

Kode sampel menunjukkan bagaimana Anda akan menggunakan klaim untuk memilih alamat email pengguna sebagai properti identifikasi.

Catatan

Nilai yang Anda pilih harus unik di antara semua pengguna di sistem Anda. Jika tidak, pesan yang ditujukan untuk satu pengguna dapat berakhir ke pengguna yang berbeda.

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
    }
}

Pendaftaran akun menambahkan klaim dengan jenis ClaimsTypes.Email ke database identitas ASP.NET.

// create a new user
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);

// add the email claim and value for this user
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

Daftarkan komponen ini di .Startup.ConfigureServices

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Mengotorisasi pengguna untuk mengakses hub dan metode hub

Secara default, semua metode di hub dapat dipanggil oleh pengguna yang tidak diautentikasi. Untuk memerlukan autentikasi, terapkan AuthorizeAttribute atribut ke hub:

[Authorize]
public class ChatHub: Hub
{
}

Anda dapat menggunakan argumen konstruktor dan properti [Authorize] atribut untuk membatasi akses hanya ke pengguna yang cocok dengan kebijakan otorisasi tertentu. Misalnya, jika Anda memiliki kebijakan otorisasi kustom yang disebut MyAuthorizationPolicy , Anda dapat memastikan bahwa hanya pengguna yang cocok dengan kebijakan tersebut yang dapat mengakses hub menggunakan kode berikut:

[Authorize("MyAuthorizationPolicy")]
public class ChatHub : Hub
{
}

Metode hub individual juga dapat [Authorize] memiliki atribut yang diterapkan. Jika pengguna saat ini tidak cocok dengan kebijakan yang diterapkan ke metode , kesalahan dikembalikan ke pemanggil:

[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... ban a user from the chat room (something only Administrators can do) ...
    }
}

Menggunakan handler otorisasi untuk menyesuaikan otorisasi metode hub

SignalR menyediakan sumber daya kustom untuk penangan otorisasi saat metode hub memerlukan otorisasi. Sumber daya adalah instans .HubInvocationContext HubInvocationContext termasuk HubCallerContext, nama metode hub yang dipanggil, dan argumen ke metode hub.

Pertimbangkan contoh ruang obrolan yang memungkinkan beberapa organisasi masuk melalui ID Microsoft Entra. Siapa pun dengan akun Microsoft dapat masuk ke obrolan, tetapi hanya anggota organisasi pemilik yang harus dapat melarang pengguna atau melihat riwayat obrolan pengguna. Selain itu, kita mungkin ingin membatasi fungsionalitas tertentu dari pengguna tertentu. Menggunakan fitur yang diperbarui di ASP.NET Core 3.0, ini sepenuhnya mungkin. Perhatikan bagaimana DomainRestrictedRequirement berfungsi sebagai kustom IAuthorizationRequirement. Sekarang setelah HubInvocationContext parameter sumber daya diteruskan, logika internal dapat memeriksa konteks di mana Hub sedang dipanggil dan membuat keputusan untuk memungkinkan pengguna menjalankan metode Hub individual.

[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

public class DomainRestrictedRequirement : 
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>, 
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement, 
        HubInvocationContext resource)
    {
        if (IsUserAllowedToDoThis(resource.HubMethodName, context.User.Identity.Name) && 
            context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") && 
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}

Di Startup.ConfigureServices, tambahkan kebijakan baru, berikan persyaratan kustom DomainRestrictedRequirement sebagai parameter untuk membuat DomainRestricted kebijakan.

public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services
        .AddAuthorization(options =>
        {
            options.AddPolicy("DomainRestricted", policy =>
            {
                policy.Requirements.Add(new DomainRestrictedRequirement());
            });
        });
}

Dalam contoh sebelumnya, DomainRestrictedRequirement kelas adalah dan IAuthorizationRequirement sendiri AuthorizationHandler untuk persyaratan tersebut. Dapat diterima untuk membagi kedua komponen ini menjadi kelas terpisah untuk memisahkan kekhawatiran. Manfaat dari pendekatan contoh adalah tidak perlu menyuntikkan AuthorizationHandler selama startup, karena persyaratan dan handler adalah hal yang sama.

Sumber Daya Tambahan: