在 ASP.NET Core Authorize-Attribute 中使用带有 DbContext 的存储库:“无法访问已处置的对象”

Posted

技术标签:

【中文标题】在 ASP.NET Core Authorize-Attribute 中使用带有 DbContext 的存储库:“无法访问已处置的对象”【英文标题】:Use repository with DbContext in ASP.NET Core Authorize-Attribute: "Cannot access a disposed object" 【发布时间】:2019-05-20 03:18:06 【问题描述】:

对于第三方身份验证,我需要一个自定义 Authorize 属性。这里需要一个存储库(SessionManager)类来检查用户是否登录。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class VBAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter 
    public async void OnAuthorization(AuthorizationFilterContext context) 
        var sessionManager = (VBSessionManager)context.HttpContext.RequestServices.GetService(typeof(VBSessionManager));
        var user = await sessionManager.GetCurrentSessionAsync();
        if (user == null) 
            context.Result = new UnauthorizedResult();
            return;
        
    

sessionManager.GetCurrentSessionAsync()之类的会出现以下异常:

无法访问已处置的对象。此错误的常见原因是 处理从依赖注入解决的上下文和 然后稍后尝试在您的其他地方使用相同的上下文实例 应用。如果您在 上下文,或将上下文包装在 using 语句中。如果你是 使用依赖注入,你应该让依赖注入 容器负责处理上下文实例。对象名称: 'AsyncDisposer'。

我知道这一点,不要自行处置。 VBSessionManager 将我的 DbContext 注入到它的构造函数中。内部GetCurrentSessionAsync cookie 使用 LinQ 数据库查询进行检查。所以不要调用Disposeusing 指令或类似的东西。

注入VBSessionManager

public class VBSessionManager 
    readonly VBDbContext db;
    readonly IHttpContextAccessor contextAccessor;
    const string sessionHashCookieName = "xxx";
    VBSession currentSession;

    public VBSessionManager(VBDbContext db, IHttpContextAccessor contextAccessor) 
        this.db = db;
        this.contextAccessor = contextAccessor;
    

    public async Task<VBSession> GetCurrentSessionAsync() 
        if (currentSession == null) 
            string sessionCookie = GetCookieWithoutPrefix(sessionHashCookieName);
            currentSession = await GetSessionAsync(sessionCookie);

            if (currentSession == null) 
                var cookieUser = GetUserFromCookiePassword().Result;
                // No session detected
                if (cookieUser == null) 
                    return null;
                
                currentSession = db.Sessions.FirstOrDefault(s => s.UserId == cookieUser.Id);
            
        
        return currentSession;
    
    // ...

服务注入

        services.AddDbContext<VBDbContext>(options => 
            string connectionString = Configuration.GetValue<string>("VBConnectionString");
            options.Usemysql(connectionString,
                    mySqlOptions => 
                        mySqlOptions.ServerVersion(new Version(10, 2, 19), ServerType.MariaDb);
                    
            );
            bool isDev = CurrentEnvironment.IsDevelopment();
            options.EnableSensitiveDataLogging(isDev);
        );

        services.AddScoped<VBSessionManager>();

【问题讨论】:

尝试从Scoped 切换到SingletonTransient。对于DI,我参考了这个(docs.microsoft.com/en-us/aspnet/core/fundamentals/…) 我在问题中编辑了我的 DbContext 的注入。 @RyanWilson 尝试过Transient 同样的问题。单例似乎不是一个好的做法,因为 DbContext 类旨在缩短活动时间(参见例如 codereview.stackexchange.com/a/15818/169176) 你能显示你的代码GetCurrentSessionAsync()吗? dbcontext 可能已经被释放了。 @wannadream 方法在我的问题中添加。 【参考方案1】:

似乎async 的使用会导致问题。当我将OnAuthorization 更改为这样的同步方法时,我没有收到任何错误:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
    public class VBAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter 
        public void OnAuthorization(AuthorizationFilterContext context) 
            var sessionManager = (VBSessionManager)context.HttpContext.RequestServices.GetService(typeof(VBSessionManager));
            var user = sessionManager.GetCurrentSessionAsync().Result;
            if (user == null) 
                context.Result = new UnauthorizedResult();
                return;
            
        
    

不知道这些属性(或者可能只有AuthorizeAttribute)是否不是为异步工作而设计的。对我来说,当前的解决方法是使用 syn 方法。我也认为这不应该降低性能。但是,如果有人知道背景,甚至知道我们如何使用属性 async,我会很高兴另一个答案。

【讨论】:

仅供参考:docs.microsoft.com/en-us/dotnet/csharp/language-reference/…。 await 表达式不会阻塞它正在执行的线程。相反,它会导致编译器注册 async 方法的其余部分作为等待任务的延续。然后控制权返回给异步方法的调用者。当任务完成时,它调用它的继续,并且异步方法的执行从它停止的地方继续。因此,当此处调用延续时,dbcontext 可能已经被释放。【参考方案2】:
public async void OnAuthorization(AuthorizationFilterContext context) 

这里重要的是使用async void,即according to David Fowler,总是不好。通过您在此处的设置,对OnAuthorization 的调用本身不能是awaited,这意味着正在发生以下情况:

    VBSessionManagerVBDbContext 的作用域实例在调用您的 OnAuthorization 方法之前的一段时间内被创建。 您的OnAuthorization 执行并调用VBSessionManager.GetCurrentSessionAsync,在所述方法有机会完成之前返回(由于使用async/await)。 OnAuthorization 完成后,IDisposable-实现VBDbContext 被释放。 VBSessionManager.GetCurrentSessionAsync 中的代码仍在运行 - 它尝试使用已被处理掉的 VBDbContext 实例。

在您的情况下使用async void 的原因仅仅是因为这是在IAuthorizationFilter 接口中声明的内容-您想使用await,唯一的方法是将您的实现方法标记为@ 987654344@(你不能让它async Task,因为那不会实现接口)。

就解决方案而言,我同意 Gabriel Luci 的观点,即使用 policy-based authorisation 将是可行的方法。

【讨论】:

请注意,这里还有一个IAsyncAuthorizationFilter,应该使用它来允许过滤器正确异步。【参考方案3】:
public class VBAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter

    public async void OnAuthorization(AuthorizationFilterContext context)
    
        // …

        await something;

        // …
    

有一个方法async void 是almost always a bad idea。异步方法应该返回一个Task 以使调用者能够确定异步过程的结果。

由于您正在实施IAuthorizationFilter,因此您正在实施同步授权过滤器。当您不需要异步执行某些操作时,您可以使用它。例如,如果您只需要查看一些参数,然后通过一些规则来确定是否允许访问,则这是正确的。

如果您需要异步进程,您应该使 void 方法异步,而是实现 IAsyncAuthorizationFilter。这是实现异步授权过滤器的接口。在这种情况下,您需要实现的方法看起来有点不同:

Task OnAuthorizationAsync(AuthorizationFilterContext context)

如您所见,此方法返回Task,因此它可以正确执行异步进程。在您的情况下,您想在方法内部 await 某些东西,您可以这样做:

public class VBAuthorizeAttribute : AuthorizeAttribute, IAsyncAuthorizationFilter

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    
        // …

        await something;

        // …
    

现在,使用返回 Task 的适当异步方法,调用系统将能够正确使用该方法,并且请求处理的继续将等待您的授权过滤器被处理。

【讨论】:

【参考方案4】:

OnAuthorization 方法不应该用于验证授权。这只是“嘿,正在授权”的通知。

也就是说,some have used it for this。但是由于您将其声明为async void,因此没有什么可以等待此方法完成。这就是您的异常的根源:在进行数据库调用时,请求已经完成并且上下文已被释放。您可以删除async....

但正确的解决方案是使用IAuthorizationHandler,顾名思义,它是为处理授权而设计的。它有一个HandleAsync 方法,这是一个正确的async 方法,实际上是在等待(它在继续之前等待您做出授权决定)。

查看来自 Microsoft 员工的 this answer。您设置处理程序,然后将其与常规 AuthorizeAttribute 一起使用,如下所示:

[Authorize(Policy = "MyCustomPolicy")]

【讨论】:

以上是关于在 ASP.NET Core Authorize-Attribute 中使用带有 DbContext 的存储库:“无法访问已处置的对象”的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core JWT 身份验证以保护 webAPI [Authorize] 属性错误 401 Unauthorized

ASP.NET Core 身份 [Authorize(Roles ="ADMIN")] 不起作用

拦截asp.net core Authorize action,授权成功后执行自定义动作

ASP.NET Core Authorize 属性不适用于 JWT

ASP.NET Core 2:使用 [Authorize] 对 API 的 Ajax 调用使预检请求失败

JS Fetch API 不适用于具有 Authorize 属性的 ASP.NET Core 2 控制器