MVC5 (VS2012) Identity CreateIdentityAsync - 值不能为空。

42
我正在尝试为MVC5网站(在VS2012中)设置OAuth。
我正在使用Fluent NHibernate。我已经设置了自己的Userstore,并传递一个仓储对象来访问NHibernate会话对象。我将我的存储传递给默认的aspnet usermanager提供程序。这最终适用于本地注册和登录。我现在尝试连接/注册Facebook。
它成功获取帐户信息。在用户表中添加一个用户,在登录表中添加一条记录,然后出现错误。我没有在用户存储中实现claims,也没有在用户对象中放置claims collection。(不确定是否实际上需要这样做,我正在剥离所有可能引起问题的内容以查找问题的来源)。
在账户控制器中,出现错误的代码是:
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);            

在这种方法内:

private async Task SignInAsync(IdentityUser user, bool isPersistent)

这是堆栈跟踪的末尾

[ArgumentNullException: Value cannot be null.
Parameter name: value]
   System.Security.Claims.Claim..ctor(String type, String value, String valueType, String issuer, String originalIssuer, ClaimsIdentity subject, String propertyKey, String propertyValue) +14108789
   System.Security.Claims.Claim..ctor(String type, String value, String valueType) +62
   Microsoft.AspNet.Identity.<CreateAsync>d__0.MoveNext() +481
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +144
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +84
   System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() +49
   Web.Controllers.<SignInAsync>d__42.MoveNext() in d:\Google Drive\Development\GoalManagement\Web\Controllers\AccountController.cs:375
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +144
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +84
   Web.Controllers.<ExternalLoginConfirmation>d__35.MoveNext() in d:\Google Drive\Development\GoalManagement\Web\Controllers\AccountController.cs:311
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +144
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +84

public class IdentityUser : IUser
{
    public IdentityUser()
    {
        Logins = new List<IdentityUserLogin>();
    }

    public string Id { get; set; }
    public string UserName { get; set; }
    public string PasswordHash { get; set; }
    public string SecurityStamp { get; set; }
    public IList<IdentityUserLogin> Logins { get; set; }

}

public class IdentityUserLogin
{
    public string LoginProvider { get; set; }
    public string ProviderKey { get; set; }
}

如果需要,我可以包含我的用户存储代码:我没有把它放进去,因为它是一个很大的文件,可能会分散注意力。

我不确定为什么它甚至要尝试创建claim对象并为什么会出错。由于我只有VS2012,所以主要是从网上的示例中拼凑起来的。


如@Shoe所建议的那样,我从UserManager继承:

public class NHibernateAspnetUserManager<TUser> : UserManager<TUser> where TUser : IdentityUser
{
    public NHibernateAspnetUserManager(IUserStore<TUser> store) : base(store)
    {
    }        

    public override Task<ClaimsIdentity> CreateIdentityAsync(TUser user, string authenticationType)
    {
        ClaimsIdentity identity = new ClaimsIdentity();
        return Task.FromResult(identity);
    }
}

现在它不再抛出错误,但是无论我使用Facebook注册/登录多少次,它都无法实际验证我的身份。


总结一下。根据@Shoe的信息,我尝试使用以下方式重写UserManager.CreateIdentityAsync

public override Task<ClaimsIdentity> CreateIdentityAsync(TUser user, string authenticationType)
    {
        var identity = new ClaimsIdentity();
        identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
        return Task.FromResult(identity);
    }

同时尝试使用返回默认值(空列表)实现IUserClaimStore。

第一个不会抛出错误,但最终不会进行身份验证。后者仍会抛出奇怪的声明“System.Security.Claims.Claim..ctor”错误。

编辑

找出了为什么会发生构造函数错误。用户对象返回时没有ID,因此默认的感到不满。解决了这个问题并使用了默认的,它现在不再抛出错误,但仍无法登录用户。从我所看到的来看,它返回的标识对象很好。


我使用了内置的UserManager...尝试使用ReSharper反编译它,但是在另一端没有读取到任何可读内容。 - Jon
7个回答

95

我以前也遇到过同样的错误,但只有在使用Entity Framework Migration Tool创建用户时才会出现。在网站上创建用户并进行登录时,我没有出现错误。

我的错误在于我没有在迁移中提供SecurityStamp

SecurityStamp = Guid.NewGuid().ToString()

这个属性集,一切都正常工作。


在使用EF创建用户而不使用管理方法后,我需要这样做。 - Jamie Keeling
身份证不应该为您填充所需的字段吗?我正在使用 EF,我只是覆盖了关键字的类型为 int,但我也遇到了这个错误。 - Zapnologica
然而这确实起作用了,我无法感谢你的足够,我已经在这个问题上纠结了好几个小时了。 - Zapnologica
这对我有效。我一直在尝试为开发用户创建种子。 - QuantumHive
我有一个用户名并想要登录。遇到了同样的问题,这个代码帮助了我:var user = userManager.FindByNameAsync("some_username").Result; user.SecurityStamp = Guid.NewGuid().ToString(); signInManager.SignIn(user, false, false); - Edwin
显示剩余2条评论

14

我遇到了类似的问题。解决方法是设置用户实体的SecurityStamp属性。

背景:客户需要在数据库中拥有具有密码的管理员/超级用户帐户以及一堆其他用户 - 这些用户应该能够在不使用密码的情况下登录XML文件...

因此,我从Entity Framework UserStore继承,覆盖FindByIdAsync和FindByNameAsync,搜索XML文件中的用户,并返回一个新的User实体(如果默认实现未找到用户)。

当创建ClaimsIdentity时,我遇到了与Jon相同的异常。

经过一番调查,我发现我新创建的User实体没有SecurityStamp。而asp.net默认的UserManager期望一个SecurityStamp,并希望将其作为ClaimsIdentity中的一个Claim来设置。

在将值分配给该属性后 - 我使用包含前缀和用户名的字符串 - 对我来说一切都正常运行。


1
问题已解决。神奇地给安全戳赋值使其正常工作。 - Zapnologica

5

当密码长度小于6个字符时,我遇到了这个问题。如果长度大于等于6个字符,我就不会出现这个错误。

6个字符是其默认的最小设置。

当然,你可以更改这种行为。

services.Configure<IdentityOptions>(options =>
                {
                    options.Password.RequiredLength = 6;
                    options.Password.RequireUppercase = false;
                    options.Password.RequireLowercase = false;
                    options.Password.RequireDigit = false;
                    options.Password.RequireNonAlphanumeric = false;
                });

2
我和@user3347549做了相同的事情。
我花了一些时间才找到错误实际来自哪里,感谢dotPeek!
我使用自己的UserManager和UserStore实现,因为我想要Guid类型(MSSQL中的uniqueidentifier)作为键,而不是字符串(虽然它们只是Guid的占位符)
感谢这个链接,特别是HaoK(@Hao Kung在SO上)提供的答案:这里,我已经将其包含在内以备参考,以防链接失效:
“你应该使用类似新的guid工作的随机值来种子安全戳。”
我实现了自己的ClaimsIdentityFactory(从我在dotPeek中收集的信息看,它看起来完全相同),并只改变了CreateAsync方法中的一行。
public class ClaimsIdentityFactory<TUser, TKey> : IClaimsIdentityFactory<TUser, TKey>
    where TUser : class, IUser<TKey>
    where TKey : IEquatable<TKey>
{
    /// <summary>
    /// Claim type used for role claims
    /// </summary>
    public string RoleClaimType { get; set; }

    /// <summary>
    /// Claim type used for the user name
    /// </summary>
    public string UserNameClaimType { get; set; }

    /// <summary>
    /// Claim type used for the user id
    /// </summary>
    public string UserIdClaimType { get; set; }

    /// <summary>
    /// Claim type used for the user security stamp
    /// </summary>
    public string SecurityStampClaimType { get; set; }

    /// <summary>
    /// Constructor
    /// </summary>
    public ClaimsIdentityFactory()
    {
        RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
        UserIdClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
        UserNameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name";
        SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
    }

    /// <summary>
    /// Create a ClaimsIdentity from a user
    /// </summary>
    /// <param name="manager">
    /// </param>
    /// <param name="user">
    /// </param>
    /// <param name="authenticationType">
    /// </param>
    /// <returns>
    /// </returns>
    public virtual async Task<ClaimsIdentity> CreateAsync(UserManager<TUser, TKey> manager, TUser user, string authenticationType)
    {
        if (manager == null)
            throw new ArgumentNullException("manager");
        if (user == null)
            throw new ArgumentNullException("user");

        var id = new ClaimsIdentity(authenticationType, UserNameClaimType, RoleClaimType);
        id.AddClaim(new Claim(UserIdClaimType, ConvertIdToString(user.Id), "http://www.w3.org/2001/XMLSchema#string"));
        id.AddClaim(new Claim(UserNameClaimType, user.UserName, "http://www.w3.org/2001/XMLSchema#string"));
        id.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string"));
        if (manager.SupportsUserSecurityStamp)
        {
            ClaimsIdentity claimsIdentity1 = id;
            string securityStampClaimType = SecurityStampClaimType;
            ClaimsIdentity claimsIdentity2 = claimsIdentity1;
            string str = await manager.GetSecurityStampAsync(user.Id).ConfigureAwait(false);
            Claim claim = new Claim(securityStampClaimType, str ?? Guid.NewGuid().ToString());
            claimsIdentity2.AddClaim(claim);
        }
        if (manager.SupportsUserRole)
        {
            IList<string> roles = await manager.GetRolesAsync(user.Id).ConfigureAwait(false);
            foreach (string str in roles)
                id.AddClaim(new Claim(RoleClaimType, str, "http://www.w3.org/2001/XMLSchema#string"));
        }
        if (manager.SupportsUserClaim)
            id.AddClaims(await manager.GetClaimsAsync(user.Id).ConfigureAwait(false));
        return id;
    }

    /// <summary>
    /// Convert the key to a string, by default just calls .ToString()
    /// </summary>
    /// <param name="key">
    /// </param>
    /// <returns>
    /// </returns>
    protected virtual string ConvertIdToString(TKey key)
    {
        if ((object)key == null)
            throw new ArgumentNullException("key");
        else
            return key.ToString();
    }
}

我修改的那一行是

Claim claim = new Claim(securityStampClaimType, str);

为了

Claim claim = new Claim(securityStampClaimType, str ?? Guid.NewGuid().ToString());

我还没有搞清楚这意味着什么,但至少它现在可以使用,我可以继续测试我的应用程序。我猜测这个错误是因为我还没有完全实现身份验证栈的某些部分。要使用这个新的工厂,只需在UserManager构造函数中输入以下内容:

ClaimsIdentityFactory = new ClaimsIdentityFactory<TUser, Guid>();

这对我来说已经解决了。 - jao

1
默认的 UserManager 会尝试获取声明并添加/删除声明,即使您没有实现它们。如果您不需要声明,我找到的解决方案是实现自己的 UserManager 或在 UserStore 中实现“什么也不做”的方法。
public Task AddClaimAsync(TUser user, Claim claim)
{
    return Task.FromResult<int>(0);
}

public Task<IList<Claim>> GetClaimsAsync(TUser user)
{
    return Task.FromResult<IList<Claim>>(new List<Claim>());
}

public Task RemoveClaimAsync(TUser user, Claim claim)
{
    return Task.FromResult<int>(0);
}

更新了帖子并考虑了你的反馈,现在不再出现错误,但仍无法认证/显示已登录。 - Jon
@Jon 这些方法是在 UserStore 中吗?它们应该放在那里,而不是 UserManager 中。 - jamesSampica
我的用户存储中没有这些方法,我没有实现IUserClaimStore接口,我以为如果没有实现它就不会出问题。 - Jon
没有仔细阅读您的帖子,(错过了“或”)。我尝试了覆盖UserManager。 - Jon
@Jon 你可以使用默认的 UserManager 并实现自己的 UserStore。无论哪种方式,你都需要一个自定义的 UserStore - jamesSampica

0

我必须实现ClaimsIdentityFactory并设置UserManager.ClaimsIdentityFactory属性,即在AccountController类中。


0
在我的情况下,问题完全不同。这是一个 Owin 启动代码顺序的问题。
我的有缺陷的代码:
public void ConfigureAuth(IAppBuilder app)
{

   //...

   app.CreatePerOwinContext(ApplicationDbContext.Create);
   app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
   app.CreatePerOwinContext<AppSignInManager>(AppSignInManager.Create);
   app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);

   //...

}

原来,AppSignInManager 正在尝试初始化 AppUserManager,但它始终为 null,因为它尚未添加到 Owin 中。

只需简单地将它们交换一下,一切都像魔术般地运行了。

public void ConfigureAuth(IAppBuilder app)
{

   //...

   app.CreatePerOwinContext(ApplicationDbContext.Create);
   app.CreatePerOwinContext<AppSignInManager>(AppSignInManager.Create);
   app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
   app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);

   //...

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接