【问题标题】:ASP.Net Core MVC Identity - Add temporary (session) claimASP.Net Core MVC Identity - 添加临时(会话)声明
【发布时间】:2018-03-30 17:10:04
【问题描述】:

对于初学者来说,这里 (Temporary Session Based Claims in ASP.NET Core Identity) 和这里 (How to add claim to user dynamically?) 也有人问过这个问题,但是这两个问题都没有得到答案,所以我试图恢复它......

我正在创建一个多租户网络应用程序。用户登录,他们可以看到与自己公司相关的数据(但不能看到使用该应用程序的其他公司的数据)。由于特许经营集团拥有多个店面等,单个用户要求访问多个不同公司的情况并不少见,但在这种情况下,他们必须在登录时选择单个公司。

几乎所有数据查询都需要公司 ID 作为参数,因此我需要一种方便的方法来检查用户当前登录的公司。如果他们注销并登录到不同的公司,我需要查看不同的公司 ID。我想将它存储为我可以在会话期间看到的身份声明,但我不一定要将它存储到数据库中,因为它可能会随着每次登录而改变。

这是我的登录操作(基于标准 ASP.Net Core MVC 模板):

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            // BEGIN CUSTOM CODE - Check which companies a user can access
            IEnumerable<int> allCompanies = _myDbService.GetUserCompanies(model.Email);

            if (allCompanies.Count() == 1)
            {
                // then they can only access one company - log them into it automatically.
                // I need easy access to its ID with the User since it is used with almost every db query.

                int companyID = allCompanies[0];
                ((ClaimsIdentity)User.Identity).AddClaim(new Claim("CompanyID", companyID.ToString())); // this shows as null on future controller actions.

                Debug.WriteLine("Set breakpoint here to examine User"); // I see 1 claim (my custom claim) in the debugger, but it is not in the database yet.

                // future controller actions show me 3 claims (nameidentifier, name, and security stamp) but User.Claims.First(c => c.Type == "CompanyID").Value throws error.

                return RedirectToLocal(returnUrl);
            }
            else
            {
                // the user has access to several companies - make them choose one to complete login process
                RedirectToAction("ChooseCompany", new { companyList = allCompanies });
            }
            // END CUSTOM CODE
        }

        // REMAINING CODE OMITTED FOR BREVITY
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

所以我的问题是:为什么自定义声明不“坚持”以便在未来的控制器操作中可以看到?如果这是标准行为,为什么不将其保存到数据库中?有没有办法在不使用 AddSession() 的情况下在会话期间保持这个值?如果我将此作为声明来实现,我是否每次访问该值时都会往返于数据库?

【问题讨论】:

    标签: c# asp.net-mvc asp.net-core-mvc asp.net-identity


    【解决方案1】:

    对于遇到此问题的其他人,这是我到目前为止的位置...要将声明永久存储在数据库中并在登录时自动加载,您必须使用以下内容,并且更改将在下次登录之前生效:

    await userManager.AddClaimAsync(user, new Claim("your-claim", "your-value"));
    

    使用以下内容只会存储当前会话的声明,所以我在正确的轨道上:

    ((ClaimsIdentity)User.Identity).AddClaim(new Claim("your-claim", "your-value"));
    

    我发现在控制器操作之间“坚持”声明的唯一方法是覆盖 UserClaimsPrincipalFactory,以便 ASP.Net 将您的自定义代码用作登录过程的一部分。这里的问题是您只能在相关方法中访问 ApplicationUser,并且您不能真正传递任何自定义参数 AFAIK。所以我首先通过 ApplicationUser 类修改如下:

    public class ApplicationUser : IdentityUser
    {
        public long ActiveDealershipID { get; set; }
    } 
    

    (您必须应用 EF 迁移)。现在我创建一个派生自 UserClaimsPrincipalFactory 的类,如下所示:

    public class MyClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
    {
        public MyClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> options) : base(userManager, roleManager, options)
        {
        }
    
        public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
        {
            var principal = await base.CreateAsync(user);
    
            long dealershipID = user.ActiveDealershipID;
            ((ClaimsIdentity)principal.Identity).AddClaim(new Claim("DealershipID", dealershipID.ToString()));
    
            return principal;
        }
    }
    

    (必须在startup.cs ConfigureServices中注册服务):

    services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, MyClaimsPrincipalFactory>();
    

    然后在我的登录操作中,我在登录之前从模型数据中设置新的用户属性,以便 UserClaimsPrincipalFactory 在设置声明时可以使用它:

    public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;
        if (ModelState.IsValid)
        {
            ApplicationUser user = await _userManager.FindByEmailAsync(model.Email);
            user.ActiveDealershipID = model.ChosenDealership
            await _userManager.UpdateAsync(user);
    
            var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
            if (result.Succeeded)
            {
                _logger.LogInformation("User logged in.");
                ...
    

    我仍然愿意接受更好的建议或其他想法,但这似乎对我有用。

    【讨论】:

    • 如果用户只有一家公司,这将是可行的方法,但不适用于您的情况,因为用户可以在每个会话中动态更改公司。
    • @Evk - 这就是为什么我在每次登录之前设置用户属性并在登录操作中调用 userManager.UpdateAsync(user) - 除非我遗漏了什么,否则该步骤会更改 AspNetUsers 表中的 ActiveDealershipID ClaimsPrincipalFactory 获取数据,因此在身份验证 cookie 中使用了全新的 ID。
    • 是的,但我的意思是当用户可以访问多家公司并且您询问使用哪个公司时。但毕竟您确实可以将“选择浏览哪家公司”概念更改为“活跃公司”,这是全球性的,而不是每个会话。因此,在登录后,用户正在浏览适合他的任何活跃公司的上下文,但可以更改它(为此,您必须在代码中注销并再次登录他)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多