You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core下IdentityServer4重复创建AzureAD用户问题及方案咨询

解决IdentityServer4中重复登录请求导致重复创建用户的问题

你的问题核心是并发请求下的竞态条件:当同一个用户的两次登录请求同时到达时,它们都在AutoProvisionUser中检测到用户不存在,进而同时执行CreateUserAsync,最终创建重复账号。下面针对你的疑问和场景给出可行的解决方案:

一、你的核心思路是否可行?

你提到的“让重复请求等待正在执行的CreateUserAsync任务完成后再判断用户是否存在”是完全可行的,而且是解决这类并发问题的常用思路。不需要复杂的任务队列,通过跟踪正在进行的用户创建任务就能实现。

二、具体实现方案

1. 用并发字典跟踪正在进行的创建任务

在UserStore类中添加一个ConcurrentDictionary,用来缓存当前正在执行的用户创建任务,以用户的UPN(或唯一标识)作为Key,对应的创建任务作为Value。这样当第二个请求进来时,它会发现已有一个正在进行的创建任务,直接等待该任务完成,再去查询用户是否存在。

修改UserStore的代码:

private readonly ConcurrentDictionary<string, Task<UserIdentity>> _pendingUserCreations = new ConcurrentDictionary<string, Task<UserIdentity>>();

public async Task<User> AutoProvisionUser(string provider, string userId, ClaimsPrincipal principal, string accessToken, Globals globals)
{
    var upn = principal.FindFirstValue(ClaimConstants.UPN);
    // 先尝试获取或创建用户创建任务
    var creationTask = _pendingUserCreations.GetOrAdd(upn, async key =>
    {
        try
        {
            // 这里执行实际的创建逻辑
            return await CreateUserAsync(principal, userId, accessToken, globals.AzureApplication);
        }
        finally
        {
            // 任务完成后从字典中移除,避免内存泄漏
            _pendingUserCreations.TryRemove(key, out _);
        }
    });

    // 先检查用户是否已存在
    var user = await GetFullUserByUsernameAsync(upn);
    if (user.Account == null)
    {
        // 等待创建任务完成(不管是当前请求发起的还是其他请求发起的)
        await creationTask;
        // 任务完成后再次查询用户
        user = await GetFullUserByUsernameAsync(upn);
    }
    else
    {
        await UpdateGroupsAsync(user.Account, accessToken, globals.AzureApplication);
    }

    return new User
    {
        Claims = principal.Claims.ToList(),
        Name = user.Profile.DisplayName,
        Provider = provider,
        SubjectId = user.Account.AzureId,
        Username = user.Account.UserName
    };
}

2. 数据库层面的防护(必不可少)

即使有了上面的内存级别的锁,也要在数据库层面添加唯一约束(比如给UserName或AzureId字段加唯一索引)。这样如果因为极端情况(比如多实例部署时内存锁不生效)导致重复创建请求到达数据库,数据库会抛出唯一约束冲突的异常,你可以在CreateUserAsync中捕获该异常,然后重新查询用户信息即可:

public async Task<UserIdentity> CreateUserAsync(ClaimsPrincipal principal, string userId, string graphAccessToken, AzureApplication options)
{
    try
    {
        // 调用Microsoft API、将用户添加至数据库并返回用户信息...
    }
    catch (DbUpdateException ex) when (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2601) // SQL Server的唯一约束冲突错误码
    {
        // 捕获到重复创建的异常,直接查询已存在的用户
        var upn = principal.FindFirstValue(ClaimConstants.UPN);
        var existingUser = await GetFullUserByUsernameAsync(upn);
        return existingUser.Account;
    }
}

三、对你提到的其他方案的评估

  • 任务队列:这个方案可行,但相对过重。任务队列适合批量处理异步任务,而你的场景是实时登录请求,需要即时响应,用任务等待的方式更高效。
  • 自定义DelegateHandler:过滤重复请求需要识别请求的唯一性(比如同一个用户的登录请求),但登录请求的参数可能会有变化(比如state参数),很难准确判断重复。而且在中间件层处理业务逻辑的竞态条件,不如在业务逻辑层直接处理清晰。

四、多实例部署的注意事项

如果你的系统是多实例部署,上面的内存级别的ConcurrentDictionary只在单个实例内生效,此时需要用分布式锁(比如基于Redis的分布式锁)来替代内存锁。比如在创建用户前先获取分布式锁,锁的Key是用户的UPN,获取到锁的请求执行创建逻辑,其他请求等待锁释放后再查询用户。

内容的提问来源于stack exchange,提问作者HenrikM

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:20:40