在具有Cookie身份验证的mvc6中使用简单的注入器 [英] using simple injector in mvc6 with cookie auth

查看:72
本文介绍了在具有Cookie身份验证的mvc6中使用简单的注入器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a MVC6 project using simple injector and cookie middleware for authentication without ASP.NET identity (tutorials below)

http://simpleinjector.readthedocs.org/en/latest/aspnetintegration.html http://docs.asp.net/en/latest/安全性/身份验证/cookie.html

我有一个自定义的SignInManager/UserManager,其中包装了PrincipalContext以验证Windows凭据(侧注:我不将Azure AD与aspnet 5一起使用,因为[将来]我知道将会混合使用Windows和非Windows用户名.另外,我没有足够的时间来获得权限).我最初的问题是将IHttpContextAccessor注入到SignInManagerCookieAuthenticationOptions到这两个类中.我不断收到以下错误:

I have a custom SignInManager / UserManager that wraps PrincipalContext to validate windows credentials (SideNote: I am not using the Azure AD with aspnet 5 because [in the future] I know there will be a mix of windows and non windows usernames. Plus I could not get permissions to do so in enough time). My initial issue was injecting IHttpContextAccessor into the SignInManager and CookieAuthenticationOptions into both classes. I kept receiving the error below:

未配置身份验证处理程序来处理方案:ThisCompany.Identity

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

要解决我的问题,我必须从asp.net服务获取IHttpContextAccessor,然后使用简单的注入器进行注册.这是可行的,但似乎是错误的,也许还有另一种方法可以做到.那么,这是错的吗?如果是这样,我希望其他人可以尝试此方法,并且可以在存在其他解决方案的情况下使用.以下是我的课程的缩写版本:

To solve my issue I had to get the IHttpContextAccessor from asp.net services and then register it with simple injector. This worked, but seemed wrong and maybe there is another way to do it. So, is this wrong? If so, I was hoping others have tried this and can chime in with another solution if it exists. Below are abbreviated versions of my 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并声明了创建内容:

And here is the SignInManager (I won't show the UserManager, that wraps my repo,PrincipalContext and claims creation:

  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);

例外:由于AuthController也是作用域,因此ISignInManager被作为作用域注入到我的AuthController中:

Exception: the ISignInManager is injected into my AuthController as scoped since AuthController is scoped too:

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中使用的以下新类和注册:

I am sure I will be corrected if this is not correct, but I went with @Steven's answer for an adapter. I guess this was more of a lesson of design patterns that I was not too familiar with. Here is my new class and registration below that I will use in my custom SignInManager:

  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);
}

推荐答案

集成包中的CrossWire扩展方法在Simple Injector中进行委托注册,允许Simple Injector在ASP时了解"服务. NET 5配置系统仍在控制该服务的构建.您可以自己执行以下操作:

The CrossWire extension method in the integration package makes a delegate registration in Simple Injector that allows Simple Injector to 'know' about the service, while the ASP.NET 5 configuration system is still in control of building that service. You can do the same yourself as follows:

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

CrossWire扩展方法似乎毫无用处,因为它似乎可以自己完成,但是CrossWire做的另一件事是,抑制瞬态组件实现IDisposable时引发的诊断警告.这可以解决ASP.NET 5中的设计缺陷,因为ASP.NET 5中存在实现IDisposable的抽象,而抽象绝不应该实现IDisposable(确实有抽象,违反了依赖倒置原则).

The CrossWire extension method seems rather useless, since it seems a one-liner to do it yourself, but CrossWire does one extra thing, which is suppressing the diagnostic warning that is thrown when a transient component implements IDisposable. This works around design flaws in ASP.NET 5, because there are abstractions in ASP.NET 5 that implement IDisposable, while abstractions should never implement IDisposable (abstractions that do, violate the Dependency Inversion Principle).

但是,这使我想起下一个问题,即使在ASP.NET中,注册可能是作用域或单例的,CrossWire总是使在Simple Injector中的注册为瞬态. ASP.NET中组件具有哪种生活方式通常是实现细节,并且可能会不时更改.至少,用户和Simple Injector都不了解这种生活方式.这就是为什么默认情况下为所有交叉接线注册提供临时生活方式最安全的原因.但是,这的确意味着所有依赖的应用程序组件也应该是瞬态的,以防止被限制的依赖关系(也称为生活方式不匹配).我会说这通常不是问题,因为依赖ASP.NET服务的应用程序组件与ASP.NET密切相关.您不太可能拥有依赖ASP.NET的核心应用程序组件,因为这将违反Dependency Inversion Principle,并可能导致难以维护代码.

But this brings me to the next point, CrossWire always makes the registration in Simple Injector as transient, even though in ASP.NET the registration might be scoped or singleton. Which lifestyle a component has in ASP.NET is often an implementation detail, and might change from time to time. Or at least, the lifestyle is unknown to both the user and Simple Injector. That's why it is safest to give all cross-wired registrations the transient lifestyle by default. This does mean however that all dependent application components should be transient as well to prevent Captive Dependencies (a.k.a. Lifestyle Mismatches). I would say this is typically not a problem, because application components that depend ASP.NET services are very ASP.NET related. It's unlikely that you have core application components depend on ASP.NET stuff, because that would violate the Dependency Inversion Principle and might lead to hard to maintain code.

根据您的情况,您可以做一些事情.最简单的方法是使SignInManager也处于瞬态状态.它似乎不太可能具有应在单个请求中维持的任何状态,并且当确实存在时,该状态可能无论如何都不应属于该请求(违反单一责任).

In your case you can do a few things. Simplest thing to do is to make SignInManager transient as well. It seems unlikely that it has any state that it should maintain over a single request, and when it does, that state should probably not belong there anyway (Single Responsibility Violation).

另一个选择是将IHttpContextAccessor交叉连接为Simple Injector中的单例.这是有效的,因为此服务也在ASP.NET中注册为单例.这不会造成任何隐藏的强制依赖(除非Microsoft更改将来的生存期;在这种情况下,我们都被搞砸了).您可以这样做:

Another option is to cross wire the IHttpContextAccessor as singleton in Simple Injector. This is valid, because this service is registered as singleton in ASP.NET as well. This won't cause any hidden Captive Dependencies (unless Microsoft changes the lifetime in the future; in that case we're all screwed). You can do it like this:

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

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

Your third option is to prevent registration of this IHttpContextAccessor completely. It is by itself already a Dependency Inversion Principle violation for your application code. It's a DIP violation, because IHttpContextAccessor is not defined by your application but by the framework. It will therefore never be defined in a way that exactly suits your application needs. Your application code will hardly ever need to get a HttpContext object. Rather it is interested in some particular value such as a UserId, TenantId, or other contextual value. So instead, your application is much better of when it depends on an IUserContext, ITenantContext or other specific abstraction. Whether or not the value is extracted from the HttpContext is an implementation detail.

这种实现(适配器)可以在运行时解析IHttpContextAccessor并从中获取HttpContext.当然,在大多数情况下,这种适配器的实现会非常简单,但这很好.我们的目标仅仅是使应用程序不受这些知识的影响.由于适配器具有有关ASP.NET抽象的知识,因此可以通过其配置解析服务.适配器只是一个反腐败层.

Such implementation (adapter) can resolve the IHttpContextAccessor at runtime and get the HttpContext from it. The implementation of such adapter would most of the time be really simple of course, but this is fine; our goal is simply to shield the application from this knowledge. Since the adapter has knowledge about the ASP.NET abstraction, it is fine for it to resolve services from it configuration. The adapter is merely an anti-corruption layer.

这些基本上是您的选择.

These are basically your options.

这篇关于在具有Cookie身份验证的mvc6中使用简单的注入器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆