问题
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/security/authentication/cookie.html
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:
no authentication handler is configured to handle the scheme: ThisCompany.Identity
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);
}
}
};
}
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);
}
}
UPDATE
Here are the details when I added container.CrossWire<IHttpContextAccessor>(app);
and remove
var accessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
container.Register(() => accessor, Lifestyle.Scoped);
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:
UPDATE
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);
}
回答1:
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>());
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).
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.
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).
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>());
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.
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.
回答2:
I had to do something similar in order to register IdentityOptions
and IDataProtectionProvder
with simpleinjector to get some authentication things to work. I don't think it's "wrong", but I could be, and I'm sure Steven will chime in with his canonical opinion to set us both on the right path.
One minor difference was that I did not provide an IApplicationBuilder
instance to my InitializeContainer
method, only the IServiceProvider
(which is also available via the IApplicationBuilder.ApplicationServices
property). Do you really need the whole IApplicationBuilder
in order to initialize the container?
回答3:
UPDATE
You should be able to suppress the diagnostic warning in this instance (I am unable to get the code working enough to confirm it tonight though sorry)
container.CrossWire<IHttpContextAccessor>(app);
var registration = container.GetRegistration(
typeof(IHttpContextAccessor)).Registration;
registration.SuppressDiagnosticWarning(
DiagnosticType.LifestyleMismatch, "Owned by ASP.NET");
It looks to me like your registration will always return the same instance of accessor
.
resolve an instance of
IHttpContextAccessor
:var accessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
register a delegate to always return the same instance:
container.Register(() => accessor, Lifestyle.Scoped);
I suggest you avoid complicating your registrations by trying to manage the lifetime of objects you do not own and rely on CrossWire
container.CrossWire<IHttpContextAccessor>(app);
来源:https://stackoverflow.com/questions/34657983/using-simple-injector-in-mvc6-with-cookie-auth