ASP.NET Core: Xác thực và ủy quyền trong ASP.NET Core SignalR
Xác thực người dùng kết nối với hub SignalR
SignalR có thể được sử dụng với xác thực ASP.NET Core để liên kết người dùng với từng kết nối. Trong một hub, dữ liệu xác thực có thể được truy cập từ property HubConnectionContext.User. Xác thực cho phép hub gọi các phương thức trên tất cả các kết nối được liên kết với người dùng. Để biết thêm thông tin, hãy xem Quản lý người dùng và nhóm trong SignalR. Nhiều kết nối có thể được liên kết với một người dùng.
Đoạn code sau là một ví dụ sử dụng xác thực SignalR và 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();
Lưu ý
Nếu code thông báo hết hạn trong thời gian tồn tại của kết nối, theo mặc định, kết nối đó sẽ tiếp tục hoạt động.
LongPolling
vàServerSentEvent
các kết nối không thành công trong các yêu cầu tiếp theo nếu chúng không gửi mã thông báo truy cập mới. Để kết nối đóng khi mã thông báo xác thực hết hạn, hãy đặt CloseOnAuthenticationExpiration.
Xác thực cookie
Trong một ứng dụng dựa trên trình duyệt, xác thực cookie cho phép thông tin đăng nhập của người dùng hiện có tự động chuyển đến các kết nối SignalR. Khi sử dụng ứng dụng trình duyệt, không cần cấu hình thêm. Nếu người dùng đã đăng nhập vào một ứng dụng, thì kết nối SignalR sẽ tự động kế thừa xác thực này.
Cookie là một cách dành riêng cho trình duyệt để gửi mã thông báo truy cập, nhưng client không sử dụng trình duyệt có thể gửi chúng. Khi sử dụng .NET Client, property Cookies
có thể được cấu hình trong lời gọi .WithUrl
để cung cấp cookie. Tuy nhiên, việc sử dụng xác thực cookie từ ứng dụng khách .NET yêu cầu ứng dụng cung cấp API để trao đổi dữ liệu xác thực lấy cookie.
Xác thực mã thông báo mang
Client có thể cung cấp mã thông báo truy cập thay vì sử dụng cookie. Server xác thực mã thông báo và sử dụng nó để xác định người dùng. Việc xác thực này chỉ được thực hiện khi kết nối được thiết lập. Trong suốt thời gian kết nối, Server không tự động xác thực lại để kiểm tra việc thu hồi mã thông báo.
Trong ứng dụng khách JavaScript, mã thông báo có thể được cung cấp bằng tùy chọn accessTokenFactory.
// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
.build();
Trong ứng dụng khách .NET, có một thuộc tính AccessTokenProvider tương tự có thể được sử dụng để định cấu hình mã thông báo:
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chathub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
})
.Build();
Lưu ý
Chức năng mã thông báo truy cập được cung cấp được gọi trước mọi yêu cầu HTTP do SignalR thực hiện. Nếu mã thông báo cần được gia hạn để duy trì kết nối hoạt động, hãy làm như vậy từ bên trong chức năng này và trả lại mã thông báo đã cập nhật. Mã thông báo có thể cần được gia hạn để mã không hết hạn trong quá trình kết nối.
Trong các API web tiêu chuẩn, mã thông báo mang được gửi trong tiêu đề HTTP. Tuy nhiên, SignalR không thể đặt các tiêu đề này trong trình duyệt khi sử dụng một số phương tiện truyền tải. Khi sử dụng WebSockets và Server-Sent Events, mã thông báo được truyền dưới dạng tham số chuỗi truy vấn.
Xác thực JWT tích hợp
Trên server, xác thực mã thông báo mang được định cấu hình bằng middleware JWT Bearer :
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();
Lưu ý
Chuỗi truy vấn được sử dụng trên trình duyệt khi kết nối với WebSockets và Server-Sent Events do giới hạn API của trình duyệt. Khi sử dụng HTTPS, các giá trị chuỗi truy vấn được bảo mật bằng kết nối TLS. Tuy nhiên, nhiều server đăng nhập các giá trị chuỗi truy vấn. Để biết thêm thông tin, hãy xem Cân nhắc bảo mật trong ASP.NET Core SignalR. SignalR sử dụng các tiêu đề để truyền mã thông báo trong môi trường hỗ trợ chúng (chẳng hạn như client .NET và Java).
Xác thực JWT của server nhận dạng
Khi sử dụng Duende IdentityServer, hãy thêm dịch vụ PostConfigureOptions<TOptions> vào dự án:
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;
}
}
};
}
}
Đăng ký dịch vụ sau khi thêm dịch vụ để xác thực (AddAuthentication) và trình xử lý xác thực cho Server nhận dạng (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 so với mã thông báo mang
Cookie dành riêng cho trình duyệt. Việc gửi chúng từ các loại client khác sẽ tăng thêm độ phức tạp so với việc gửi mã thông báo mang. Xác thực cookie không được khuyến nghị trừ khi ứng dụng chỉ cần xác thực người dùng từ ứng dụng khách của trình duyệt. Xác thực mã thông báo mang là phương pháp được đề xuất khi sử dụng ứng dụng khách không phải ứng dụng khách trình duyệt.
Xác thực Windows
Nếu xác thực Windows được định cấu hình trong ứng dụng, SignalR có thể sử dụng danh tính đó để bảo mật các hub. Tuy nhiên, để gửi tin nhắn cho từng người dùng, hãy thêm nhà cung cấp ID người dùng tùy chỉnh. Hệ thống xác thực Windows không cung cấp xác nhận quyền sở hữu "Name Identifier". SignalR sử dụng xác nhận quyền sở hữu để xác định tên người dùng.
Thêm một lớp mới triển khai IUserIdProvider
và truy xuất một trong các yêu cầu từ người dùng để sử dụng làm mã định danh. Ví dụ: để sử dụng yêu cầu "Name" (là tên người dùng Windows ở dạng [Domain]/[Username]
), hãy tạo lớp sau:
public class NameUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
return connection.User?.Identity?.Name;
}
}
Thay vì ClaimTypes.Name
, hãy sử dụng bất kỳ giá trị nào từ User
, chẳng hạn như mã định danh Windows SID, v.v.
Lưu ý
Giá trị được chọn phải là duy nhất trong số tất cả người dùng trong hệ thống. Mặt khác, một thông báo dành cho một người dùng có thể sẽ chuyển đến một người dùng khác.
Đăng ký thành phần này trong 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.
Trong Client .NET, phải bật Xác thực Windows bằng cách đặt thuộc tính UseDefaultCredentials:
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chathub", options =>
{
options.UseDefaultCredentials = true;
})
.Build();
Xác thực Windows được hỗ trợ trong Microsoft Edge, nhưng không phải trong tất cả các trình duyệt. Ví dụ: trong Chrome và Safari, cố gắng sử dụng xác thực Windows và WebSockets không thành công. Khi xác thực Windows không thành công, client sẽ cố gắng quay lại các phương tiện vận chuyển khác có thể hoạt động.
Sử dụng xác nhận quyền sở hữu để tùy chỉnh xử lý danh tính
Một ứng dụng xác thực người dùng có thể lấy ID người dùng SignalR từ yêu cầu của người dùng. Để chỉ định cách SignalR tạo ID người dùng, hãy triển khai IUserIdProvider
và đăng ký triển khai.
Mã mẫu trình bày cách sử dụng xác nhận quyền sở hữu để chọn địa chỉ email của người dùng làm thuộc tính nhận dạng.
Lưu ý
Giá trị được chọn phải là duy nhất trong số tất cả người dùng trong hệ thống. Mặt khác, một thông báo dành cho một người dùng có thể sẽ chuyển đến một người dùng khác.
public class EmailBasedUserIdProvider : IUserIdProvider
{
public virtual string GetUserId(HubConnectionContext connection)
{
return connection.User?.FindFirst(ClaimTypes.Email)?.Value!;
}
}
Việc đăng ký tài khoản thêm một yêu cầu với kiểu ClaimsTypes.Email
để cơ sở dữ liệu nhận dạng 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.
Đăng ký thành phần này trong Program.cs
:
builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();
Cho phép người dùng truy cập vào các hub và phương thức hub
Theo mặc định, tất cả các phương thức trong một hub có thể được gọi bởi người dùng chưa được xác thực. Để yêu cầu xác thực, hãy áp dụng attribute AuthorizeAttribute cho hub:
[Authorize]
public class ChatHub: Hub
{
}
Các đối số hàm tạo và các property của attribute [Authorize]
có thể được sử dụng để hạn chế quyền truy cập chỉ đối với những người dùng phù hợp với các chính sách ủy quyền cụ thể. Ví dụ: với chính sách ủy quyền tùy chỉnh được gọi là MyAuthorizationPolicy
, chỉ những người dùng phù hợp với chính sách đó mới có thể truy cập vào hub bằng code sau:
[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.
Attribute [Authorize]
có thể được áp dụng cho các phương thức hub riêng lẻ. Nếu người dùng hiện tại không khớp với chính sách được áp dụng cho phương thức, một lỗi sẽ được trả về cho người gọi:
[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) ...
}
}
Sử dụng trình xử lý ủy quyền để tùy chỉnh ủy quyền phương thức hub
SignalR cung cấp tài nguyên tùy chỉnh cho trình xử lý ủy quyền khi phương thức hub yêu cầu ủy quyền. Tài nguyên là một phiên bản của HubInvocationContext. HubInvocationContext
bao gồm HubCallerContext, tên của phương thức hub được gọi và các đối số cho phương thức hub.
Xem xét ví dụ về phòng trò chuyện cho phép nhiều tổ chức đăng nhập qua Azure Active Directory. Bất kỳ ai có tài khoản Microsoft đều có thể đăng nhập để trò chuyện nhưng chỉ thành viên của tổ chức sở hữu mới có thể cấm người dùng hoặc xem lịch sử trò chuyện của người dùng. Hơn nữa, chúng ta có thể muốn hạn chế một số chức năng từ những người dùng cụ thể. Lưu ý cách DomainRestrictedRequirement
phân phát dưới dạng IAuthorizationRequirement tùy chỉnh. Bây giờ tham số tài nguyên HubInvocationContext
đang được truyền vào, logic bên trong có thể kiểm tra ngữ cảnh mà Hub đang được gọi và đưa ra quyết định về việc cho phép người dùng thực thi các phương thức Hub riêng lẻ:
[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));
}
}
Trong Program.cs
, hãy thêm chính sách mới, cung cấp yêu cầu DomainRestrictedRequirement
tùy chỉnh dưới dạng tham số để tạo chính sách 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.
Trong ví dụ trên, lớp DomainRestrictedRequirement
vừa là một IAuthorizationRequirement
vừa là AuthorizationHandler
của chính nó đối với yêu cầu đó. Có thể chấp nhận chia hai thành phần này thành các lớp riêng biệt để tách biệt các mối quan tâm. Một lợi ích của cách tiếp cận ví dụ là không cần phải tiêm AuthorizationHandler
trong khi khởi động, vì yêu cầu và trình xử lý là giống nhau.
Tài nguyên bổ sung
- Xác thực mã thông báo mang trong ASP.NET Core
- Ủy quyền dựa trên tài nguyên
- Xem hoặc tải xuống mã mẫu (cách tải xuống)