Пользователь (IPrincipal) не доступен для конструктора ApiController с использованием Web Api 2.1 и Owin

Я использую Web Api 2.1 с идентификатором Asp.Net 2. Я пытаюсь получить аутентифицированного пользователя в своем конструкторе ApiController (я использую AutoFac для ввода моих зависимостей), но пользователь показывает, что он не аутентифицирован, когда конструктор называется.

Я пытаюсь получить Пользователя, чтобы я мог генерировать информацию аудита для любых операций записи в БД.

Несколько вещей, которые я делаю, могут помочь в диагнозе:
Я использую только app.UseOAuthBearerTokens как аутентификацию с помощью Asp.Net Identity 2. Это означает, что я удалил app.UseCookieAuthentication(new CookieAuthenticationOptions()), который включен по умолчанию при создании нового проекта Web Api 2.1 с идентификатором Asp.Net 2.

Внутри WebApiConfig Я вставляю свой репозиторий:

builder.RegisterType<ValueRepository>().As<IValueRepository>().InstancePerRequest();

Здесь мой контроллер:

[RoutePrefix("api/values")]
public class ValuesController : ApiController
{
    private IValueRepository valueRepository;

    public ValuesController(IValueRepository repo)
    {
        valueRepository = repo;
        // I would need the User information here to pass it to my repository
        // something like this:
        valueRepository.SetUser(User);
    }

    protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
    {
        base.Initialize(controllerContext);

        // User is not avaliable here either...
    }
}

Но если я проверяю объект User на конструкторе, это то, что я получаю: User

Проверка подлинности работает, если я не передаю свой токен, он ответит Unauthorized. Если я передаю токен, и я пытаюсь получить доступ к пользователю из любого из методов, он проверяется и заполняется правильно. Он просто не отображается на конструкторе, когда он вызывается.

В моем WebApiConfig я использую:

public static void Register(HttpConfiguration config)
{
    config.SuppressDefaultHostAuthentication();
    config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

    // Web API routes
    config.MapHttpAttributeRoutes();

    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

     // ... other unrelated injections using AutoFac
 } 

Я заметил, что если я удалю эту строку: config.SuppressDefaultHostAuthentication() Пользователь заносится в конструктор.

Ожидается ли это? Как я могу получить аутентифицированного пользователя в конструкторе?

EDIT: Как предложил Рикард, я попытался получить пользователя в методе Initialize, но он все еще не доступен, что дает мне то же самое, что описано на изображении.

Ответ 1

Не знаю, насколько это актуально, но у меня были те же проблемы, что и выше. Мне удалось решить эту проблему, используя пользовательский компонент промежуточного программного обеспечения OWIN.

Некоторая информация о моей структуре приложения:

  • Использование MVC WebApp и WebAPI в одном проекте (возможно, не самый лучший вариант, но у меня нет времени его менять, так как приближается крайний срок;))
  • Использование AutoFac в качестве контейнера IoC
  • Реализован пользовательский ICurrentContext для хранения информации о текущем пользователе (с CookieAuth в MVC и Toker Auten в WebAPI), который вводится там, где это необходимо (контроллеры, объекты BAL и т.д.).
  • Использование EntityFramework 6 для доступа к Db
  • Преобразованная идентификация ASP.NET для использования ключей int вместо строки (http://www.asp.net/identity/overview/extensibility/change-primary-key-for-users-in-aspnet-identity)

Итак, к коду. Это мой интерфейс ICurrentContext:

public interface ICurrentContext
{
     User CurrentUser { get; set; } // User is my User class which holds some user properties
     int? CurrentUserId { get; }
}

и его реализация:

public class DefaultCurrentContext : ICurrentContext
{
    public User CurrentUser { get; set; }

    public int? CurrentUserId { get { return User != null ? CurrentUser.Id : (int?)null; } }
}

Я также создал компонент промежуточного программного обеспечения OWIN:

using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;

namespace MyWebApp.Web.AppCode.MiddlewareOwin
{
    public class WebApiAuthInfoMiddleware : OwinMiddleware
    {
        public WebApiAuthInfoMiddleware(OwinMiddleware next)
            : base(next)
        {
        }

        public override Task Invoke(IOwinContext context)
        {
            var userId = context.Request.User.Identity.GetUserId<int>();
            context.Environment[MyWebApp.Constants.Constant.WebApiCurrentUserId] = userId;
            return Next.Invoke(context);
        }
    }
}

Некоторая информация об этом компоненте: MyWebApp.Constants.Constant.WebApiCurrentUserId - это некоторая строковая константа (вы можете использовать свою собственную), которую я использовал, чтобы избежать опечаток, поскольку она используется в нескольких местах. В основном, что делает это промежуточное программное обеспечение, заключается в том, что он добавляет текущий UserId в словарь среды OWIN и затем вызывает следующее действие в конвейере.

Затем я создал оператор расширения * для включения OMC (OWIN Middleware Component) в конвейер OWIN:

using System;
using Owin;

namespace MyWebApp.Web.AppCode.MiddlewareOwin
{
    public static class OwinAppBuilderExtensions
    {
        public static IAppBuilder UseWebApiAuthInfo(this IAppBuilder @this)
        {
            if (@this == null)
            {
                throw new ArgumentNullException("app");
            }

            @this.Use(typeof(WebApiAuthInfoMiddleware));
            return @this;
        }
    }
}

Чтобы использовать этот OMC, я поместил оператор Use * сразу после использования оператора * для маркера Bearer внутри моего Startup.Auth.cs:

// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions); // This was here before

// Register AuthInfo to retrieve UserId before executing of Api controllers
app.UseWebApiAuthInfo(); // Use newly created OMC

Теперь фактическое использование этого принципа было в методе AutoFac Register (вызвано некоторым кодом начальной загрузки в начале веб-приложения, в моем случае это было внутри класса Startup (Startup.cs), метода конфигурации) для моей реализации ICurrentContext, которая является:

private static void RegisterCurrentContext(ContainerBuilder builder)
{
    // Register current context
    builder.Register(c =>
    {
        // Try to get User Id first from Identity of HttpContext.Current
        var appUserId = HttpContext.Current.User.Identity.GetUserId<int>();

        // If appUserId is still zero, try to get it from Owin.Enviroment where WebApiAuthInfo middleware components puts it.
        if (appUserId <= 0)
        {
            object appUserIdObj;
            var env = HttpContext.Current.GetOwinContext().Environment;
            if (env.TryGetValue(MyWebApp.Constants.Constant.WebApiCurrentUserId, out appUserIdObj))
            {
                appUserId = (int)appUserIdObj;
            }
        }

        // WORK: Read user from database based on appUserId and create appUser object.

        return new DefaultCurrentContext
        {
            CurrentUser = appUser,
        };
    }).As<ICurrentContext>().InstancePerLifetimeScope();
}

Этот метод вызывается, где я создаю контейнер AutoFac (следовательно, входной параметр типа ContainerBuilder).

Таким образом, я получил единую реализацию CurrentContext, независимо от того, как пользователь был аутентифицирован (через веб-приложение MVC или веб-API). Вызовы Web API в моем случае были сделаны из какого-либо настольного приложения, но база данных и большая часть кодовой базы были одинаковыми для MVC App и веб-API.

Не знаю, есть ли у него правильный путь, но он сработал у меня. Хотя я все еще немного обеспокоен тем, как это будет вести себя по-настоящему, поскольку я точно не знаю, как будет работать HttpContext.Current внутри вызовов API. Я где-то читал, что словарь OWIN используется для каждого запроса, поэтому я считаю, что это безопасный подход. И я также думаю, что это не такой аккуратный код, а немного неприятный взломать для чтения UserId.;) Если с этим соглашением что-то не так, я был бы признателен за любые замечания относительно этого. Я боролся с этим в течение двух недель, и это самое близкое, что я получил от UserId в одном месте (при разрешении ICurrentContext из AutoFac через lambda).

ПРИМЕЧАНИЕ. Где бы то ни было использование GetUserId, его можно заменить оригинальной реализацией GetUserId (которая возвращает строку). Причина, по которой я использую GetUserId, заключается в том, что я в некоторой степени переписал ASP.NET для использования int вместо строк для TKey. Я сделал это на основе следующей статьи: http://www.asp.net/identity/overview/extensibility/change-primary-key-for-users-in-aspnet-identity

Ответ 2

Пользовательское свойство контроллера не заполняется до тех пор, пока не будет вызван метод Initialize, который возникает после вызова конструктора, следовательно, поэтому Identity еще не заполнена авторизованными пользовательскими данными.

Ответ 3

Проблема лежит в самом деле с config.SuppressDefaultHostAuthentication().

Эта статья Брок Аллена прекрасно объясняет, почему это так. Метод задает основной намеренно значение null, так что аутентификация по умолчанию, такая как файлы cookie, не работает. Вместо этого фильтр проверки подлинности веб-API затем берет на себя часть аутентификации.

Удаление этой конфигурации, если у вас нет аутентификации cookie, может быть вариантом.

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

public void Configuration(IAppBuilder app)
{
    var configuration = WebApiConfiguration.HttpConfiguration;
    app.Map("/api", inner =>
    {
        inner.SuppressDefaultHostAuthentication();
        // ...
        inner.UseWebApi(configuration);
    });
}

Ответ 4

Я понял, что удаление config.SuppressDefaultHostAuthentication() позволило мне получить идентификатор в конструкторе намного раньше. Тем не менее, я бы не предложил сделать это, если вы используете аутентификацию Token.

Ответ 5

Thread.CurrentPrincipical доступен по всему конвейеру, вы можете пропустить регистрацию пользователя ниже:

valueRepository.SetUser(User);

и доступ

Thread.CurrentPrincipical

Вместо этого в репозитории создается контекст репозитория. Кроме того, вы можете добавить контекстный слой.

Ответ 6

Если ни одно из вышеперечисленных решений не работает, попробуйте следующее:

    public ActionResult GetFiles()
    {
        ...
        string domainID = System.Web.HttpContext.Current.Request.LogonUserIdentity.Name;
        ...
    }