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

.NET Core自定义身份服务多用户登录串号问题解决方案

问题根因

多用户并发时身份串号是两个核心错误写法共同导致的:

  • 服务生命周期注册错误:你将IUserIdentity注册为*Singleton(单例)*生命周期,整个应用全局只会创建一个UserIdentity实例,所有用户的所有请求都会共用这同一个实例。
  • 状态存储逻辑错误:UserIdentity类中用来存储登录用户信息的private Users user;、private string userPosition;是实例级私有字段,单例模式下这两个字段属于全局共享的内存变量。并发场景下用户A刚把自己的身份信息写入字段,用户B的请求就会直接覆盖字段值,此时用户A的后续请求读到的就是用户B的信息,自然会出现跨用户身份混淆。
  • 额外隐患:SignInController中定义的private Users authorized_user;也是控制器实例字段,一旦控制器开启实例复用,同样会触发串号问题。
修复方案

不需要新增额外数据表,也不需要依赖.NET Core Identity框架,只需要调整服务生命周期、将身份信息存储到请求隔离的上下文即可彻底解决问题。

第一步:修正服务注册生命周期

将IUserIdentity的注册生命周期从单例改为Scoped(作用域),该生命周期下每个HTTP请求会创建独立的UserIdentity实例,请求结束后实例自动回收,从实例层面隔离不同请求的状态:

// 替换原有的Singleton注册
// services.AddSingleton<IUserIdentity, UserIdentity>();
services.AddScoped<IUserIdentity, UserIdentity>();
// 注册HttpContext访问器,用于读取当前请求的Cookie和上下文
services.AddHttpContextAccessor();

注意:仅修改生命周期只能保证单次请求内的数据隔离,由于HTTP是无状态协议,跨请求的登录状态不会自动保留,还需要结合认证Cookie实现跨请求身份识别。

第二步:改造身份认证与读取逻辑

移除UserIdentity中全局共享的user、userPosition字段,认证通过后给客户端下发HttpOnly加密Cookie存储用户唯一标识(用户Id),后续每次请求先从当前请求携带的Cookie中解析用户Id,再加载对应用户信息,完全实现用户级隔离:

  1. 改造UserIdentity的构造函数,注入IHttpContextAccessor用于访问当前请求上下文:
    private readonly IUnitOfWorkFactory _unitOfWorkFactory;
    private readonly IHttpContextAccessor _httpContextAccessor;
    // 替换原有构造函数
    public UserIdentity(IUnitOfWorkFactory unitOfWorkFactory, IHttpContextAccessor httpContextAccessor)
    {
        _unitOfWorkFactory = unitOfWorkFactory ?? throw new ArgumentNullException(nameof(unitOfWorkFactory));
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }
    
  2. 改造Authenticate方法,认证通过后写入认证Cookie,不再给共享字段赋值:
    // 服务器端固定签名密钥,不要泄露,可存在配置文件中
    private const string SIGN_KEY = "your_own_secret_key_123456";
    public void Authenticate(string Login, string PasswordHash)
    {
        using (var unitOfWork = _unitOfWorkFactory.MakeUnitOfWork())
        {
            var loginUser = unitOfWork.Users.GetUser(Login, PasswordHash);
            if (loginUser != null)
            {
                var httpContext = _httpContextAccessor.HttpContext;
                // 生成签名防止用户篡改Cookie中的UserId
                var sign = Convert.ToBase64String(
                    SHA256.HashData(Encoding.UTF8.GetBytes($"{loginUser.Id}{SIGN_KEY}"))
                );
                var cookieOptions = new CookieOptions
                {
                    HttpOnly = true, // 禁止前端JS读取,防范XSS攻击
                    Secure = true, // HTTPS部署时开启
                    Expires = DateTimeOffset.Now.AddHours(8), // 登录态有效期8小时
                    SameSite = SameSiteMode.Lax // 防范CSRF攻击
                };
                // 写入用户Id和签名到Cookie
                httpContext.Response.Cookies.Append("Uid", loginUser.Id.ToString(), cookieOptions);
                httpContext.Response.Cookies.Append("UidSign", sign, cookieOptions);
            }
        }
    }
    
  3. 新增内部方法,从当前请求上下文加载当前用户信息,避免单次请求重复查库:
    private Users GetCurrentUser()
    {
        var httpContext = _httpContextAccessor.HttpContext;
        // 先从当前请求的Items缓存取用户,单次请求内只查一次数据库
        if (httpContext.Items.TryGetValue("CurrentUser", out var cached) && cached is Users cachedUser)
        {
            return cachedUser;
        }
        // 从Cookie读取用户Id和签名
        if (!httpContext.Request.Cookies.TryGetValue("Uid", out var uidStr) 
            || !int.TryParse(uidStr, out var userId)
            || !httpContext.Request.Cookies.TryGetValue("UidSign", out var sign))
        {
            return null;
        }
        // 校验签名,防止Cookie被篡改
        var expectedSign = Convert.ToBase64String(
            SHA256.HashData(Encoding.UTF8.GetBytes($"{userId}{SIGN_KEY}"))
        );
        if (expectedSign != sign)
        {
            return null;
        }
        // 查询数据库获取用户信息
        using (var unitOfWork = _unitOfWorkFactory.MakeUnitOfWork())
        {
            var currentUser = unitOfWork.Users.GetById(userId);
            if (currentUser != null)
            {
                httpContext.Items["CurrentUser"] = currentUser;
            }
            return currentUser;
        }
    }
    
  4. 改造所有原有获取用户、判断权限的方法,统一调用GetCurrentUser()获取当前请求对应的用户,不再读取共享字段:
    public Users GetActiveUser() => GetCurrentUser();
    
    public string GetActiveUserName() => GetCurrentUser()?.FullName ?? "";
    
    public string GetActiveUserPositionName()
    {
        var user = GetCurrentUser();
        if (user == null) return "";
        using (var unitOfWork = _unitOfWorkFactory.MakeUnitOfWork())
        {
            var position = unitOfWork.Positions.Get(user.PositionId);
            return position?.Name ?? "";
        }
    }
    
    public Users DeactivateUser()
    {
        var httpContext = _httpContextAccessor.HttpContext;
        // 退出时清除认证Cookie
        httpContext.Response.Cookies.Delete("Uid");
        httpContext.Response.Cookies.Delete("UidSign");
        httpContext.Items.Remove("CurrentUser");
        return null;
    }
    
    // 剩下的IsVisibleMainMenu、IsVisibleContractMenu、IsVisibleDistributionMenu方法
    // 全部改为先调用GetCurrentUser()拿到当前用户,再做角色、岗位判断即可
    

安全提示:上述签名校验逻辑可以有效防止用户篡改Cookie伪造身份,全程不需要新增数据表,所有逻辑基于现有用户表实现。

第三步:清理控制器冗余字段

删除SignInController中定义的private Users authorized_user;实例字段,所有需要获取当前用户的位置直接注入IUserIdentity调用对应方法获取即可,避免控制器实例复用时出现状态共享。

改造完成后,每个请求的身份数据完全基于当前请求携带的Cookie解析,和其他用户的请求完全隔离,即使高并发场景下也不会出现身份串号问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 14:01:14