在 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 上下文:
- ASP.NET Web 表单页面
- ASP.NET MVC 5 个动作
- 信号中心
对于 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);
}
}
问题:
- 这是在 ASP.NET 中访问登录用户的正确方法吗
身份?
- 在中间件和集线器管道模块中重用
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 解决了这个问题。
我正在开发一个 Web 应用程序,我希望能够通过我的 IUserContext 界面访问当前登录的用户。看起来是这样的:
public interface IUserContext
{
int UserId { get; }
string UserName { get; }
}
这个接口可以在我的命令处理程序和查询处理程序中使用。
Web 应用程序使用 ASP.NET 身份库版本 2.2.1。所以,我想使用 IOwinContext 来实现接口。我有三层需要访问 owin 上下文:
- ASP.NET Web 表单页面
- ASP.NET MVC 5 个动作
- 信号中心
对于 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);
}
}
问题:
- 这是在 ASP.NET 中访问登录用户的正确方法吗 身份?
- 在中间件和集线器管道模块中重用
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 解决了这个问题。