在带有cookie auth的mvc6中使用简单的注入器

using simple injector in mvc6 with cookie auth

我有一个使用简单注入器和 cookie 中间件进行身份验证的 MVC6 项目没有ASP.NET身份(下面的教程)

http://simpleinjector.readthedocs.org/en/latest/aspnetintegration.html http://docs.asp.net/en/latest/security/authentication/cookie.html

我有一个自定义 SignInManager / UserManager 包装 PrincipalContext 以验证 windows 凭据(旁注:我没有将 Azure AD 与 aspnet 5 一起使用,因为 [在未来] 我知道会有 windows 和非 windows 用户名的混合。而且我无法在足够的时间内获得这样做的权限)。我最初的问题是将 IHttpContextAccessor 注入 SignInManager 并将 CookieAuthenticationOptions 注入两个 classes。我一直收到以下错误:

no authentication handler is configured to handle the scheme: ThisCompany.Identity

为了解决我的问题,我必须从 asp.net 服务中获取 IHttpContextAccessor,然后使用简单的注入器注册它。这行得通,但似乎是错误的,也许还有另一种方法可以做到。那么,这是错误的吗?如果是这样,我希望其他人已经尝试过这个并且可以在存在的情况下加入另一个解决方案。以下是我的 classes:

的缩写版本
  public class Startup
  {
    public static IConfigurationRoot Configuration;
    private readonly Container container = new Container();
    private readonly AppSettings settings;
    private readonly CookieAuthenticationOptions cookieOptions;

    public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
    {
      // config builder here...

      cookieOptions = createCookieOptions();
    }

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

      services.AddInstance<IControllerActivator>(new SimpleInjectorControllerActivator(container));
      services.AddInstance<IViewComponentInvokerFactory>(new SimpleInjectorViewComponentInvokerFactory(container));
      services.Add(ServiceDescriptor.Instance<IHttpContextAccessor>(new NeverNullHttpContextAccessor()));
    }

    public async void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IHostingEnvironment env)
    {
      app.UseCookieAuthentication(cookieOptions);

      #region DI

      container.Options.DefaultScopedLifestyle = new AspNetRequestLifestyle();
      container.Options.LifestyleSelectionBehavior = new ScopeLifestyleSelectionBehavior();
      app.UseSimpleInjectorAspNetRequestScoping(container);
      InitializeContainer(app);

      // this is the part I am unsure about
      var accessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();                    
      container.Register(() => accessor, Lifestyle.Scoped);

      container.RegisterAspNetControllers(app);
      container.RegisterAspNetViewComponents(app);
      container.Verify();

      #endregion

      using (var scope = SimpleInjectorExecutionContextScopeExtensions.BeginExecutionContextScope(container))
      {
        // seed cache and dummy data
      }
    }

    private void InitializeContainer(IApplicationBuilder app)
    {
      var conn = new SqlConnection(Configuration["Data:AppMainConnection"]);

      // bunch of registrations...

      container.RegisterSingleton(() => cookieOptions);
    }

    private sealed class NeverNullHttpContextAccessor : IHttpContextAccessor
    {
      private readonly AsyncLocal<HttpContext> context = new AsyncLocal<HttpContext>();

      public HttpContext HttpContext
      {
        get { return context.Value ?? new DefaultHttpContext(); }
        set { context.Value = value; }
      }
    }

    private sealed class ScopeLifestyleSelectionBehavior : ILifestyleSelectionBehavior
    {
      public Lifestyle SelectLifestyle(Type serviceType, Type implementationType)
      {
        return Lifestyle.Scoped;
      }
    }
    private CookieAuthenticationOptions createCookieOptions()
    {
      return new CookieAuthenticationOptions()
      {
        AuthenticationScheme = "ThisCompany.Identity",
        AutomaticChallenge = true,
        AutomaticAuthenticate = true,
        LoginPath = new PathString("/Auth/Login/"),
        LogoutPath = new PathString("/Auth/Logout"),
        AccessDeniedPath = new PathString("/Auth/Forbidden/"), // TODO
        CookieName = "yumyum.net",
        SlidingExpiration = true,
        ExpireTimeSpan = TimeSpan.FromDays(1),
        Events = new CookieAuthenticationEvents()
        {
          OnRedirectToAccessDenied = ctx =>
          {
            if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
            {
              ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
            }
            else
            {
              ctx.Response.Redirect(ctx.RedirectUri);
            }

            return Task.FromResult(0);
          }
        }
      };
    }

这是 SignInManager(我不会显示 UserManager,它包装了我的回购协议,PrincipalContext 并声称创建:

  public class SignInManager : ISignInManager
  {
    private readonly IUserManager userManager;
    private readonly HttpContext context;
    private readonly CookieAuthenticationOptions options;

    public SignInManager(IHttpContextAccessor contextAccessor, IUserManager userManager, CookieAuthenticationOptions options)
    {
      if (contextAccessor == null || contextAccessor.HttpContext == null)
      {
        throw new ArgumentNullException(nameof(contextAccessor));
      }
      if (options == null) throw new ArgumentNullException(nameof(options));   
      if (userManager == null) throw new ArgumentNullException(nameof(userManager));

      context = contextAccessor.HttpContext;
      this.userManager = userManager;
      this.options = options;
    }

    public async Task<bool> PasswordSignInAsync(string user, string password, bool isPersistent)
    {
      if (user == null) throw new ArgumentNullException(nameof(user));

      if (await userManager.CheckPasswordAsync(user, password))
      {
        await signInAsync(user, isPersistent);
        return true;
      }

      return false;
    }

    public async Task SignOutAsync() => await context.Authentication.SignOutAsync(options.AuthenticationScheme);

    private async Task signInAsync(string user, bool isPersistent)
    {
      var authenticationProperties = new AuthenticationProperties { IsPersistent = isPersistent };
      var userPrincipal = await userManager.CreateUserPrincipalAsync(user);
      if (userPrincipal == null) throw new InvalidOperationException($"{user} not found");

        // this is where the error was happening
        await context.Authentication.SignInAsync(options.AuthenticationScheme,
          new ClaimsPrincipal(userPrincipal),
          authenticationProperties);
     }
  }

更新

这是我添加 container.CrossWire<IHttpContextAccessor>(app); 和删除

时的详细信息
  var accessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();                    
  container.Register(() => accessor, Lifestyle.Scoped);

异常:ISignInManager 被注入到我的 AuthController 中作为范围,因为 AuthController 也是范围:

SimpleInjector.DiagnosticVerificationException was unhandled
  HResult=-2146233088
  Message=The configuration is invalid. The following diagnostic warnings were reported:
-[Lifestyle Mismatch] SignInManager (ASP.NET Request) depends on IHttpContextAccessor (Transient).
See the Error property for detailed information about the warnings. Please see https://simpleinjector.org/diagnostics how to fix problems and how to suppress individual warnings.
  Source=SimpleInjector
  StackTrace:
       at SimpleInjector.Container.ThrowOnDiagnosticWarnings()
       at SimpleInjector.Container.Verify(VerificationOption option)
       at SimpleInjector.Container.Verify()
       at Startup.<Configure>d__7.MoveNext() in ... line 109
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<>c.<ThrowAsync>b__6_1(Object state)
       at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
       at System.Threading.ThreadPoolWorkQueue.Dispatch()
       at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
  InnerException:

更新

我相信如果这不正确我会得到纠正,但我接受了@Steven 对适配器的回答。我想这更像是一堂我不太熟悉的设计模式课。这是我将在自定义 SignInManager 中使用的新 class 和下面的注册:

  public class DefaultAuthenticationManager : IAuthenticationManager
  {
    private readonly HttpContext context;

    public DefaultAuthenticationManager(IHttpContextAccessor accessor)
    {
      if (accessor == null || accessor.HttpContext == null) throw new ArgumentNullException(nameof(accessor));

      context = accessor.HttpContext;
    }

    public Task SignInAsync(string authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties properties)
    {
      return context.Authentication.SignInAsync(authenticationScheme, principal, properties);
    }

    public Task SignOutAsync(string authenticationScheme)
    {
     return  context.Authentication.SignOutAsync(authenticationScheme);
    }
  }


private void InitializeContainer(IApplicationBuilder app)
{
     var accessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();

     container.Register<IAuthenticationManager>(() => new DefaultAuthenticationManager(accessor), Lifestyle.Scoped);
}

我必须做类似的事情才能使用 simpleinjector 注册 IdentityOptionsIDataProtectionProvder 以使一些身份验证工作正常进行。我不认为是"wrong",但我可以,而且我相信史蒂文会附和他的规范意见,让我们都走上正确的道路。

一个小区别是我没有为我的 InitializeContainer 方法提供 IApplicationBuilder 实例,只有 IServiceProvider (也可以通过 IApplicationBuilder.ApplicationServices 属性).您真的需要整个 IApplicationBuilder 来初始化容器吗?

更新

您应该能够在这种情况下抑制诊断警告(抱歉,今晚我无法让代码正常工作以确认它)

container.CrossWire<IHttpContextAccessor>(app);
var registration = container.GetRegistration(
    typeof(IHttpContextAccessor)).Registration;
registration.SuppressDiagnosticWarning(
    DiagnosticType.LifestyleMismatch, "Owned by ASP.NET");

在我看来,您的注册总是 return accessor 的相同实例。

  1. 解析 IHttpContextAccessor:

    的实例

    var accessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();

  2. 注册一个代理以始终return同一个实例:

    container.Register(() => accessor, Lifestyle.Scoped);

我建议您通过尝试管理您不拥有并依赖的对象的生命周期来避免使注册复杂化 CrossWire

container.CrossWire<IHttpContextAccessor>(app);

集成包中的CrossWire扩展方法在Simple Injector中做了委托注册,允许Simple Injector'know'关于服务,而ASP.NET5配置系统还在控制构建该服务。你也可以自己做同样的事情:

container.Register(() => app.ApplicationServices.GetRequiredService<ISomeService>());

CrossWire 扩展方法似乎没什么用,因为它似乎 one-liner 自己做,但 CrossWire 做了一件额外的事情,它抑制了瞬变时抛出的诊断警告组件实现 IDisposable。这解决了 ASP.NET 5 中的设计缺陷,因为 ASP.NET 5 中有实现 IDisposable 的抽象,而抽象永远不应该实现 IDisposable(实现的抽象违反了依赖倒置原则)。

但这让我想到了下一点,CrossWire 总是使简单注入器中的注册成为瞬态的,即使在 ASP.NET 中注册可能是作用域的或单例的。组件在 ASP.NET 中的生活方式通常是一个实现细节,并且可能会不时更改。或者至少,用户和 Simple Injector 都不知道这种生活方式。这就是为什么默认为所有 cross-wired 注册提供短暂的生活方式是最安全的原因。然而,这确实意味着所有依赖的应用程序组件也应该是瞬态的,以防止捕获依赖(a.k.a。生活方式不匹配)。我会说这通常不是问题,因为依赖 ASP.NET 服务的应用程序组件非常 ASP.NET 相关。您的核心应用程序组件不太可能依赖 ASP.NET 东西,因为这会违反依赖倒置原则并可能导致难以维护代码。

对于您的情况,您可以做一些事情。最简单的做法是让 SignInManager 也成为瞬态的。它似乎不太可能有任何状态应该在单个请求上维护,并且当它存在时,该状态可能不应该属于那里(单一责任违规)。

另一种选择是将 IHttpContextAccessor 交叉连接为简单注入器中的单例。这是有效的,因为此服务也在 ASP.NET 中注册为单例。这不会导致任何隐藏的 Captive Dependencies(除非微软在未来改变生命周期;在那种情况下我们都完蛋了)。你可以这样做:

container.RegisterSingleton(app.ApplicationServices.GetRequiredService<IHttpContextAccessor>());

您的第三个选择是完全阻止此 IHttpContextAccessor 的注册。它本身已经违反了您的应用程序代码的依赖倒置原则。这是 DIP 违规,因为 IHttpContextAccessor 不是由您的应用程序定义的,而是由框架定义的。因此,它永远不会以完全适合您的应用程序需求的方式定义。您的应用程序代码几乎不需要获取 HttpContext 对象。相反,它对某些特定值感兴趣,例如 UserId、TenantId 或其他上下文值。因此,相反,当您的应用程序依赖于 IUserContext、ITenantContext 或其他特定抽象时,它会好得多。值是否从 HttpContext 中提取是一个实现细节。

这样的实现(适配器)可以在运行时解析 IHttpContextAccessor 并从中获取 HttpContext。当然,这种适配器的实现在大多数情况下会非常简单,但这很好;我们的目标只是保护应用程序免受这些知识的影响。由于适配器了解 ASP.NET 抽象,因此它可以从其配置中解析服务。适配器只是一个 anti-corruption 层。

这些基本上是您的选择。