上下文
我正在实现一个单点登录服务器,其中用户,角色,声明等将保留在Couchbase中 . 到目前为止,我的步骤是:
-
在MVC Web应用程序中实现IdentityServer3(使用v2.5.4 NuGet package) .
-
实施IdentityServer3.AspNetIdentity以将Identity用作用户和其他实体的后备存储(使用v2.0.0 NuGet package) .
-
为简单的后端用户管理UI实现IdentityManager(1.0.0-beta5-5 NuGet package)和IdentityManager.AspNetIdentity(1.0.0-beta5-1 NuGet package) .
-
从Couchbase Labs实施couchbase-aspnet-identity Identity自定义存储提供程序 . 这是作为source (from GitHub)包含在我的项目中,因为它只是作为一个不完整的developer preview发布到NuGet,后来被撤回 .
当实施步骤1-3时,一切都很完美 . 用户通过EF存储在LocalDB实例(IdentityServer3.AspNetIdentity的默认实现)中的常用 AspNetUsers
表中,我可以使用IdentityManager UI添加,编辑和删除用户 .
第4步涉及implementing custom store classes,这些基本上直接来自GitHub上的couchbase-aspnet-identity项目,并进行了一些小的本地更改(例如实现 UserStore.Users
getter,其中throws NotImplementedException in the original code) .
问题
现在,当我创建一个用户时,它按预期存储在Couchbase中 . 如果我编辑添加到自定义 ApplicationUser
类的自定义字段(例如 FirstName
或 Age
),则更改将保持正确 . 到现在为止还挺好 .
但是,如果我编辑用户的密码,电子邮件地址或电话号码(在IdentityManager UI中),则会发生以下情况:
-
IdentityManager UI显示成功消息,说明更改成功(参见下文)
-
但是,编辑的字段不反映更改(也见下文)
-
Couchbase中没有保留更改,因此很明显当重新加载用户时,更改未显示
单步执行代码,我发现对我的 UserStore.UpdateAsync
方法进行了多次调用:
public async Task UpdateAsync(T user)
{
await _bucket.UpdateAsync(user.Id, user);
}
在每次调用时,用户都被正确地保存到Couchbase . 所以实际上问题不在于变化根本不存在,而是它们被持久化然后被原始值覆盖 .
例如,如果我更改用户的电话号码,则会调用该方法三次 . 将它从空更改为123,我的 user
对象中的相关字段如下:
Call 1
-
PhoneNumber
: 123 (在UI中输入的新值) -
PhoneNumberConfirmed
:假
Call 2
-
PhoneNumber
:123 -
PhoneNumberConfirmed
: true
Call 3
-
PhoneNumber
:null(原始值) -
PhoneNumberConfirmed
:false(原值)
(该对象每次都有不同的 SecurityStamp
值,否则对象值相同 . )
当我更改电子邮件地址或密码时,会发生同样的事情 . (对于密码,只有两个调用,但在每种情况下,最后一个调用会将所有字段重置为其原始值 . )
这个问题
What is causing the extra call to UserStore.UpdateAsync() and how do I fix it?
不幸的是我无法在调用堆栈中向上移动:我认为 UserStore
是从 UserManager
调用的,它是 Microsoft.AspNet.Identity.Core
的一部分,我没有源代码 .
额外细节
我的UserStore类实现了所讨论的所有可选接口here,即它看起来像这样:
public class UserStore<T> :
IUserLoginStore<T>,
IUserClaimStore<T>,
IUserRoleStore<T>,
IUserSecurityStampStore<T>,
IQueryableUserStore<T>,
IUserPasswordStore<T>,
IUserPhoneNumberStore<T>,
IUserStore<T>,
IUserLockoutStore<T, string>,
IUserTwoFactorStore<T, string>,
IUserEmailStore<T>
where T : IdentityUser
{
// ...
}
这是我刚刚将电话号码更改为123时IdentityManager UI的屏幕截图 . (注意成功消息,但空电话字段 . )
2 回答
我设法深究这一点 . 事实上,它与默认的EF实现(我的问题中的第3步)一起工作但是当我将它换成Couchbase存储提供程序时失败意味着我最初怀疑Couchbase提供程序有问题(特别是因为它基于开发人员预览代码) .
实际上问题出在IdentityManager.AspNetIdentity包中,特别是方法AspNetIdentityManagerService.SetUserPropertyAsync():
如果你进入
SetUserProperty()
电话足够远,你会看到AspNetIdentityManagerService.SetPhone(),如下所示:因为我们只将用户ID传递给
SetPhoneNumber()
,所以UserManager
要求UserStore
用户的另一个实例(通过调用UserStore.FindByIdAsync()
),因此PhoneNumber
属性永远不会在传递给SetUserProperty()
的实例上更新 . 因此,当我们在SetUserPropertyAsync()
中调用userManager.UpdateAsync(user)
时,我们将传入一个陈旧的对象而不应用更改 .据推测,EF确保了两个实例这里使用的是同一个,但是其他提供商将Couchbase提供商与其他更成熟的实现(例如AspNet.Identity.Mongo)进行了比较,并且Couchbase代码看起来很合理 .
我遇到了同样的问题,这个线程让我理解为什么IdentityManager会多次调用此调用 . 我也把我的DAL交换到非sql商店 .
我的解决方案有些类似 . 我在更新后将用户模型存储在临时缓存中,另外,我使用Guid类型的实例级变量作为缓存键的transactionId . 我还将UserManager生命周期更改为InstancePerHttpRequest,以确保对Update方法进行的两个并发调用(在本例中)来自同一个http请求,因此将共享相同的transactionId . 我没有遇到随后的http请求和使用缓存的任何竞争条件 .