Внедрение зависимостей Autofac в реализации OAuthAuthorizationServerProvider

Я создаю приложение Web Api, и я хочу использовать токены-носители для аутентификации пользователя. Я реализовал логику токенов, следуя этой статье, и все работает нормально. ПРИМЕЧАНИЕ. Я не использую ASP.NET Identity Provider. Вместо этого я создал для него пользовательский объект и службы.

 public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureOAuth(app);

        var config = new HttpConfiguration();
        var container = DependancyConfig.Register();
        var dependencyResolver = new AutofacWebApiDependencyResolver(container);
        config.DependencyResolver = dependencyResolver;

        app.UseAutofacMiddleware(container);
        app.UseAutofacWebApi(config);

        WebApiConfig.Register(config);
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
        app.UseWebApi(config);
    }

    public void ConfigureOAuth(IAppBuilder app)
    {
        var oAuthServerOptions = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new SimpleAuthorizationServerProvider()
        };

        // Token Generation
        app.UseOAuthAuthorizationServer(oAuthServerOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

    }
}

и это моя реализация класса SimpleAuthorizationServerProvider

private IUserService _userService;
    public IUserService UserService
    {
        get { return (IUserService)(_userService ?? GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IUserService))); }
        set { _userService = value; }
    }

    public async override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        context.Validated();
    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

        var user = await UserService.GetUserByEmailAndPassword(context.UserName, context.Password);

        if (user == null)
        {
            context.SetError("invalid_grant", "The user name or password is incorrect.");
            return;
        }

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);
        identity.AddClaim(new Claim("sub", context.UserName));
        identity.AddClaim(new Claim("role", "user"));

        context.Validated(identity);

    }
}

После вызова URL-адреса я получаю следующую ошибку

Никакая область с тегом, соответствующим "AutofacWebRequest", не видна из области, в которой запрашивался экземпляр. Это обычно указывает на то, что компонент, зарегистрированный в качестве HTTP-запроса, запрашивается компонентом SingleInstance() (или аналогичным сценарием). В рамках веб-интеграции всегда запрашиваются зависимости от DependencyResolver.Current или ILifetimeScopeProvider.RequestLifetime, никогда из самого контейнера

Есть ли способ использовать инъекцию зависимостей внутри этого класса? Я использую шаблон репозитория для доступа к моим объектам, поэтому я не думаю, что это хорошая идея создать новый экземпляр контекста объекта. Каков правильный способ сделать это?

Ответы

Ответ 1

У меня была аналогичная проблема.

Проблема заключается в том, что при попытке ввести IUserService у вашего провайдера Autofac обнаруживает, что он был зарегистрирован как InstancePerRequest (который использует известный тег области видимости времени 'AutofacWebRequest'), но SimpleAuthorizationServerProvider зарегистрирован в области контейнера 'root', где область 'AutofacWebRequest' не отображается.

Предлагаемое решение заключается в регистрации зависимостей как InstancePerLifetimeScope. Это, видимо, решило проблему, но вводит новые. Все зависимости регистрируются в области 'root', что подразумевает наличие тех же DbContext и экземпляров служб для всех запросов. Steven объясняет очень хорошее в этом ответе, почему не рекомендуется делиться DbContext между запросы.

После более сложных задач исследования я решил проблему получения 'AutofacWebRequest' из OwinContext в классе OAuthAuthorizationServerProvider и разрешения зависимостей служб от него, вместо того, чтобы позволить Autofac автоматически вводить их. Для этого я использовал метод расширения OwinContextExtensions.GetAutofacLifetimeScope() из Autofac.Integration.Owin, см. Пример ниже:

using Autofac.Integration.Owin;
...
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
    ...
    // autofacLifetimeScope is 'AutofacWebRequest'
    var autofacLifetimeScope = OwinContextExtensions.GetAutofacLifetimeScope(context.OwinContext);
    var userService = autofacLifetimeScope.Resolve<IUserService>();
    ...
}

Я сделал OAuthAuthorizationServerProvider регистрацию и впрыскивание внутри метода ConfigureOAuth аналогичным образом, чем предлагалось Laurentiu Stamate в другой ответ на этот вопрос, как SingleInstance(). Я реализовал RefreshTokenProvider таким же образом.

ИЗМЕНИТЬ

@BramVandenbussche, это мой метод Configuration в классе Startup, где вы можете увидеть порядок промежуточных ссылок, добавленных в конвейер OWIN:

public void Configuration(IAppBuilder app)
{
    // Configure Autofac
    var container = ConfigureAutofac(app);

    // Configure CORS
    ConfigureCors(app);

    // Configure Auth
    ConfigureAuth(app, container);

    // Configure Web Api
    ConfigureWebApi(app, container);
}

Ответ 2

Чтобы использовать инъекцию зависимостей в SimpleAuthorizationServerProvider, вам необходимо зарегистрировать IOAuthAuthorizationServerProvider в контейнере Autofac, как и любой другой тип. Вы можете сделать что-то вроде этого:

builder
  .RegisterType<SimpleAuthorizationServerProvider>()
  .As<IOAuthAuthorizationServerProvider>()
  .PropertiesAutowired() // to automatically resolve IUserService
  .SingleInstance(); // you only need one instance of this provider

Вам также нужно передать контейнер методу ConfigureOAuth и позволить Autofac разрешить ваш экземпляр следующим образом:

var oAuthServerOptions = new OAuthAuthorizationServerOptions
{
    AllowInsecureHttp = true,
    TokenEndpointPath = new PathString("/token"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
    Provider = container.Resolve<IOAuthAuthorizationServerProvider>()
};

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

Ответ 3

Я также попробовал ответить @jumuro, используя OwinContextExtensions.GetAutofacLifetimeScope, который экономит мой день. Вместо того, чтобы регистрировать IUserService во время выполнения, этот ответ дает возможность проверки/создания службы экземпляра после запроса.

Я добавил новый ответ, потому что я не могу комментировать пока из-за моей низкой репутации, но добавил дополнительные коды, чтобы помочь кому-то.

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {

        try
        {
            if (service == null)
            {
                var scope = Autofac.Integration.Owin.OwinContextExtensions.GetAutofacLifetimeScope(context.OwinContext);
                service = scope.Resolve<IUserService>();
            }
            var user = await service.FindUserAsync(context.UserName);
            if (user?.HashedPassword != Helpers.CustomPasswordHasher.GetHashedPassword(context.Password, user?.Salt))
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                return;
            }
        }
        catch(Exception ex)
        {
            context.SetError("invalid_grant", ex.Message);
            return;
        }

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);
        identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));

        AuthenticationProperties properties = CreateProperties(context.UserName);
        AuthenticationTicket ticket = new AuthenticationTicket(identity, properties);
        context.Validated(ticket);
        context.Request.Context.Authentication.SignIn(identity);

    }