Как извлечь ClaimsPrincipal из AuthenticationStateProvider в службе промежуточного программного обеспечения Transient

У меня есть веб-приложение сервера blazor и рабочий процесс .NET Core, оба они используют общий класс для доступа к данным (общая единица работы/общий репозиторий).

В базе данных я хотел бы регистрировать имена пользователей, которые вставляют или редактируют записи. Для этого я хочу внедрить ClaimsPrincipal в общие классы UoW и Repo).

Итак, я хотел бы иметь возможность извлекать текущий ClaimsPrincipal в переходной службе с помощью внедрения зависимостей.

Для работника я могу внедрить ClaimsPrincipal с помощью следующего кода;

 public static IServiceCollection CreateWorkerClaimsPrincipal(this IServiceCollection services, string workerName) 
        {
            Claim workerNameClaim = new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", workerName);

            ClaimsIdentity identity = new ClaimsIdentity(
                new System.Security.Claims.Claim[] { workerNameClaim },
                "My-Worker-Authentication-Type", 
                "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
                "role");

            ClaimsPrincipal principal = new ClaimsPrincipal(identity);

            services.AddTransient<ClaimsPrincipal>(s => principal);

            return services;
        }

Это работает и удовлетворяет мои потребности.

Для веб-приложения сервера blazor мне нужно сделать что-то подобное.

Я считаю, что правильный способ извлечения ClaimsPrincipal — через AuthenticationStateProvider, однако для этого требуется вызов асинхронного метода GetAuthenticationStateAsync.

ПРИМЕЧАНИЕ. Я не могу использовать IHttpContextAccessor, так как это не работает со службой приложений Azure.

Я хочу что-то вроде;

public void ConfigureServices(IServiceCollection services)
{
    /// ...
    services.AddTransient<ClaimsPrincipal>(); // I think I need to do something here?
    /// ...
}

Поэтому, когда я запрашиваю ClaimsPrincipal через внедрение зависимостей, я хочу вернуть пользователя;

var authState = await AUthenticationStateProvider.GetAuthenticationStateAsync();
return authState.User;

Это возможно?


person Mark Cooper    schedule 04.03.2021    source источник
comment
Как вы используете ClaimsPrincipal в настоящее время и как вы хотите использовать, чтобы он не работал?   -  person Alexander    schedule 04.03.2021
comment
Какой аромат Blazor вы используете?   -  person enet    schedule 04.03.2021
comment
@Alexander Александр Я использую ClaimsPrincipal для регистрации информации аудита о том, какие пользователи какие действия выполняют в моей БД. Все мои действия с БД выполняются через общий класс UnitOfWork, и я хотел бы вводить информацию о пользователе здесь, а не передавать ее из каждой вызывающей службы. У меня есть как веб-, так и рабочие процессы, обращающиеся к классу UoW, поэтому я не могу внедрить AuthenticationStateProvider вместо ClaimsPrincipal.   -  person Mark Cooper    schedule 04.03.2021
comment
@enet Я использую серверную часть Blazor.   -  person Mark Cooper    schedule 04.03.2021
comment
Я добавил дополнительную информацию о контексте проблемы.   -  person Mark Cooper    schedule 04.03.2021
comment
Я не могу представить, почему IHttpContextAccessor не работает при развертывании приложения в Azure. Это так просто странно. Каждая обработка запроса использует только один и тот же контекст с именем HttpContext, здесь нет многопоточности. HttpContext — это одно место для хранения Пользователя (после аутентификации), поэтому, если вы создаете собственную службу с ограниченной областью действия, это просто другое место. Ваше решение кажется подходящим, если вы знаете, где передать свою службу области действия пользователю (конечно, сразу после аутентификации). Весь последующий код может использовать его нормально.   -  person King King    schedule 04.03.2021
comment
@KingKing да, поверьте мне, я очень усердно работал над созданием и тестированием масштабируемого и хорошо изолированного приложения, и был в супер самодовольном режиме, когда развертывал в Azure, а затем бум :-( Я прочитал подробности о том, почему это не работает, но это поставило меня в тупик...   -  person Mark Cooper    schedule 04.03.2021
comment
Я нашел работоспособное решение, но был бы признателен за любые комментарии по этому подходу, особенно по асинхронной инициализации, которая мне незнакома. Это кажется рискованным. Это безопасно?   -  person Mark Cooper    schedule 04.03.2021
comment
Не беспокоиться. Это безопасно, так как вы не делаете ничего рискованного или авантюрного... Вы только присваиваете постоянное значение AuthenticationStateProvider, и пока GetAuthenticationStateAsync не вызывается до того, как вы установите значение... все хорошо   -  person enet    schedule 04.03.2021
comment
@KingKing - я нашел объяснение, почему это не работает.... Кроме того, опять же из соображений безопасности, вы не должны использовать IHttpContextAccessor в приложениях Blazor. Приложения Blazor выполняются вне контекста конвейера ASP.NET Core. Не гарантируется, что HttpContext будет доступен в IHttpContextAccessor, а также не гарантируется содержание контекста, запустившего приложение Blazor. взято из docs.microsoft.com/en-us/aspnet/core/fundamentals/   -  person Mark Cooper    schedule 05.03.2021
comment
@MarkCooper спасибо, что поделились, на самом деле у меня раньше не было возможности поработать с Blazer.   -  person King King    schedule 05.03.2021


Ответы (1)


Как это часто бывает, разработав это в простом примере для сообщения SO, я нашел работоспособное (я думаю) решение из https://docs.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-5.0#implement-a-custom-authenticationstateprovider


ПРИМЕЧАНИЕ. Я до сих пор не уверен на 100 %, что шаблон асинхронной инициализации всегда разрешает AuthenticationState до вызова свойства Repository, но до сих пор он держится вместе... Просто остерегайтесь этого, если вы решите использовать этот код.


Я изменил подход и вместо того, чтобы пытаться разрешить ClaimsPrincipal через DI (поскольку AuthenticationStateProvider недоступен для рабочего процесса), я создал пользовательский AuthenticationStateProvider в рабочем процессе.

    public class WorkerAuthStateProvider : AuthenticationStateProvider
    {
        private readonly string _workerName;

        public WorkerAuthStateProvider(string workerName)
        {
            _workerName = workerName;
        }

        public override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var identity = new ClaimsIdentity(new[] {
                new Claim(ClaimTypes.Name, _workerName),
            }, "My-Worker-Authentication-Type");

            ClaimsPrincipal user = new ClaimsPrincipal(identity);

            return Task.FromResult(new AuthenticationState(user));
        }
    }

а затем зарегистрируйте это в configureServices, чтобы разрешить экземпляры AuthenticationStateProvider в файле worker program.cs (также передав имя пользовательского рабочего процесса, чтобы я мог использовать его для всех своих worker'ов);

services.AddScoped<AuthenticationStateProvider, WorkerAuthStateProvider>(serviceProvider =>
{
   return new WorkerAuthStateProvider(Constants.Logging.RoleNames.MYWORKERNAME);
});

AuthenticationStateProvider уже работает в веб-приложениях blazor, поэтому это позволяет мне правильно решить эту проблему, например, в конструкторе для моего шаблона GenericUnitOfWork для доступа к данным как в Интернете, так и в Workers;

        private TDbContext _dbContext;
        private readonly ILogger<TEntity> _logger;
        private GenericRepository<TEntity, TDbContext> _repository;
        private ClaimsPrincipal _user;
        private readonly AuthenticationStateProvider _authenticationStateProvider;


        public GenericUnitOfWork(TDbContext context, ILogger<TEntity> logger, AuthenticationStateProvider authenticationStateProvider)
        {
            _dbContext = context;
            _logger = logger;
            _authenticationStateProvider = authenticationStateProvider;
            UserInit = InitUserAsync();
        }

        /// <summary>
        /// Async initialisation pattern from https://blog.stephencleary.com/2013/01/async-oop-2-constructors.html
        /// </summary>
        public Task UserInit { get; private set; }

        private async Task InitUserAsync()
        {
            var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
            _user = authState.User;
        }

        public IGenericRepository<TEntity, TDbContext> Repository
        {
            get
            {
                if (_repository == null)
                {
                    // when accessing the repository, we are expecting to pass the current application claims principal
                    // however the ClaimsPrincipal is resolved using an Async method from the AuthenticationStateProvider.
                    // In the event that the Async method has not yet completed we need to throw an exception so we can determine
                    // if a further async code fix is required.
                    if (_user == null)
                    {
                        throw new InvalidOperationException("Async ClaimsPrincipal has not been loaded from the AuthenticationStateProvider");
                    }

                    _repository = new GenericRepository<TEntity, TDbContext>(_dbContext, _logger, _user);
                   
                }
                return _repository;
            }
        }
person Mark Cooper    schedule 04.03.2021
comment
Если работает, то хорошо... - person enet; 04.03.2021
comment
@enet лол, спасибо (у) - person Mark Cooper; 04.03.2021