在 Web Forms、MVC、Signalr 中一致访问 IOwinContext

Consistent access to IOwinContext in Web Forms, MVC, Signalr

我正在开发一个 Web 应用程序,我希望能够通过我的 IUserContext 界面访问当前登录的用户。看起来是这样的:

public interface IUserContext
{
    int UserId { get; }
    string UserName { get; }
}

这个接口可以在我的命令处理程序和查询处理程序中使用。

Web 应用程序使用 ASP.NET 身份库版本 2.2.1。所以,我想使用 IOwinContext 来实现接口。我有三层需要访问 owin 上下文:

对于 Web 表单页面和 MVC 操作,我可以编写自定义中间件来填充 IUserContext 属性 (using code from this doc):

public interface IOwinContextAccessor
{
    IOwinContext CurrentContext { get; }
}

public class CallContextOwinContextAccessor : IOwinContextAccessor
{
    public static AsyncLocal<IOwinContext> OwinContext = new AsyncLocal<IOwinContext>();
    public IOwinContext CurrentContext => OwinContext.Value;
}

public class OwinUserContext : IUserContext
{
    public int UserId => CallContextOwinContextAccessor.OwinContext.Value.Authentication.User.Identity.GetUserId<int>();
    public string UserName => CallContextOwinContextAccessor.OwinContext.Value.Authentication.User.Identity.Name;
}

    // inside my Startup.cs Configuration method
    app.Use(async (context, next) => {
        CallContextOwinContextAccessor.OwinContext.Value = context;
        await next();
    });

对于信号器集线器,我可以创建 HubPipelineModule,它将填充 CallContextOwinContextAccessor.OwinContext:

public class OwinContextPipelineModule : HubPipelineModule
{
    protected override bool OnBeforeIncoming(IHubIncomingInvokerContext context)
    {
        var owinContext = context.Hub.Context.Request.GetHttpContext().GetOwinContext();
        CallContextOwinContextAccessor.OwinContext.Value = owinContext;
        return base.OnBeforeIncoming(context);
    }
}

问题:

  1. 这是在 ASP.NET 中访问登录用户的正确方法吗 身份?
  2. 在中间件和集线器管道模块中重用 CallContextOwinContextAccessor 安全吗?我读过 documentation 关于 AsyncLocal 类型的内容,似乎变量的值在调用堆栈的方法调用中持续存在。我担心当一个用户调用 MVC 操作而另一个用户同时调用 signalr hub 方法时可能出现的竞争条件,反之亦然。

我有另一种方法可以将您的实施问题与特定的 OWIN 问题分离,例如 IOwinContext

public class UserContext : IUserContext {
    private Lazy<IPrincipal> user;

    public UserContext(Func<IPrincipal> valueFactory) {
        this.user = new Lazy<IPrincipal>(valueFactory);
    }

    public int UserId {
        get { return user.Value.Identity.GetUserId<int>(); }
    }

    public string UserName {
        get { return user.Value.Identity.Name; }
    }
}

然后您可以从那里配置组合根目录中的所有内容。根据链接的文档,假设您正在使用 Simple Injector for DI。

void Configure(IAppBuilder app) {
    IPrincipal currentUser = null;
    Func<IPrincipal> activator = () => currentUser;

    // Create the container as usual.
    var container = new Container();
    //Only one instance will be created by the container per web request 
    //and the instance will be disposed when the web request ends.
    container.Options.DefaultScopedLifestyle = new WebRequestLifestyle();

    // Register your types, for instance:
    container.Register<IUserContext>(() => new UserContext(activator),
        Lifestyle.Scoped);

    //...Configure respective DI as needed

    //Set MVC DI if needed
    System.Web.Mvc.DependencyResolver
        .SetResolver(new SimpleInjectorDependencyResolver(container));
    //Set Web API DI if needed
    System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver =
        new SimpleInjectorWebApiDependencyResolver(container);

    //...use other middleware 

    //...custom middleware to grab user (placed especially after Identity config)
    app.Use(async (context, next) => {
        //get the current user from request pipeline
        currentUser = context.Authentication.User;
        await next();
    });

    app.MapSignalR();
}

如上面的评论所示,主要思想是您配置 IoC 容器来管理各个框架的已解析依赖项的生命周期和范围。所需的信息从请求中延迟加载并传递给注入的上下文,消除了传递 IOwinContext 的需要,保持用户上下文实现干净和解耦。

通过将容器配置为使用 WebRequestLifestyle,您可以确保

Only one instance will be created by the container per web request and the instance will be disposed when the web request ends.

这将解决您对以下问题的担忧:关于在请求期间访问用户的竞争条件

然而,对于 SignalR,它看起来需要稍作更改,因为它的请求不遵循相同的流程

public class ChatHub : Hub {
    private IUserContext user;

    public override Task OnConnected() {
        user = GetAuthInfo();
        return Clients.All.joined(user);;
    }

    protected IUserContext GetAuthInfo() {
        var user = Context.User;
        return new UserContext(user);
    }

    public void Send(string name, string message){
        var userId = user.UserId;
        //...        
    }
}

这让我想到可以创建一个工厂来抽象出与实现问题的紧密耦合。

public interface IUserContextFactory {
    IUserContext Create(IPrincipal user);
}

public class UserContextFactory {
    public IUserContext Create(IPrincipal user) {
         return new UserContext(user);
    }
}

并在启动时配置了自己的容器以避免与网络请求范围发生冲突

public void ConfigureSignalR(IAppBuilder app) {
    // Create the container as usual.
    var container = new Container();
    // Register your types, for instance:
    container.Register<IUserContextFactory,UserContextFactory>();    
    //Set SignalR DI
    var config = new HubConfiguration() {
        Resolver = new SignalRSimpleInjectorDependencyResolver(container);
    };
    app.MapSignalR(config);
}

这将最终允许以下

public class ChatHub : Hub {
    private IUserContextFactory userFactory;

    public ChatHub(IUserContextFactory userFactory) {
        this.userFactory = userFactory;
    }

    public void Send(string name, string message){
        var user = userFactory.Create(Context.User);
        var userId = user.UserId;
        //...        
    }
}

参考文献:

Simple Injector Documentation : ASP.NET MVC Integration Guide

Simple Injector Documentation : ASP.NET Web API Integration Guide

Microsoft Documentation: Using IoC Containers in SignalR

Microsoft Documentation: Authentication and Authorization for SignalR Hub

解法:

事实证明,使用命令对象并使用适当的 mvc/signalr User 属性初始化当前用户 属性 会容易得多。

例如:

public class MyCommand
{
    public string Payload { get; set; }
    public IPrincipal CurrentUser { get; set; } // or int UserId, or string UserName
}

然后初始化命令对象:

// mvc action
new MyCommand
{
    Payload = "foo"
    CurrentUser = User // User is a Controller.User property
};

// signalr hub
new MyCommand
{
    Payload = "bar"
    CurrentUser = Context.User
};

这样一来,我就根本不需要IUserContext了。 感谢@Nkosi 解决了这个问题。