OWIN 存储更新、插入或删除语句影响了意外的行数 (0) 错误

OWIN Store update, insert, or delete statement affected an unexpected number of rows (0) Error

我 运行 遇到了一个非常奇怪的问题,在托管我的 Wep Api 项目的 Azure 应用服务上几个小时后刷新令牌 "disappears"。我已经为我的密码流程实施了 OAuth。我们的 AccessToken 在 1 小时后过期,我们的 RefreshToken 在一周后过期。

一些背景。这发生在我托管我的 Web Api 的 Azure 应用程序服务上,移动前端正在调用它(有多个 users/mobile 设备调用此应用程序服务) .

下面是使用 /token 调用的初始调用示例:

我的grant_type是密码。通常我会返回一个 refresh_token 字段以及 access_tokentoken_typeexpires_in.

在我推送到应用程序服务后的前几个小时工作正常,然后 refresh_token 消失了。我真的被这个问题难住了。

这是我的 CreateAsync 代码:

public async Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        var clientid = context.Ticket.Properties.Dictionary["as:client_id"];

        if (string.IsNullOrEmpty(clientid))
        {
            return;
        }

        string refreshTokenId = await CreateRefreshTokenId(clientid, context);

        if (refreshTokenId != null)
        {
            context.SetToken(refreshTokenId);
        }
        else
        {
            throw new Exception("refresh token could not be created");
        }
    }

    private async Task<string> CreateRefreshTokenId(string clientId, AuthenticationTokenCreateContext context)
    {
        var ticket = context.Ticket;
        var refreshTokenId = Guid.NewGuid().ToString("n");
        var refreshTokenLifeTime = ConfigurationManager.AppSettings["as:clientRefreshTokenLifeTime"];

        var token = new CreateRefreshTokenDTO
        {
            RefreshTokenId = refreshTokenId,
            ClientId = clientId,
            Subject = ticket.Identity.Name,
            IssuedUtc = DateTime.UtcNow,
            ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
        };

        ticket.Properties.IssuedUtc = token.IssuedUtc;
        ticket.Properties.ExpiresUtc = token.ExpiresUtc;

        token.ProtectedTicket = context.SerializeTicket();

        var result = await createRefreshTokenManager.ManagerRequest(new CreateRefreshTokenRequest
        {
            RefreshToken = token
        });

        return result.IsError ? null : refreshTokenId;
    }

我在 else 语句中添加了异常以查看它是否会抛出异常并且它确实抛出了异常,这让我相信 refreshTokenId 为空。我还将日志记录添加到日志 table 但无论出于何种原因,当抛出此错误时,它应该保存到数据库 table (我已经在本地测试并工作)但在应用程序服务器上它没有保存到 table。非常令人困惑... 更新:请参阅下面的更新,了解为什么没有保存日志

然后,这之后应该发生的是,现在前端(在本例中为移动端)具有访问和刷新令牌,当访问令牌过期时,将再次调用 /token 但 grant_type = refresh_token

更新

最终,通过反复试验并等待访问令牌过期,我能够在本地重现该问题(不完全确定)。但无论如何,我能够产生这个错误:

Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.

这个错误是我无法将任何日志保存到数据库的原因。

我使用 Castle Windsor 作为我的 IoC 和 EF6。 我的通话顺序是这样的:

1] 尝试验证上下文。在这里,我对 LoginUserManager 进行了另一个 await 调用,在那里我基本上获取并验证了用户信息

// This is used for validating the context
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)

2] CreateAsync 用于从上下文创建访问和刷新令牌

public async Task CreateAsync(AuthenticationTokenCreateContext context)

在 CreateAsync 内部,我进行了一个 await 调用 CreateOrUpdateRefreshTokenManager,如果条目存在则执行更新或创建。并最终做出SaveChanges()这个 SaveChanges() 是导致错误的原因 如果我不调用 SaveChanges(),则不会在 table 中更新或创建任何条目。这很奇怪,因为在我代码的其他部分,我在 Web 请求生命周期结束时根本不调用 SaveChanges(),但会生成 update/create/delete。我假设 EF/Windsor 为我处理储蓄。

我的想法是,因为这个流程不同于我的所有其他端点,并且它处理介于两者之间的两个异步调用,所以我正在处理 DbContext,这也许就是为什么我看到它在第二次失败(CreateAsync) 调用。不确定,只是我的想法。

无论如何,抱歉在这里啰嗦 post。我想 post 获得尽可能多的信息,并希望这也可以帮助其他面临此问题或类似问题的人。

谢谢!

更新 2

我注意到在 /token 调用中出现此错误后,任何其他 (AllowAnonymous) 调用我都可以正常工作 - 即使是那些涉及数据库的调用。但特别是 /token 调用不再有效。我解决这个问题的唯一方法是重新启动服务器。

更新 3 我只能在移动测试(链接到 Azure 服务器)上重现此问题,但无法在本地重现。我用来重现的步骤:

  1. 一个账号登录
  2. 注销
  3. 使用其他帐户登录
  4. 注销
  5. 使用我尝试过的第一个帐户登录)- 失败

好的,我已经弄清楚了这个问题,我会尽力描述这里发生的事情。

对于那些已经学习了 this 教程或​​任何其他类似教程的人,您会发现基本上已经设置了一些存储库结构以及可能是您继承的自己的上下文基本上下文,对吗?

在我的例子中,我通过覆盖 ApiController class 中的 Dispose(bool disposing) 方法来处理请求末尾的上下文 Dispose。如果您正在构建 WebApi,您就会知道我在说什么,因为您编写的任何控制器都继承了它。如果您设置了一些 IoC,并将生命周期设置为请求的末尾,它会在那里处理 :)

但是,在 /token 调用的这种情况下,我注意到我们从未点击过任何控制器......或者至少 none 使用了 ApiController 所以我什至无法点击 Dispose方法。这意味着上下文保持 "active"。在我对 /token 端点的第二次、第三次、第四次等调用中,我在观察者中注意到上下文调用正在建立(这意味着它正在将之前的编辑保存到我从之前的 /token 中所做的上下文中电话 - 他们永远不会被处理掉!因此使上下文陈旧)。

不幸的是,对于我的情况,解决这个问题的方法是将 /token 请求中进行的任何上下文调用包装在 using 语句中,这样我在 using 完成后就知道了它会妥善处理。这似乎有效:)

因此,如果您有一个与我分享的类似项目或与我分享的那个教程类似的项目,您可以 运行 解决这个问题。