ASP.NET Core 6如何获取AspNetUsers表StoreId实现身份校验
登录时StoreId匹配校验实现方案
你可以按照以下步骤完成需求,所有修改都基于现有代码结构,不需要重构原有身份认证逻辑:
- 第一步:给LoginModel的StoreId字段增加必填校验
现有模型里的StoreId是可空类型且没有校验规则,容易出现空值提交绕过校验的问题,先补充数据注解:
public class LoginModel { [Required] [EmailAddress] public string Email { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } public IList<AuthenticationScheme>? ExternalLogins { get; set; } public string? ReturnUrl { get; set; } public string? ErrorMessage { get; set; } [Required(ErrorMessage = "门店标识缺失,禁止登录")] public string? StoreId { get; set; } }
- 第二步:修改Login接口的校验逻辑,在密码校验前增加StoreId匹配判断
首先确认ShopController已经注入了UserManager<你的用户实体类>,如果没有自定义Identity用户类,默认用IdentityUser即可(需要保证用户实体类中已经定义了StoreId属性,和数据库表字段对应)。
不要在密码校验通过后直接跳转,先查询对应用户的StoreId做比对,不匹配直接返回登录错误,避免越权登录。修改后的Login方法核心代码如下:
// 还没注入UserManager的话,补上构造函数注入 private readonly UserManager<IdentityUser> _userManager; private readonly SignInManager<IdentityUser> _signInManager; private readonly ILogger<ShopController> _logger; public ShopController(SignInManager<IdentityUser> signInManager, ILogger<ShopController> logger, UserManager<IdentityUser> userManager) { _signInManager = signInManager; _logger = logger; _userManager = userManager; } public async Task<IActionResult> Login(LoginModel model) { model.ReturnUrl ??= Url.Content("~/Customer/Shop/"); model.ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { // 先根据提交的邮箱查询用户,提前做StoreId校验,减少无效密码校验的开销 var user = await _userManager.FindByEmailAsync(model.Email); if (user == null) { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return View(model); } // 比对请求携带的StoreId和用户数据库中绑定的StoreId // 如果StoreId是int/Guid类型,先做类型转换再比对即可 if (user.StoreId != model.StoreId) { _logger.LogWarning("账号{Email}尝试在非授权门店{StoreId}登录,已拦截", model.Email, model.StoreId); ModelState.AddModelError(string.Empty, "Invalid login attempt."); return View(model); } // StoreId校验通过后,再走原有密码、双因子、锁定判断逻辑 var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { _logger.LogInformation("账号{Email}在门店{StoreId}登录成功", model.Email, model.StoreId); return LocalRedirect(model.ReturnUrl); } if (result.RequiresTwoFactor) { return RedirectToPage("./LoginWith2fa", new { ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); } if (result.IsLockedOut) { _logger.LogWarning("账号{Email}已被锁定", model.Email); return RedirectToPage("./Lockout"); } else { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return View(model); } } return View(model); }
- 第三步(可选但强烈推荐):增加全局请求的StoreId校验
仅在登录入口做校验无法避免用户登录后篡改门店标识的越权行为,可以实现一个全局Action过滤器,对所有已登录的请求自动校验StoreId是否匹配,不匹配则自动登出跳转登录页:
public class StoreIdValidateFilter : IAsyncActionFilter { private readonly UserManager<IdentityUser> _userManager; private readonly SignInManager<IdentityUser> _signInManager; public StoreIdValidateFilter(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager) { _userManager = userManager; _signInManager = signInManager; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // 未登录请求直接放行,走默认认证逻辑 if (!context.HttpContext.User.Identity.IsAuthenticated) { await next(); return; } // 从请求中获取当前门店标识,可以根据自己的业务存储位置调整取值逻辑,比如从Header、Cookie、路由中取 var currentStoreId = context.HttpContext.Request.Form["StoreId"].FirstOrDefault() ?? context.HttpContext.Request.Query["StoreId"].FirstOrDefault() ?? context.HttpContext.Request.Cookies["CurrentStoreId"]; if (string.IsNullOrWhiteSpace(currentStoreId)) { await _signInManager.SignOutAsync(); context.Result = new RedirectResult("~/Customer/Shop/Login"); return; } var currentUserId = _userManager.GetUserId(context.HttpContext.User); var currentUser = await _userManager.FindByIdAsync(currentUserId); if (currentUser == null || currentUser.StoreId != currentStoreId) { await _signInManager.SignOutAsync(); context.Result = new RedirectResult("~/Customer/Shop/Login"); return; } await next(); } }
在Program.cs中注册全局过滤器即可生效:
builder.Services.AddControllersWithViews(options => { options.Filters.Add<StoreIdValidateFilter>(); });
注意事项
- 所有登录失败的提示统一返回「Invalid login attempt」,不要明确提示「门店不匹配」,避免攻击者枚举系统有效账号
- 前端隐藏域传值可以被用户随意篡改,所有校验逻辑必须放在后端实现,不要依赖前端传值的安全性
- 如果StoreId是数值、Guid类型,比对前一定要先做格式合法性校验,避免类型转换触发异常
内容的提问来源于stack exchange,提问作者Nayeem Rahman
相关产品推荐
相关产品推荐

