.NET6 Blazor WASM IdentityServer4登录事件获取user始终为null
问题原因
- 监听的事件类型完全错误:
ClientAuthenticationSuccess是客户端认证成功事件,触发时机是Blazor WASM这类OAuth客户端使用客户端ID、密钥向IdentityServer请求令牌的阶段,此时认证流程还未完成用户身份绑定,HttpContext.User中不存在有效的用户身份声明,UserManager.GetUserAsync自然无法匹配到用户实体。 - 取值方式存在设计缺陷:IdentityServer的事件对象本身携带了认证全流程的上下文信息,不需要依赖
IHttpContextAccessor间接获取用户数据,这种跨组件获取HttpContext的方式在IdentityServer事件管道中本身就存在上下文不同步的问题,稳定性极差。 - 事件注册方式有误:直接通过
services.AddTransient<IEventSink, CustomEventSink>()注册自定义事件Sink,会覆盖IdentityServer内置的默认事件处理实现,同时可能导致事件生命周期异常。
正确实现方案
1. 修正事件监听逻辑
首先移除对ClientAuthenticationSuccess事件的监听,用户主动登录成功/失败对应的事件ID分别是EventIds.UserLoginSuccess、EventIds.UserLoginFailure,同时直接从事件对象中提取用户ID查询用户,不再依赖HttpContext。修正后的CustomEventSink代码如下:
public class CustomEventSink : IEventSink { private readonly ApplicationDbContext _context; private readonly UserManager<ApplicationUser> _userManager; // 移除不必要的IHttpContextAccessor依赖 public CustomEventSink(ApplicationDbContext context, UserManager<ApplicationUser> userManager) { _context = context; _userManager = userManager; } public async Task PersistAsync(Event evt) { // 处理用户登录成功事件 if (evt.Id == EventIds.UserLoginSuccess) { var loginEvent = evt as UserLoginSuccessEvent; if (loginEvent == null) return; try { // 直接从事件携带的用户主键(SubjectId)查询用户,无需走HttpContext var user = await _userManager.FindByIdAsync(loginEvent.SubjectId); if (user == null) return; var log = new EventLog { EventName = "Login", EventDescription = $"{evt.Name} ({evt.Id}), 登录客户端:{loginEvent.ClientId}, 登录IP:{loginEvent.RemoteIpAddress}", Created = DateTime.Now, UserName = user.UserName }; _context.EventLog.Add(log); await _context.SaveChangesAsync(); } catch (Exception ex) { // 建议在此处添加异常日志,不要空吞异常 } } // 处理用户登录失败事件 if (evt.Id == EventIds.UserLoginFailure) { var failEvent = evt as UserLoginFailureEvent; if (failEvent == null) return; try { var log = new EventLog { EventName = "Login Failure", EventDescription = $"{evt.Name} ({evt.Id}), 尝试登录用户名:{failEvent.Username}, 失败原因:{failEvent.Error}, 客户端:{failEvent.ClientId}, IP:{failEvent.RemoteIpAddress}", Created = DateTime.Now, UserName = failEvent.Username }; _context.EventLog.Add(log); await _context.SaveChangesAsync(); } catch (Exception ex) { // 建议在此处添加异常日志,不要空吞异常 } } } }
2. 修正IdentityServer服务配置
首先打开信息类事件的开关(用户登录事件属于Information级别,不打开开关不会触发),然后使用IdentityServer内置的AddEventSink方法注册自定义Sink,删除原来单独注册IEventSink的代码,避免覆盖默认实现:
services.AddIdentityServer(options => { options.Events.RaiseSuccessEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseErrorEvents = true; // 必须打开,否则UserLogin相关事件不会触发 options.Events.RaiseInformationEvents = true; }) .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => { options.IdentityResources["openid"].UserClaims.Add("role"); options.ApiResources.Single().UserClaims.Add("role"); }) // 链式注册自定义事件Sink,不要单独在外面注册IEventSink .AddEventSink<CustomEventSink>(); // 删除下面这行旧的注册代码 // services.AddTransient<IEventSink, CustomEventSink>();
补充说明
- 如果需要记录令牌签发、刷新令牌的行为,可以额外监听
EventIds.TokenIssuedSuccess事件,但注意该事件在每次access_token刷新时都会触发,如果只需要统计用户主动输入账号密码的登录行为,仅监听UserLoginSuccess即可,不会产生重复日志。 - 原有中间件管道顺序(
UseIdentityServer放在UseAuthentication之前)无需调整,符合IdentityServer的中间件注册要求。
内容的提问来源于stack exchange,提问作者user3656651
相关产品推荐
相关产品推荐

