ASP.NET Core SignalR 中的驗證和授權

驗證連線到 SignalR 中樞的使用者

SignalR 可與 ASP.NET Core 驗證搭配使用,以將使用者與每個連線產生關聯。 在中樞中,可以從 HubConnectionContext.User 屬性存取驗證資料。 驗證可讓中樞在與使用者相關聯的所有連線上呼叫方法。 如需詳細資訊,請參閱在 SignalR 中管理使用者和群組。 多個連線可能與單一使用者相關聯。

下列程式碼是使用 SignalR 和 ASP.NET 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();

注意

如果權杖在連線存留期期間到期,則連線預設會繼續運作。 如果後續要求未傳送新的存取權杖,LongPollingServerSentEvent 連線就會失敗。 若要在驗證權杖到期時關閉連線,請設定 CloseOnAuthenticationExpiration

在瀏覽器型應用程式中,cookie 驗證可讓現有的使用者認證自動流向 SignalR 連線。 使用瀏覽器用戶端時,不需要額外的設定。 如果使用者登入應用程式,SignalR 連線會自動繼承此驗證。

Cookie 是傳送存取權杖的瀏覽器特定方式,但非瀏覽器用戶端可以傳送存取權杖。 使用 .NET 用戶端時,可以在 .WithUrl 呼叫中設定 Cookies 屬性以提供 cookie。 不過,使用 .NET 用戶端的 cookie 驗證需要應用程式提供 API 來交換 cookie 的驗證資料。

持有人權杖驗證

用戶端可以提供存取權杖,而不是使用 cookie。 伺服器會驗證權杖,並使用它來識別使用者。 只有在建立連線時,才會進行此驗證。 在連線期間,伺服器不會自動重新驗證以檢查權杖撤銷。

在 JavaScript 用戶端中,可以使用 accessTokenFactory 選項來提供權杖。

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

在 .NET 用戶端中,有類似的 AccessTokenProvider 屬性可用來設定權杖:

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

注意

所提供的存取權杖函式會在 SignalR 發出的每個 HTTP 要求之前呼叫。 如果需要更新權杖,才能讓連線保持作用中,請從此函式中執行,並傳回更新的權杖。 權杖可能需要更新,因此在連線期間不會過期。

在標準 Web API 中,持有人權杖會在 HTTP 標頭中傳送。 不過,在使用某些傳輸時,SignalR 無法在瀏覽器中設定這些標頭。 使用 WebSocket 和伺服器傳送事件時,權杖會以查詢字串參數的形式傳輸。

內建 JWT 驗證

在伺服器上,持有人權杖驗證是使用 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();

注意

由於瀏覽器 API 限制,使用 WebSocket 和伺服器傳送事件進行連線時,會在瀏覽器上使用查詢字串。 使用 HTTPS 時,查詢字串值會受到 TLS 連線保護。 不過,許多伺服器都會記錄查詢字串值。 如需詳細資訊,請參閱 ASP.NET Core SignalR 中的安全性考慮。 SignalR 會使用標頭在支援權杖的環境中傳輸權杖 (例如 .NET 和 JAVA 用戶端)。

Identity 伺服器 JWT 驗證

使用 Duende IdentityServer 將 PostConfigureOptions<TOptions> 服務新增至專案時:

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

在新增服務以進行驗證 (AddAuthentication) 以及 Identity 伺服器的驗證處理常式 (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.

Cookie 與持有人權杖

Cookie 是瀏覽器特有的。 相較於傳送持有人權杖,從其他類型的用戶端傳送它們會增加複雜性。 除非應用程式只需要從瀏覽器用戶端驗證使用者,否則不建議使用 Cookie 驗證。 使用瀏覽器用戶端以外的用戶端時,持有人權杖驗證是建議的方法。

Windows 驗證

如果在應用程式中設定 Windows 驗證,SignalR 可以使用該身分識別來保護中樞。 不過,若要將訊息傳送給個別使用者,請新增自訂使用者識別碼提供者。 Windows 驗證系統不提供「名稱識別碼」宣告。 SignalR 會使用宣告來判斷使用者名稱。

新增類別,這個類別會實作 IUserIdProvider 並從使用者擷取其中一個宣告,以做為識別碼。 例如,若要使用「名稱」宣告 (也就是表單 [Domain]/[Username] 中的 Windows 使用者名稱),請建立下列類別:

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

請使用來自 User 的任何值,例如 Windows SID 識別碼等,而不是 ClaimTypes.Name

注意

所選的值在系統中的所有使用者中必須是唯一的。 否則,適用於一位使用者的訊息最終可能會前往不同的使用者。

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.

在 .NET 用戶端中,必須藉由設定 UseDefaultCredentials 屬性來啟用 Windows 驗證:

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

Microsoft Edge 支援 Windows 驗證,但並非所有瀏覽器都支援。 例如,在 Chrome 和 Safari 中,嘗試使用 Windows 驗證和 WebSocket 失敗。 當 Windows 驗證失敗時,用戶端會嘗試回復到其他可能運作的傳輸。

使用宣告來自訂身分識別處理

驗證使用者的應用程式可以從使用者宣告衍生 SignalR 使用者識別碼。 若要指定 SignalR 如何建立使用者識別碼,請實作 IUserIdProvider 並註冊實作。

範例程式碼示範如何使用宣告,將使用者的電子郵件地址選取為識別屬性。

注意

所選的值在系統中的所有使用者中必須是唯一的。 否則,適用於一位使用者的訊息最終可能會前往不同的使用者。

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

帳戶註冊會將類型 ClaimsTypes.Email 的宣告新增至 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.

Program.cs 中註冊此元件:

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

授權使用者存取中樞和中樞方法

根據預設,中樞中的所有方法都可以由未經驗證的使用者呼叫。 若要要求驗證,請將 AuthorizeAttribute 屬性套用至中樞:

[Authorize]
public class ChatHub: Hub
{
}

[Authorize] 屬性的建構函式引數和屬性可用來限制只存取符合特定授權原則的使用者。 例如,使用稱為 MyAuthorizationPolicy 的自訂授權原則,只有符合該原則的使用者可以使用下列程式碼來存取中樞:

[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.

[Authorize] 屬性可以套用至個別中樞方法。 如果目前的使用者不符合套用至方法的原則,則會將錯誤傳回給呼叫者:

[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) ...
    }
}

使用授權處理常式自訂中樞方法授權

當中樞方法需要授權時,SignalR 可提供自訂資源給授權處理常式。 資源是 HubInvocationContext 的執行個體。 HubInvocationContext 包含 HubCallerContext、要叫用的中樞方法名稱,以及中樞方法的引數。

請考慮允許多個組織透過 Microsoft Entra ID 登入的聊天室範例。 擁有 Microsoft 帳戶的任何人都可以登入聊天,但只有擁有組織的成員才能夠禁止使用者或檢視使用者的聊天歷程記錄。 此外,我們可能想要限制特定使用者的某些功能。 請注意 DomainRestrictedRequirement 如何做為自訂 IAuthorizationRequirement。 現在已傳入 HubInvocationContext 資源參數,內部邏輯可以檢查正在呼叫中樞的內容,並決定允許使用者執行個別中樞方法:

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

Program.cs 中,新增原則,提供自訂 DomainRestrictedRequirement 需求做為參數來建立 DomainRestricted 原則:

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.

在上述範例中,DomainRestrictedRequirement 類別為 IAuthorizationRequirement,也是滿足該需求的其本身 AuthorizationHandler。 可以接受將這兩個元件分割成不同的類別,以區分考慮。 範例方法的優點是不需要在啟動期間插入 AuthorizationHandler,因為需求和處理常式相同。

其他資源

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

驗證連線到 SignalR 中樞的使用者

SignalR 可與 ASP.NET Core 驗證搭配使用,以將使用者與每個連線產生關聯。 在中樞中,可以從 HubConnectionContext.User 屬性存取驗證資料。 驗證可讓中樞在與使用者相關聯的所有連線上呼叫方法。 如需詳細資訊,請參閱在 SignalR 中管理使用者和群組。 多個連線可能與單一使用者相關聯。

以下是 Startup.Configure 使用 SignalR 和 ASP.NET 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?}");
    });
}

注意

如果權杖在連線存留期期間到期,則連線會繼續運作。 如果後續要求未傳送新的存取權杖,LongPollingServerSentEvent 連線就會失敗。

在瀏覽器型應用程式中,cookie 驗證可讓現有的使用者認證自動流向 SignalR 連線。 使用瀏覽器用戶端時,不需要額外的設定。 如果使用者登入您的應用程式,SignalR 連線會自動繼承此驗證。

Cookie 是傳送存取權杖的瀏覽器特定方式,但非瀏覽器用戶端可以傳送存取權杖。 使用 .NET 用戶端時,可以在 .WithUrl 呼叫中設定 Cookies 屬性以提供 cookie。 不過,使用 .NET 用戶端的 cookie 驗證需要應用程式提供 API 來交換 cookie 的驗證資料。

持有人權杖驗證

用戶端可以提供存取權杖,而不是使用 cookie。 伺服器會驗證權杖,並使用它來識別使用者。 只有在建立連線時,才會進行此驗證。 在連線期間,伺服器不會自動重新驗證以檢查權杖撤銷。

在 JavaScript 用戶端中,可以使用 accessTokenFactory 選項來提供權杖。

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

在 .NET 用戶端中,有類似的 AccessTokenProvider 屬性可用來設定權杖:

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

注意

你提供的存取權杖函式會在 SignalR 發出的每個 HTTP 要求之前呼叫。 如果您需要更新權杖,才能讓連線保持作用中 (因為權杖在連線期間可能會過期),請從此函式中更新權杖並傳回更新的權杖。

在標準 Web API 中,持有人權杖會在 HTTP 標頭中傳送。 不過,在使用某些傳輸時,SignalR 無法在瀏覽器中設定這些標頭。 使用 WebSocket 和伺服器傳送事件時,權杖會以查詢字串參數的形式傳輸。

內建 JWT 驗證

在伺服器上,持有人權杖驗證是使用 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. 
}

如果您想要查看翻譯為英文以外語言的程式碼註解,請在此 GitHub 討論問題中告訴我們。

注意

由於瀏覽器 API 限制,使用 WebSocket 和伺服器傳送事件進行連線時,會在瀏覽器上使用查詢字串。 使用 HTTPS 時,查詢字串值會受到 TLS 連線保護。 不過,許多伺服器都會記錄查詢字串值。 如需詳細資訊,請參閱 ASP.NET Core SignalR 中的安全性考慮。 SignalR 會使用標頭在支援權杖的環境中傳輸權杖 (例如 .NET 和 JAVA 用戶端)。

Identity 伺服器 JWT 驗證

使用 Identity Server 將 PostConfigureOptions<TOptions> 服務新增至專案時:

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

新增服務以進行驗證 (AddAuthentication) 以及 Identity 伺服器的驗證處理常式 (AddIdentityServerJwt) 之後,在 Startup.ConfigureServices 中註冊服務:

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

Cookie 與持有人權杖

Cookie 是瀏覽器特有的。 相較於傳送持有人權杖,從其他類型的用戶端傳送它們會增加複雜性。 因此,除非應用程式只需要從瀏覽器用戶端驗證使用者,否則不建議使用 cookie 驗證。 使用瀏覽器用戶端以外的用戶端時,持有人權杖驗證是建議的方法。

Windows 驗證

如果您的應用程式中已設定 Windows 驗證,SignalR 可以使用該身分識別來保護中樞。 不過,若要將訊息傳送給個別使用者,您必須新增自訂使用者識別碼提供者。 Windows 驗證系統不提供「名稱識別碼」宣告。 SignalR 會使用宣告來判斷使用者名稱。

新增類別,這個類別會實作 IUserIdProvider 並從使用者擷取其中一個宣告,以做為識別碼。 例如,若要使用「名稱」宣告 (也就是表單 [Domain]\[Username] 中的 Windows 使用者名稱),請建立下列類別:

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

您可以使用來自 User 的任何值,例如 Windows SID 識別碼等,而不是 ClaimTypes.Name

注意

您選擇的值在系統中的所有使用者中必須是唯一的。 否則,適用於一位使用者的訊息最終可能會前往不同的使用者。

在您的 Startup.ConfigureServices 方法中註冊此元件。

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

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

在 .NET 用戶端中,必須藉由設定 UseDefaultCredentials 屬性來啟用 Windows 驗證:

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

Internet Explorer 和 Microsoft Edge 支援 Windows 驗證,但並非所有瀏覽器都支援。 例如,在 Chrome 和 Safari 中,嘗試使用 Windows 驗證和 WebSocket 失敗。 當 Windows 驗證失敗時,用戶端會嘗試回復到其他可能運作的傳輸。

使用宣告來自訂身分識別處理

驗證使用者的應用程式可以從使用者宣告衍生 SignalR 使用者識別碼。 若要指定 SignalR 如何建立使用者識別碼,請實作 IUserIdProvider 並註冊實作。

範例程式碼示範您如何使用宣告,將使用者的電子郵件地址選取為識別屬性。

注意

您選擇的值在系統中的所有使用者中必須是唯一的。 否則,適用於一位使用者的訊息最終可能會前往不同的使用者。

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

帳戶註冊會將類型 ClaimsTypes.Email 的宣告新增至 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));

在您的 Startup.ConfigureServices 中註冊此元件。

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

授權使用者存取中樞和中樞方法

根據預設,中樞中的所有方法都可以由未經驗證的使用者呼叫。 若要要求驗證,請將 AuthorizeAttribute 屬性套用至中樞:

[Authorize]
public class ChatHub: Hub
{
}

您可以使用 [Authorize] 屬性的建構函式引數和屬性來限制只存取符合特定授權原則的使用者。 例如,如果您有稱為 MyAuthorizationPolicy 的自訂授權原則,您可以確定只有符合該原則的使用者可以使用下列程式碼來存取中樞:

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

個別中樞方法也可以套用 [Authorize] 屬性。 如果目前的使用者不符合套用至方法的原則,則會將錯誤傳回給呼叫者:

[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) ...
    }
}

使用授權處理常式自訂中樞方法授權

當中樞方法需要授權時,SignalR 可提供自訂資源給授權處理常式。 資源是 HubInvocationContext 的執行個體。 HubInvocationContext 包含 HubCallerContext、要叫用的中樞方法名稱,以及中樞方法的引數。

請考慮允許多個組織透過 Microsoft Entra ID 登入的聊天室範例。 擁有 Microsoft 帳戶的任何人都可以登入聊天,但只有擁有組織的成員才能夠禁止使用者或檢視使用者的聊天歷程記錄。 此外,我們可能想要限制特定使用者的特定功能。 使用 ASP.NET Core 3.0 中更新的功能,這完全可行。 請注意 DomainRestrictedRequirement 如何做為自訂 IAuthorizationRequirement。 現在已傳入 HubInvocationContext 資源參數,內部邏輯可以檢查正在呼叫中樞的內容,並決定允許使用者執行個別中樞方法。

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

Startup.ConfigureServices 中,新增原則,提供自訂 DomainRestrictedRequirement 需求做為參數來建立 DomainRestricted 原則。

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

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

在上述範例中,DomainRestrictedRequirement 類別為 IAuthorizationRequirement,也是滿足該需求的其本身 AuthorizationHandler。 可以接受將這兩個元件分割成不同的類別,以區分考慮。 範例方法的優點是不需要在啟動期間插入 AuthorizationHandler,因為需求和處理常式相同。

其他資源