首页 > 解决方案 > 有没有比通过 userManager.CreateAsync() 更快地批量创建身份用户(1M 行)的方法?

问题描述

我正在使用 ASP.NET Core 2.2 MVC 开发一个新站点,该站点正在替换经典 ASP 现有站点。他们有近 100 万用户需要转移到使用 Entity Framework 和 Identity Core 的新站点。我目前正在创建所有现有用户的列表,然后在 foreach 中调用 userManager.CreateAsync()。然后,我使用 userManager.AddToRoleAsync() 将该用户添加到角色(如果需要,我可以稍后在 SQL 中执行此操作)。每次通过 foreach 运行大约需要 3000-4000 毫秒,在每个异步调用之间平均分配,所以让它在一夜之间运行让我导入了 35K,这是不切实际的。

更新:好/坏消息是密码当前存储为纯文本,因此我可以将它们带入并对其进行哈希处理,准备好放入 AspNetUsers。显然,这是一种非常糟糕的密码存储方式,但会使当前的任务变得更简单!

我已经研究过编写一个存储过程来从代码中调用以将用户名、电子邮件和散列密码添加到 AspNetUsers 表中,但是该表需要所有字段,我不确定如何生成诸如 id 之类的项目(虽然我认为它只是一个 GUID,因此可以通过 NEWID()、SecurityStamp 和 ConcurrencyStamp 创建。

我通过创建所有现有身份用户的列表并将其添加到 Linq 查询中删除了重复项,因此只处理未处理的用户。此列表构建确实需要时间,但只需几分钟,所以这不是一个真正的问题。

我搜索了其他甚至稍微相关的帖子,但没有一个能完全回答这个具体问题。从那我至少发现了如何散列密码,这样如果我可以创建一种直接更新 AspNetUsers 表的新方法,而不是如何生成其他必要的字段 Id、SecurityStamp 和 ConcurrencyStamp。

即使我可以找出如何生成 SecurityStamp 和 ConcurrencyStamp,我也可以将数据传递到 Entity Framework 中的存储过程以添加到 AspNetUsers 表中。(我们在实体框架中使用存储过程——我知道这不是“完成的事情”,但现有的代码库是我无法解开的存储过程的挂毯!)

public async Task<ViewResult> LoadExisting()
    {
        List<ImportModel> existing = new List<ImportModel>();

        var users = userManager.Users.ToList();

        existing = _context.Individual
                    .Where(i => (i.AccessId == 20 && !users.Any(s => s.Email == i.Email) ))
                    .Select(i => new ImportModel { Email = i.Email, Password = i.Password, existingId = i.existingId })
                    .ToList();

        List<string> successes = new List<string>();
        List<string> duplicates = new List<string>();
        List<string> failures = new List<string>();

        IdentityResult result = new IdentityResult();
        IdentityResult roleresult = new IdentityResult();

        foreach (ImportModel indiv in existing)
        {
            var hasher = userManager.PasswordHasher;
            AppUser user = new AppUser
            {
                UserName = indiv.Email,
                Email = indiv.Email,
                Individual_id = indiv.IndividualId
            };
            //indiv.Password = hasher.HashPassword(user, indiv.Password); // this works

            // Do something faster here

            result = await userManager.CreateAsync(user, indiv.Password);

            if (result.Succeeded)
            {
                // Add to relevant role, set above
                roleresult = await userManager.AddToRoleAsync(user, "ARole");
                successes.Add(indiv.Email + ",");
            }

            else
            {
                foreach (var error in result.Errors)
                {
                    if (error.Code == "DuplicateUserName")
                    {
                        duplicates.Add(indiv.Email + ",");
                    }
                    else
                    {
                        failures.Add(indiv.Email + ", <strong>" + error.Code + "</strong>");
                    }
                }                    
            }
        }
        ViewData["LoadExistingSuccesses"] = successes;
        ViewData["LoadExistingDuplicates"] = duplicates;
        ViewData["LoadExistingFailures"] = failures;

        return View();
    }

标签: asp.net-identityasp.net-core-2.2

解决方案


是和不是。UserManager<TUser>.CreateAsync主要用于三个目的:

  1. 它验证用户名和密码(如果提供)以确保它们符合您的身份配置中指定的要求。

  2. 它将用户名和电子邮件地址规范化在单独的列 ( NormalizedUserName/ NormalizedEmail) 中,以便可以以标准化方式查找它们,同时仍保持索引。

  3. 如果提供了密码,则密码将被加盐和散列。

前两个很容易在批量插入中复制。用户名要求非常简单,主要是确保它是 URL 安全的。至于标准化,据我所知,UserName/Email值只是全部大写。

但是,复制密码散列几乎是不可能的。但是,这也不是完全必要的。您可以选择简单地为每个用户强制重置密码,这不是一个坏主意,无论如何,当从一个身份验证系统迁移到另一个身份验证系统时,这几乎是不可避免的。除非您已经很糟糕并且存储了纯文本密码,否则无法知道用户的密码是什么以便迁移它们。

老实说,我什至不会费心尝试通过代码来做到这一点。这将是最慢和最困难的方法。只需使用 SSIS 包之类的东西,然后通过 SQL 将数据从字面上移动(根据需要进行修改)。


推荐阅读