Авторизация на основе ресурсов в SignalR

#asp.net-core #.net-core #signalr #azure-signalr

Вопрос:

У меня есть веб-API с пользовательскими политиками и обработчиками авторизации. Я хотел повторно использовать обработчики авторизации, но HttpContext равен нулю, когда атрибут используется в концентраторе signalr.

Например, это мой контроллер.

 [Authorize]
public sealed class ChatsController : ControllerBase
{
    [HttpPost("{chatId}/messages/send")]
    [Authorize(Policy = PolicyNames.ChatParticipant)]
    public Task SendMessage() => Task.CompletedTask;
}
 

И это мой обработчик авторизации. Я могу извлечь «chatId» из HttpContext, а затем использовать свою пользовательскую логику для авторизации пользователя.

 internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement)
    {
        if(_httpContextAccessor.HttpContext != null)
        {
            // Logic
        }

        return Task.CompletedTask;
    }
}
 

Однако это не будет работать с Azure SignalR, потому что у меня нет доступа к HttpContext. Я знаю, что могу предоставить пользовательский идентификатор пользователя, но я понятия не имею, как получить доступ к «chatId» из метода «Присоединиться» в моем пользовательском обработчике авторизации.

 [Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
    [Authorize(Policy = PolicyNames.ChatParticipant)]
    public async Task Join(Guid chatId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
}
 

Можно ли повторно использовать мои обработчики авторизации?
Я бы хотел избежать копирования моего кода.
Одно из решений состоит в том, чтобы извлечь мой код авторизации для отдельных служб, но затем мне придется вручную вызывать их из своих концентраторов и отказаться от способа [Авторизации].

Ответ №1:

Ваш чат-это ресурс, и вы хотите использовать авторизацию на основе ресурсов. В этом случае декларативной авторизации с атрибутом недостаточно, поскольку идентификатор чата известен только во время выполнения. Таким образом, вы должны использовать императивную авторизацию IAuthorizationService .

Теперь в вашем центре:

 [Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
    private readonly IAuthorizationService authService;

    public ChatHub(IAuthorizationService authService)
    {
        this.authService = authService;
    }

    public async Task Join(Guid chatId)
    {
        // Get claims principal from authorized hub context
        var user = this.Context.User;

        // Get chat from DB or wherever you store it, or optionally just pass the ID to the authorization service
        var chat = myDb.GetChatById(chatId);

        var validationResult = await this.authService.AuthorizeAsync(user, chat, PolicyNames.ChatParticipant);

        if (validationResult.Succeeded)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
        }
    }
}
 

Ваш обработчик авторизации должен выглядеть по-другому, потому что для такой оценки ему нужен ресурс чата в его подписи:

 internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, Chat>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, Chat chat)
    {
        // You have both user and chat now
        var user = context.User;
        if (this.IsMyUserAuthorizedToUseThisChat(user, chat))
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }

        return Task.CompletedTask;
    }
}
 

Правка: на самом деле есть еще один вариант, о котором я не знал

Вы можете использовать HubInvocationContext тот концентратор SignalR, который предоставляет авторизованные методы. Это может быть автоматически введено в ваш обработчик авторизации, который должен выглядеть следующим образом:

 public class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, HubInvocationContext hubContext)
        {
            var chatId = Guid.Parse((string)hubContext.HubMethodArguments[0]);
        }
    }
 

Метод ступицы будет обычно украшен [Authorize(Policy = PolicyNames.ChatParticipant)]

У вас все равно будет два обработчика авторизации, AuthorizationHandler<ChatParticipantRequirement> и AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext> обойти это невозможно. Что касается дублирования кода, вы, однако , можете просто получить идентификатор чата в обработчике либо из HttpContext или HubInvocationContext , а затем передать его вам, написанному на заказ MyAuthorizer , который вы могли бы ввести в оба обработчика:

 public class MyAuthorizer : IMyAuthorizer 
{
  public bool CanUserChat(Guid userId, Guid chatId);
}
 

Комментарии:

1. Спасибо, все так, как я и думал, я не могу повторно использовать свою [Авторизацию(Политика = Имена политик. Участник чата)] и я должен вручную вызвать свою службу авторизации.

2. Что ж, вы все равно можете повторно использовать обработчик политики и оставаться в рамках авторизации .net core. Просто невозможно передать информацию о времени выполнения (идентификатор чата) в атрибут, потому что это конструкция времени компиляции. Это можно сделать, если вы авторизуетесь на каждом ресурсе.

3. Да, я знаю, но в обработчике авторизации я мог получить «Идентификатор чата» из значений/запроса в HttpContext, поэтому мне не нужно было получать объект «Чат» из базы данных каждый раз, когда я хотел использовать свой обработчик авторизации.