ASP.NET Core SignalR의 인증 및 권한 부여

SignalR 허브에 연결하는 사용자 인증

SignalRASP.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 인증을 사용하려면 앱에서 cookie에 대한 인증 데이터를 교환하는 API를 제공해야 합니다.

전달자 토큰 인증

클라이언트는 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 요청 전에 호출됩니다. 연결을 활성 상태로 유지하기 위해 토큰을 갱신해야 하는 경우 이 함수 내에서 토큰을 갱신하고 업데이트된 토큰을 반환합니다. 연결 중에 만료되지 않도록 토큰을 갱신해야 할 수 있습니다.

표준 웹 API에서 전달자 토큰은 HTTP 헤더로 전송됩니다. 그러나 SignalR은 일부 전송을 사용할 때 브라우저에서 이러한 헤더를 설정할 수 없습니다. WebSocket 및 Server-Sent 이벤트를 사용하는 경우 토큰은 쿼리 문자열 매개 변수로 전송됩니다.

기본 제공 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 및 Server-Sent 이벤트에 연결할 때 브라우저에서 사용됩니다. HTTPS를 사용하는 경우 쿼리 문자열 값은 TLS 연결로 보호됩니다. 그러나 많은 서버에서 쿼리 문자열 값을 기록합니다. 자세한 내용은 ASP.NET Core SignalR의 보안 고려 사항을 참조하세요. SignalR은 헤더를 사용하여 토큰을 지원하는 환경(예: .NET 및 Java 클라이언트)에서 토큰을 전송합니다.

Identity 서버 JWT 인증

Duende Identity 서버를 사용하는 경우 프로젝트에 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은 해당 ID를 사용하여 허브를 보호할 수 있습니다. 그러나 개별 사용자에게 메시지를 보내려면 사용자 지정 사용자 ID 공급업체를 추가합니다. Windows 인증 시스템은 "이름 식별자" 클레임을 제공하지 않습니다. SignalR은 클레임을 사용하여 사용자 이름을 확인합니다.

IUserIdProvider를 구현하는 새 클래스를 추가하고 식별자로 사용할 사용자로부터 클레임 중 하나를 검색합니다. 예를 들어 "Name" 클레임(형식 [Domain]/[Username]의 Windows 사용자 이름)을 사용하려면 다음 클래스를 만듭니다.

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

ClaimTypes.Name 대신 User의 모든 값(예: Windows SID 식별자 등)을 사용합니다.

참고 항목

선택한 값은 시스템의 모든 사용자 간에 고유해야 합니다. 그렇지 않으면 한 사용자를 위한 메시지가 다른 사용자에게 전달될 수 있습니다.

이 구성 요소를 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 클라이언트에서 속성을 설정하여 Windows 인증을 UseDefaultCredentials 사용하도록 설정해야 합니다.

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

Windows 인증은 Microsoft Edge에서 지원되지만 모든 브라우저에서 지원되는 것은 아닙니다. 예를 들어 Chrome 및 Safari에서 Windows 인증 및 WebSocket을 사용하려고 하면 실패합니다. Windows 인증이 실패하면 클라이언트는 작동할 수 있는 다른 전송으로 대체하려고 시도합니다.

클레임을 사용하여 ID 처리 사용자 지정

사용자를 인증하는 앱은 사용자 클레임에서 SignalR 사용자 ID를 파생시킬 수 있습니다. SignalR에서 사용자 ID를 만드는 방법을 지정하려면 IUserIdProvider를 구현하고 구현을 등록합니다.

샘플 코드는 클레임을 사용하여 사용자의 전자 메일 주소를 식별 속성으로 선택하는 방법을 보여줍니다.

참고 항목

선택한 값은 시스템의 모든 사용자 간에 고유해야 합니다. 그렇지 않으면 한 사용자를 위한 메시지가 다른 사용자에게 전달될 수 있습니다.

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

계정 등록은 ClaimsTypes.Email 형식의 클레임을 ASP.NET ID 데이터베이스에 추가합니다.

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 리소스 매개 변수가 전달되므로 내부 논리는 Hub가 호출되는 컨텍스트를 검사하고 사용자가 개별 Hub 메서드를 실행할 수 있도록 허용할지를 결정할 수 있습니다.

[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 허브에 연결하는 사용자 인증

SignalRASP.NET Core 인증과 함께 를 사용하여 각 연결에 사용자를 연결할 수 있습니다. 허브에서 인증 데이터는 속성에서 HubConnectionContext.User 액세스할 수 있습니다. 인증을 통해 허브는 사용자와 연결된 모든 연결에서 메서드를 호출할 수 있습니다. 자세한 내용은 SignalR에서 사용자 및 그룹 관리를 참조하세요. 여러 연결이 단일 사용자와 연결될 수 있습니다.

다음은 SignalR 및 ASP.NET Core 인증을 사용하는 Startup.Configure의 예입니다.

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 인증을 사용하려면 앱에서 cookie에 대한 인증 데이터를 교환하는 API를 제공해야 합니다.

전달자 토큰 인증

클라이언트는 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 요청 전에 호출됩니다. 연결을 활성 상태로 유지하기 위해 토큰을 갱신해야 하는 경우(연결 중에 만료될 수 있으므로) 이 함수 내에서 토큰을 갱신하고 업데이트된 토큰을 반환합니다.

표준 웹 API에서 전달자 토큰은 HTTP 헤더로 전송됩니다. 그러나 SignalR은 일부 전송을 사용할 때 브라우저에서 이러한 헤더를 설정할 수 없습니다. WebSocket 및 Server-Sent 이벤트를 사용하는 경우 토큰은 쿼리 문자열 매개 변수로 전송됩니다.

기본 제공 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 및 Server-Sent 이벤트에 연결할 때 브라우저에서 사용됩니다. HTTPS를 사용하는 경우 쿼리 문자열 값은 TLS 연결로 보호됩니다. 그러나 많은 서버에서 쿼리 문자열 값을 기록합니다. 자세한 내용은 ASP.NET Core SignalR의 보안 고려 사항을 참조하세요. SignalR은 헤더를 사용하여 토큰을 지원하는 환경(예: .NET 및 Java 클라이언트)에서 토큰을 전송합니다.

Identity 서버 JWT 인증

Identity 서버를 사용하는 경우 프로젝트에 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은 해당 ID를 사용하여 허브를 보호할 수 있습니다. 그러나 개별 사용자에게 메시지를 보내려면 사용자 지정 사용자 ID 공급자를 추가해야 합니다. Windows 인증 시스템은 "이름 식별자" 클레임을 제공하지 않습니다. SignalR은 클레임을 사용하여 사용자 이름을 확인합니다.

IUserIdProvider를 구현하는 새 클래스를 추가하고 식별자로 사용할 사용자로부터 클레임 중 하나를 검색합니다. 예를 들어 "Name" 클레임(형식 [Domain]\[Username]의 Windows 사용자 이름)을 사용하려면 다음 클래스를 만듭니다.

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

ClaimTypes.Name 대신 User의 모든 값(예: Windows SID 식별자 등)을 사용할 수 있습니다.

참고 항목

선택한 값은 시스템의 모든 사용자 간에 고유해야 합니다. 그렇지 않으면 한 사용자를 위한 메시지가 다른 사용자에게 전달될 수 있습니다.

Startup.ConfigureServices 메서드에 이 구성 요소를 등록합니다.

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

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

.NET 클라이언트에서 속성을 설정하여 Windows 인증을 UseDefaultCredentials 사용하도록 설정해야 합니다.

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

Windows 인증은 Internet Explorer 및 Microsoft Edge에서 지원되지만 모든 브라우저에서 지원되는 것은 아닙니다. 예를 들어 Chrome 및 Safari에서 Windows 인증 및 WebSocket을 사용하려고 하면 실패합니다. Windows 인증이 실패하면 클라이언트는 작동할 수 있는 다른 전송으로 대체하려고 시도합니다.

클레임을 사용하여 ID 처리 사용자 지정

사용자를 인증하는 앱은 사용자 클레임에서 SignalR 사용자 ID를 파생시킬 수 있습니다. SignalR에서 사용자 ID를 만드는 방법을 지정하려면 IUserIdProvider를 구현하고 구현을 등록합니다.

샘플 코드는 클레임을 사용하여 사용자의 이메일 주소를 식별 속성으로 선택하는 방법을 보여줍니다.

참고 항목

선택한 값은 시스템의 모든 사용자 간에 고유해야 합니다. 그렇지 않으면 한 사용자를 위한 메시지가 다른 사용자에게 전달될 수 있습니다.

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

계정 등록은 ClaimsTypes.Email 형식의 클레임을 ASP.NET ID 데이터베이스에 추가합니다.

// 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 리소스 매개 변수가 전달되므로 내부 논리는 Hub가 호출되는 컨텍스트를 검사하고 사용자가 개별 Hub 메서드를 실행할 수 있도록 허용할지를 결정할 수 있습니다.

[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를 삽입할 필요가 없다는 것입니다.

추가 리소스