IdentityServer4令牌过期后登出/重登触发ArgumentNullException问题求助
从你的异常堆栈信息能直接定位问题根源:在生成用户Claims的环节,Claim构造函数接收到了null值的value参数,而默认的UserClaimsPrincipalFactory(包括IdentityServer4.AspNetIdentity中的实现)没有做null值校验,刚好你的用户实体里某个用于生成Claim的属性为null,在token过期后重新验证身份时触发了这个异常。
下面是不需要修改NuGet包代码的可行解决方案:
1. 自定义UserClaimsPrincipalFactory,添加Null检查
创建一个自定义的Claims工厂,继承原有的UserClaimsPrincipalFactory,重写GenerateClaimsAsync方法,在生成Claim前先过滤掉null或空值的情况:
无角色版本(适用于未启用角色的项目)
using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using System.Security.Claims; using System.Linq; using System.Threading.Tasks; public class CustomUserClaimsPrincipalFactory<TUser> : UserClaimsPrincipalFactory<TUser> where TUser : class { public CustomUserClaimsPrincipalFactory( UserManager<TUser> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor) { } protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TUser user) { var identity = await base.GenerateClaimsAsync(user); // 移除所有值为null/空字符串的无效Claim var invalidClaims = identity.Claims.Where(c => string.IsNullOrWhiteSpace(c.Value)).ToList(); foreach (var claim in invalidClaims) { identity.RemoveClaim(claim); } // 如果需要自定义生成额外Claim,这里也可以加入null检查 // 示例:处理可能为null的自定义用户属性 // var nickname = await UserManager.GetUserAsync(user, "Nickname"); // if (!string.IsNullOrWhiteSpace(nickname)) // { // identity.AddClaim(new Claim("nickname", nickname)); // } return identity; } }
带角色版本(适用于启用角色的项目)
using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using System.Security.Claims; using System.Linq; using System.Threading.Tasks; public class CustomUserClaimsPrincipalFactory<TUser, TRole> : UserClaimsPrincipalFactory<TUser, TRole> where TUser : class where TRole : class { public CustomUserClaimsPrincipalFactory( UserManager<TUser> userManager, RoleManager<TRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor) { } protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TUser user) { var identity = await base.GenerateClaimsAsync(user); // 过滤无效Claim var invalidClaims = identity.Claims.Where(c => string.IsNullOrWhiteSpace(c.Value)).ToList(); foreach (var claim in invalidClaims) { identity.RemoveClaim(claim); } return identity; } }
2. 在依赖注入容器中注册自定义Factory
替换默认的Claims工厂实现,让系统使用我们的自定义版本:
.NET 5及之前(Startup.cs)
public void ConfigureServices(IServiceCollection services) { // ... 其他服务注册代码 ... // 替换默认工厂(根据你的项目选择对应版本) services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory<ApplicationUser>>(); // 如果启用了角色,使用带角色的版本 // services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>(); }
.NET 6+(Program.cs)
builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory<ApplicationUser>>(); // 启用角色的项目替换为: // builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
3. 额外排查:确保用户实体属性的完整性
检查你的自定义User实体(比如ApplicationUser),确认哪些属性会被用来生成Claims(比如UserName、Email、PhoneNumber等)。如果数据库中存在这些属性为null的用户记录,要么修改数据库约束确保非空,要么在自定义Factory中针对性处理(比如跳过该Claim或用空字符串替代)。
方案原理
默认的UserClaimsPrincipalFactory是NuGet包中的受保护代码无法直接修改,但我们可以通过依赖注入替换的方式,用自定义工厂覆盖默认实现,在生成Claims的环节加入null值校验,从根源避免触发ArgumentNullException,再也不用手动清除浏览器缓存和Cookie来临时解决问题。
内容的提问来源于stack exchange,提问作者BinaryNexus

