在mvc6中使用带有cookie身份验证的简单注入器

时间:2016-01-07 14:45:10

标签: asp.net authentication cookies asp.net-core-mvc simple-injector

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

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

我有一个自定义$routeParams.bookId / SignInManager包装UserManager来验证Windows凭据(SideNote:我没有使用带有aspnet 5的Azure AD,因为[将来]我知道将有一个窗口和非Windows用户名的混合。另外,我无法在足够的时间内获得这样做的权限)。我的初始问题是将PrincipalContext注入IHttpContextAccessorSignInManager两个类。我一直收到以下错误:

  

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

要解决我的问题,我必须从asp.net服务获取CookieAuthenticationOptions,然后使用简单的注入器注册它。这有效,但似乎错了,也许有另一种方法可以做到这一点。那么,这是错的吗?如果是这样,我希望其他人尝试过这个,如果存在,可以使用其他解决方案。以下是我班级的缩写版本:

IHttpContextAccessor

以下是 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

更新

如果这不正确,我相信我会得到纠正,但我选择了@Steven的答案作为适配器。我想这更像是一个我不太熟悉的设计模式的教训。这是我将在我的自定义SignInManager中使用的新类和注册:

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:

3 个答案:

答案 0 :(得分:4)

集成包中的CrossWire扩展方法在Simple Injector中进行委托注册,允许Simple Injector“了解”服务,而ASP.NET 5配置系统仍然可以控制构建该服务。你可以自己做同样的事情:

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

CrossWire扩展方法似乎相当无用,因为它似乎是一个单行自己做,但CrossWire做了一件额外的事情,它抑制了一个瞬态组件实现IDisposable时引发的诊断警告。这解决了ASP.NET 5中的设计缺陷,因为ASP.NET 5中有抽象实现IDisposable,而抽象绝不应该实现IDisposable(抽象,违反依赖倒置原则)。

但这让我想到了下一点,CrossWire总是使Simple Injector中的注册成为瞬态,即使在ASP.NET中注册可能是作用域或单例。组件在ASP.NET中具有哪种生活方式通常是实现细节,并且可能会不时发生变化。或者至少,用户和Simple Injector都不知道生活方式。这就是为什么默认情况下为所有跨线路注册提供瞬态生活方式是最安全的。然而,这确实意味着所有依赖的应用程序组件也应该是瞬态的,以防止强制性依赖(a.k.a. Lifestyle Mismatches)。我会说这通常不是问题,因为依赖ASP.NET服务的应用程序组件与ASP.NET非常相关。您的核心应用程序组件不太可能依赖于ASP.NET内容,因为这会违反依赖性倒置原则,并可能导致难以维护代码。

在你的情况下,你可以做一些事情。最简单的方法是使SignInManager也是瞬态的。它似乎不太可能有任何状态它应该维持一个单一的请求,当它确实存在时,该状态可能不属于那里(单一责任违规)。

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

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

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

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

这些基本上是您的选择。

答案 1 :(得分:0)

我必须做类似的事情才能使用simpleinjector注册IdentityOptionsIDataProtectionProvder以获得一些身份验证工作。我不会认为它&#34;错误&#34;,但我可以,并且我确信史蒂文会以他的规范意见来设定我们都在正确的道路上。

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

答案 2 :(得分:0)

更新

您应该能够在此实例中禁止诊断警告(我无法让代码工作到足以在今晚确认它,但抱歉)

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

在我看来,您的注册将始终返回accessor的同一个实例。

  1. 解析IHttpContextAccessor的实例:

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

  2. 注册委托以始终返回相同的实例:

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

  3. 我建议您尝试管理您不拥有的对象的生命周期并依赖于CrossWire

    ,从而避免使注册变得复杂化
    container.CrossWire<IHttpContextAccessor>(app);