如何在ASP.NET Identity中更新声明?

121

我正在为我的MVC5项目使用OWIN身份验证。

这是我的SignInAsync函数。

 private async Task SignInAsync(ApplicationUser user, bool isPersistent)
        {
            var AccountNo = "101";
            AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
            var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
            identity.AddClaim(new Claim(ClaimTypes.UserData, AccountNo));
            AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent, RedirectUri="Account/Index"}, identity);
        }

正如您所看到的,我将AccountNo添加到了Claims列表中。

现在,我该如何在应用程序的某个时刻更新这个Claim?目前,我的做法是:

 public string AccountNo
        {

            get
            {
                var CP = ClaimsPrincipal.Current.Identities.First();
                var Account= CP.Claims.FirstOrDefault(p => p.Type == ClaimTypes.UserData);
                return Account.Value;
            }
            set
            {
                var CP = ClaimsPrincipal.Current.Identities.First();
                var AccountNo= CP.Claims.FirstOrDefault(p => p.Type == ClaimTypes.UserData).Value;
                CP.RemoveClaim(new Claim(ClaimTypes.UserData,AccountNo));
                CP.AddClaim(new Claim(ClaimTypes.UserData, value));
            }

        }
当我尝试移除声明时,出现了以下异常:

无法移除声明'http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata: 101'。它要么不是此标识的一部分,要么是包含此标识的主体拥有的声明。例如,当使用角色创建GenericPrincipal时,主体将拥有该声明。角色将通过在构造函数中传递的标识公开,但实际上并不由标识所拥有。RolePrincipal也存在类似的逻辑。

如何移除和更新声明?

如果您正在将用户信息存储在声明中,并且希望在用户信息更改后更新声明,则可以调用 SignInManager.SignInAsync 来刷新声明的值。请参见此问题 - Hooman Bahreini
17个回答

159

我创建了一个扩展方法,用于根据给定的ClaimsIdentity添加/更新/读取声明。

namespace Foobar.Common.Extensions
{
    public static class Extensions
    {
        public static void AddUpdateClaim(this IPrincipal currentPrincipal, string key, string value)
        {
            var identity = currentPrincipal.Identity as ClaimsIdentity;
            if (identity == null)
                return;

            // check for existing claim and remove it
            var existingClaim = identity.FindFirst(key);
            if (existingClaim != null)
                identity.RemoveClaim(existingClaim);

            // add new claim
            identity.AddClaim(new Claim(key, value));
            var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
            authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
        }

        public static string GetClaimValue(this IPrincipal currentPrincipal, string key)
        {
            var identity = currentPrincipal.Identity as ClaimsIdentity;
            if (identity == null)
                return null;

            var claim = identity.Claims.FirstOrDefault(c => c.Type == key);

            // ?. prevents a exception if claim is null.
            return claim?.Value;
        }
    }
}

然后使用它

using Foobar.Common.Extensions;

namespace Foobar.Web.Main.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // add/updating claims
            User.AddUpdateClaim("key1", "value1");
            User.AddUpdateClaim("key2", "value2");
            User.AddUpdateClaim("key3", "value3");
        }

        public ActionResult Details()
        {
            // reading a claim
            var key2 = User.GetClaimValue("key2");          
        }
    }
}

2
最终,我有另一种解决方案,它似乎正在工作……大多数情况下。但最终还是改用了这种方法,因为它似乎总是有效的。谢谢! - saml
8
有没有针对 Asp.Net Core 的相同解决方案? - Martín
1
这似乎只适用于当前应用程序。我想更新SSO服务器发出的cookie,以便其他应用程序也可以访问它们。有什么想法吗?谢谢。 - Whoever
2
应该将代码从“var claim = identity.Claims.First(c => c.Type == key); return claim.Value;”修改为“var claim = identity.Claims.FirstOrDefault(c => c.Type == key); return claim?.Value;”。 - liuhongbo
1
你好,我有一个自定义的用户存储,所以我在其中实现了IUserClaimStore接口。当我从你的代码中调用identity.AddClaim(new Claim(key, value));时,用户存储的AddClaimAsync(T user, Claim claim)方法没有被调用。我需要做些额外的事情吗? - jstuardo
显示剩余9条评论

62

您可以创建一个新的ClaimsIdentity,然后使用它进行声明更新。

set {
    // get context of the authentication manager
    var authenticationManager = HttpContext.GetOwinContext().Authentication;

    // create a new identity from the old one
    var identity = new ClaimsIdentity(User.Identity);

    // update claim value
    identity.RemoveClaim(identity.FindFirst("AccountNo"));
    identity.AddClaim(new Claim("AccountNo", value));

    // tell the authentication manager to use this new identity
    authenticationManager.AuthenticationResponseGrant = 
        new AuthenticationResponseGrant(
            new ClaimsPrincipal(identity),
            new AuthenticationProperties { IsPersistent = true }
        );
}

4
您可以更新声明,但仍需使用更新后的身份注销并重新登录用户。 - user3210546
3
不,它不会注销用户,我们只是更新用户的cookie。 - Irshu
8
请记住,这将只更新身份信息。如果您希望存储这些声明并在请求时自动加载它们,您需要使用用户管理器来删除和更新它们。这花费了我一些时间!:( - Dennis van der Stelt
3
如果我没有任何 cookie,只使用 accessToken,那怎么办?在我的情况下,下一次请求的声明与更改前相同。唯一更新声明的方式是注销用户并要求其再次登录 :-( - Nozim Turakulov
1
看一下AuthenticationResponseGrant的构造函数,它也接受一个ClaimsIdentity实例而不是ClaimsPrincipal。使用它有什么区别吗?虽然没有太大的区别,但可以避免强制转换。 - luizs81
显示剩余5条评论

20

使用Identity的UserManager和SigninManager来反映Identity cookie中的更改(并可选择从db表AspNetUserClaims中删除声明)的另一种(异步)方法:

// Get User and a claims-based identity
ApplicationUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
var Identity = new ClaimsIdentity(User.Identity);

// Remove existing claim and replace with a new value
await UserManager.RemoveClaimAsync(user.Id, Identity.FindFirst("AccountNo"));
await UserManager.AddClaimAsync(user.Id, new Claim("AccountNo", value));

// Re-Signin User to reflect the change in the Identity cookie
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);

// [optional] remove claims from claims table dbo.AspNetUserClaims, if not needed
var userClaims = UserManager.GetClaims(user.Id);
if (userClaims.Any())
{
  foreach (var item in userClaims)
  {
    UserManager.RemoveClaim(user.Id, item);
  }
}

对我来说,这里的关键是在设置声明之后才执行 SignInAsync() - FirstVertex
1
感谢您提供有关从数据库中删除索赔的提示。让我意识到我需要在自己完成后进行清理。 - Uber Schnoz

16
使用最新的Asp.Net Identity和.net core 2.1,我能够使用以下逻辑更新用户声明:
  1. 注册一个UserClaimsPrincipalFactory,这样每次SignInManager将用户登录时,都会创建声明。

services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimService>();
  • 实现一个自定义的UserClaimsPrincipalFactory<TUser, TRole>,代码如下

  • public class UserClaimService : UserClaimsPrincipalFactory<ApplicationUser, ApplicationRole>
    {
        private readonly ApplicationDbContext _dbContext;
    
        public UserClaimService(ApplicationDbContext dbContext, UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
        {
            _dbContext = dbContext;
        }
    
        public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
        {
            var principal = await base.CreateAsync(user);
    
            // Get user claims from DB using dbContext
    
            // Add claims
            ((ClaimsIdentity)principal.Identity).AddClaim(new Claim("claimType", "some important claim value"));
    
            return principal;
        }
    }
    
  • 在应用程序的后续部分,当您更改数据库中的内容并希望将其反映给已经通过身份验证并登录的用户时,以下行可以实现此目的:

  • var user = await _userManager.GetUserAsync(User);
    await _signInManager.RefreshSignInAsync(user);
    

    这样可以确保用户能够查看最新的信息,而无需重新登录。我将其放在控制器中返回结果之前,以便在操作完成时,所有内容都得到安全的刷新。

    与编辑现有声明并为安全 cookie 等创建竞争条件不同,您只需静默地登录用户并刷新状态即可 :)


    谢谢,我也遇到了同样的问题,这个解决方案更适合更新已签名用户的声明。 - ameya
    谢谢!我在 .NET Core 3.1 中也遇到了同样的问题。 - Kevin Tran
    对我来说,我只添加了第三步的代码,现在它可以正常工作了。感谢@Mahmut C。 - Nileksh Dhimer

    7

    我也遇到了这个异常,我是这样解决的

    var identity = User.Identity as ClaimsIdentity;
    var newIdentity = new ClaimsIdentity(identity.AuthenticationType, identity.NameClaimType, identity.RoleClaimType);
    newIdentity.AddClaims(identity.Claims.Where(c => false == (c.Type == claim.Type && c.Value == claim.Value)));
    // the claim has been removed, you can add it with a new value now if desired
    AuthenticationManager.SignOut(identity.AuthenticationType);
    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, newIdentity);
    

    5

    从这里编译了一些答案,结合我的补充内容,形成了可重用的ClaimsManager类。

    声明被持久化,用户cookie已更新,登录已刷新。

    请注意,如果您没有自定义ApplicationUser,则可以将其替换为IdentityUser。此外,在我的情况下,在开发环境中需要具有稍微不同的逻辑,因此您可能需要删除IWebHostEnvironment依赖项。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;
    using YourMvcCoreProject.Models;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Hosting;
    
    namespace YourMvcCoreProject.Identity
    {
        public class ClaimsManager
        {
            private readonly UserManager<ApplicationUser> _userManager;
            private readonly SignInManager<ApplicationUser> _signInManager;
            private readonly IWebHostEnvironment _env;
            private readonly ClaimsPrincipalAccessor _currentPrincipalAccessor;
    
            public ClaimsManager(
                ClaimsPrincipalAccessor currentPrincipalAccessor,
                UserManager<ApplicationUser> userManager,
                SignInManager<ApplicationUser> signInManager,
                IWebHostEnvironment env)
            {
                _currentPrincipalAccessor = currentPrincipalAccessor;
                _userManager = userManager;
                _signInManager = signInManager;
                _env = env;
            }
    
            /// <param name="refreshSignin">Sometimes (e.g. when adding multiple claims at once) it is desirable to refresh cookie only once, for the last one </param>
            public async Task AddUpdateClaim(string claimType, string claimValue, bool refreshSignin = true)
            {
                await AddClaim(
                    _currentPrincipalAccessor.ClaimsPrincipal,
                    claimType,
                    claimValue, 
                    async user =>
                    {
                        await RemoveClaim(_currentPrincipalAccessor.ClaimsPrincipal, user, claimType);
                    },
                    refreshSignin);
            }
    
            public async Task AddClaim(string claimType, string claimValue, bool refreshSignin = true)
            {
                await AddClaim(_currentPrincipalAccessor.ClaimsPrincipal, claimType, claimValue, refreshSignin);
            }
    
            /// <summary>
            /// At certain stages of user auth there is no user yet in context but there is one to work with in client code (e.g. calling from ClaimsTransformer)
            /// that's why we have principal as param
            /// </summary>
            public async Task AddClaim(ClaimsPrincipal principal, string claimType, string claimValue, bool refreshSignin = true)
            {
                await AddClaim(
                    principal,
                    claimType,
                    claimValue, 
                    async user =>
                    {
                        // allow reassignment in dev
                        if (_env.IsDevelopment()) 
                            await RemoveClaim(principal, user, claimType);
    
                        if (GetClaim(principal, claimType) != null)
                            throw new ClaimCantBeReassignedException(claimType);                
                    },
                    refreshSignin);
            }
    
            public async Task RemoveClaims(IEnumerable<string> claimTypes, bool refreshSignin = true)
            {
                await RemoveClaims(_currentPrincipalAccessor.ClaimsPrincipal, claimTypes, refreshSignin);
            }
    
            public async Task RemoveClaims(ClaimsPrincipal principal, IEnumerable<string> claimTypes, bool refreshSignin = true)
            {
                AssertAuthenticated(principal);
                foreach (var claimType in claimTypes)
                {
                    await RemoveClaim(principal, claimType);
                }
                // reflect the change in the Identity cookie
                if (refreshSignin)
                    await _signInManager.RefreshSignInAsync(await _userManager.GetUserAsync(principal));
            }
    
            public async Task RemoveClaim(string claimType, bool refreshSignin = true)
            {
                await RemoveClaim(_currentPrincipalAccessor.ClaimsPrincipal, claimType, refreshSignin);
            }
    
            public async Task RemoveClaim(ClaimsPrincipal principal, string claimType, bool refreshSignin = true)
            {
                AssertAuthenticated(principal);
                var user = await _userManager.GetUserAsync(principal);
                await RemoveClaim(principal, user, claimType);
                // reflect the change in the Identity cookie
                if (refreshSignin)
                    await _signInManager.RefreshSignInAsync(user);
            }
    
            private async Task AddClaim(ClaimsPrincipal principal, string claimType, string claimValue, Func<ApplicationUser, Task> processExistingClaims, bool refreshSignin)
            {
                AssertAuthenticated(principal);
                var user = await _userManager.GetUserAsync(principal);
                await processExistingClaims(user);
                var claim = new Claim(claimType, claimValue);
                ClaimsIdentity(principal).AddClaim(claim);
                await _userManager.AddClaimAsync(user, claim);
                // reflect the change in the Identity cookie
                if (refreshSignin)
                    await _signInManager.RefreshSignInAsync(user);
            }
    
            /// <summary>
            /// Due to bugs or as result of debug it can be more than one identity of the same type.
            /// The method removes all the claims of a given type.
            /// </summary>
            private async Task RemoveClaim(ClaimsPrincipal principal, ApplicationUser user, string claimType)
            {
                AssertAuthenticated(principal);
                var identity = ClaimsIdentity(principal);
                var claims = identity.FindAll(claimType).ToArray();
                if (claims.Length > 0)
                {
                    await _userManager.RemoveClaimsAsync(user, claims);
                    foreach (var c in claims)
                    {
                        identity.RemoveClaim(c);
                    }
                }
            }
    
            private static Claim GetClaim(ClaimsPrincipal principal, string claimType)
            {
                return ClaimsIdentity(principal).FindFirst(claimType);    
            }    
    
            /// <summary>
            /// This kind of bugs has to be found during testing phase
            /// </summary>
            private static void AssertAuthenticated(ClaimsPrincipal principal)
            {
                if (!principal.Identity.IsAuthenticated)
                    throw new InvalidOperationException("User should be authenticated in order to update claims");
            }
    
            private static ClaimsIdentity ClaimsIdentity(ClaimsPrincipal principal)
            {
                return (ClaimsIdentity) principal.Identity;
            }
        }
    
    
        public class ClaimCantBeReassignedException : Exception
        {
            public ClaimCantBeReassignedException(string claimType) : base($"{claimType} can not be reassigned")
            {
            }
        }
    
    public class ClaimsPrincipalAccessor
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
    
        public ClaimsPrincipalAccessor(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }
    
        public ClaimsPrincipal ClaimsPrincipal => _httpContextAccessor.HttpContext.User;
    }
    
    // to register dependency put this into your Startup.cs and inject ClaimsManager into Controller constructor (or other class) the in same way as you do for other dependencies    
    public class Startup
    {
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<ClaimsPrincipalAccessor>();
            services.AddTransient<ClaimsManager>();
        }
    }
    

    }


    2
    感谢您的提问,关于.NET 4 / OWIN的问题得到了解答,但为了帮助寻找.NET 5或更高版本等效内容的搜索者,以下是一些示例代码。
    我相信您可以改进它,但这是一个使用Microsoft.AspNetCore.Identity中的UserManager和SignInManager的工作起点。
    // Get the user first first. 
    var claims = await _userManager.GetClaimsAsync(user);
    var givenNameClaim = claims.FirstOrDefault(r => r.Type == JwtClaimTypes.GivenName);
    
    IdentityResult result = null;
    
    if (givenNameClaim != null)
    {
        result = await _userManager.ReplaceClaimAsync(user, givenNameClaim, new Claim(JwtClaimTypes.GivenName, "<newvalue>"));
    }
    else
    {
        result = await _userManager.AddClaimAsync(user, new Claim(JwtClaimTypes.GivenName, "<newvalue>"));
    }
    
    if (result.Errors.Any())
    {
        // TODO: List errors here;
    }
    else
    {
        await _signInManager.RefreshSignInAsync(user); // refresh the login, so it takes effect immediately.
    }
    

    如何才能获取UserManager的第一行和用户? - Henrique Belotto
    这将被依赖注入到控制器或类中。也许像这篇帖子这样的内容可能会有所帮助。 - JsAndDotNet

    2
    当我使用MVC5时,在此处添加声明。
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(PATAUserManager manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            userIdentity.AddClaim(new Claim(ClaimTypes.Role, this.Role));
    
            return userIdentity;
        }
    

    当我在SignInAsync函数中检查声明结果时,无论如何都无法获取角色值。但是...
    此请求完成后,我可以在其他操作(另一个请求)中访问角色。
     var userWithClaims = (ClaimsPrincipal)User;
            Claim CRole = userWithClaims.Claims.First(c => c.Type == ClaimTypes.Role);
    

    我认为可能是异步导致了IEnumerable在进程后面被更新。


    2
    你可以通过实现 CookieAuthenticationEvents 类并重写 ValidatePrincipal 方法来更新当前用户的声明。 在那里,您可以删除旧声明,添加新声明,然后使用 CookieValidatePrincipalContext.ReplacePrincipal 替换主体。 这不会影响存储在数据库中的任何声明。 这是使用ASP.NET Core Identity 2.2。
    public class MyCookieAuthenticationEvents : CookieAuthenticationEvents
    {
        string newAccountNo = "102";
    
        public override Task ValidatePrincipal(CookieValidatePrincipalContext context)
        {
            // first remove the old claim
            var claim = context.Principal.FindFirst(ClaimTypes.UserData);
            if (claim != null)
            {
                ((ClaimsIdentity)context.Principal.Identity).RemoveClaim(claim);
            }
    
            // add the new claim
            ((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim(ClaimTypes.UserData, newAccountNo));
    
            // replace the claims
            context.ReplacePrincipal(context.Principal);
            context.ShouldRenew = true;
    
            return Task.CompletedTask;
        }
    }
    

    您需要在Startup.cs中注册事件类:

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<MyCookieAuthenticationEvents>();
    
        services.ConfigureApplicationCookie(o =>
        {
            o.EventsType = typeof(MyCookieAuthenticationEvents);
        });
    }
    

    你可以将服务注入到事件类中,以访问新的AccountNo值,但根据此页面上的警告,应避免执行任何过于昂贵的操作:

    警告

    这里描述的方法会在每个请求时触发。在每个请求中验证所有用户的身份验证 cookie 可能会对应用程序造成很大的性能损失。


    谢谢,这在 asp.net core 3.1 中对我非常有效! - darkezm0

    1

    目前对于我来说更新现有索赔的最简单解决方案是:

    //updating user data        
    await signInManager.SignOutAsync();
    await signInManager.SignInAsync(user, false);
    

    这个可行,我终于放弃了 IUserStore、SignInManager、UserManager、ClaimsPrincipalAccessor 和 ClaimsManager 等复杂的东西。 - Phoeson
    它对我不起作用,调用SignInAsync后在HttpContext.User.Claims中没有看到新用户声明,你有什么想法吗? - Caner
    @Caner 就像我一样。数据库中有基于用户数据的声明。我对它们进行了更新,然后重新注册了用户,这样应用程序就可以自动更新声明。您也可以手动更新声明,但是那样您还必须手动更新 cookie 等内容,但我还没有弄清楚如何做到这一点,也无法告诉您。 P.s. 对于使用 Google 翻译,我感到抱歉) - Vitkir

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接