Angular+BFF架构下如何通过OpenIdConnectEvents处理登录权限校验并反馈Angular
问题
我搭建了基于Angular SPA应用与ASP.NET Core 9 Web API的BFF应用架构,采用OpenId Connect对接Microsoft EntraId、Microsoft及Google身份提供商实现单点登录,已参照Damien Bowden的示例完成配置,正常登录后可重定向至指定Angular页面。
但遇到一个问题:如何处理那些已完成OIDC登录,但因用户账号停用或已从应用数据库移除而无权访问应用的情况?
我尝试通过OpenIdConnectEvents处理,但不清楚如何将未授权信息返回给Angular,编写了如下代码:
builder.Services .Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options => { options.Events = new OpenIdConnectEvents { OnTokenValidated = async context => { if (context.Principal?.Identity is not ClaimsIdentity claimsIdentity) return; var userName = claimsIdentity.FindFirst("preferred_username")?.Value; var userServiceRepository = context.HttpContext.RequestServices.GetRequiredService<IUserServiceRepository>(); var usager = await userServiceRepository.GetUsager(userName); if (usager == null) // 注:原代码逻辑写反,应为用户不存在时拒绝 { context.Fail("Unauthorized"); } } }; });
使用该方法后,Web API会重定向至https://localhost:7143/signin-oidc端点并抛出异常。
我考虑过两种思路:
- 不完成用户登录,而是在从
signin-oidc重定向后通过API调用校验认证状态; - 给用户添加
new Claim("access_denied", "true")声明后完成登录,但觉得这种情况不应视为登录成功,方案不合理。
想请教这类流程的最佳实践方案。
最佳实践方案
针对BFF架构下的本地用户权限校验场景,推荐以下落地方案:
1. 在OnTokenValidated中自定义失败重定向,拦截无效用户
你当前代码的问题是调用context.Fail()后,OIDC中间件会默认触发错误流程,需手动指定重定向路径,直接跳转到Angular的无权限页面:
OnTokenValidated = async context => { var userName = context.Principal?.FindFirst("preferred_username")?.Value; if (string.IsNullOrEmpty(userName)) { context.Fail("Missing username claim"); return; } var userService = context.HttpContext.RequestServices.GetRequiredService<IUserServiceRepository>(); var usager = await userService.GetUsager(userName); if (usager == null || !usager.IsActive) // 校验用户存在且处于激活状态 { // 清除已生成的本地认证Cookie await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); // 重定向到Angular的无权限页面,携带错误原因参数 context.Response.Redirect($"/unauthorized?reason=account_invalid"); // 终止后续OIDC流程,避免默认错误跳转 context.HandleResponse(); } }
这种方式在OIDC登录流程的最前端拦截无效用户,无需完成登录,直接引导到Angular的专属错误页,体验更直接。
2. 全局会话校验中间件,覆盖登录后全场景
如果需要支持会话中期禁用用户的检测(比如用户登录后被后台停用),可以在BFF层添加全局中间件,每次请求时校验本地用户状态:
app.Use(async (context, next) => { if (context.User.Identity?.IsAuthenticated == true) { var userName = context.User.FindFirst("preferred_username")?.Value; var userService = context.RequestServices.GetRequiredService<IUserServiceRepository>(); var usager = await userService.GetUsager(userName); if (usager == null || !usager.IsActive) { // 登出OIDC会话和本地认证Cookie await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); // 重定向到无权限页 context.Response.Redirect("/unauthorized?reason=account_disabled"); return; } } await next(); });
该方案不仅能处理登录时的校验,还能在用户使用过程中实时检测账号状态,保证安全性。
3. 明确拒绝"伪登录"方案
不建议给用户添加access_denied声明后完成登录:这种方式会让用户处于已认证状态,但实际无权限,后续需要在Angular或API层额外处理校验逻辑,增加复杂度,还可能引发安全隐患(比如用户利用有效会话Cookie尝试绕过校验)。
补充注意事项
- 拦截无效用户后,必须清除OIDC会话和本地认证Cookie,避免用户陷入登录循环;
- Angular端需实现
/unauthorized页面,解析URL参数中的错误原因,给用户友好提示; - 性能敏感场景可给本地用户状态添加缓存(如Redis),避免每次请求都查询数据库。
内容的提问来源于stack exchange,提问作者jboo

