Blazor WASM Core Hosted .NET7 Identity配置:取消注册并优化OIDC登录
Blazor WASM Core Hosted .NET7 外部OIDC认证配置问题
需求与现状
- 基于.NET7的Blazor WASM Core Hosted架构应用,创建时选择Individual Accounts模板
- 要求通过外部OIDC身份提供商完成认证,禁用本地用户名密码登录,用户角色与声明由应用内部管理
- 内部系统,仅允许管理员创建用户,禁止用户自行注册,无需账号确认流程
- 当前存在两个核心问题:
- 用户首次通过外部认证后,会被强制引导至邮箱注册页面,不符合需求;未在系统中的用户应直接进入应用,携带空声明集合
- 登录流程需跳转至内置登录页再次点击登录,需改为单页直接触发OIDC认证跳转
现有代码
MyApp.Server Program.cs
builder.Services.AddDefaultIdentity<ApplicationUser>(options => { options.SignIn.RequireConfirmedAccount = false; options.SignIn.RequireConfirmedPhoneNumber = false; options.SignIn.RequireConfirmedEmail = false; options.User.RequireUniqueEmail = true; }).AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddScoped<IClaimsTransformation, UserClaimsTransformation>(); builder.Services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(); builder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; }) .AddOpenIdConnect("oidc", options => { options.ClientId = builder.Configuration.GetSection("OpenIDConnect:ClientId").Get<string>(); options.ClientSecret = builder.Configuration.GetSection("OpenIDConnect:ClientSecret").Get<string>(); options.Authority = builder.Configuration.GetSection("OpenIDConnect:Authority").Get<string>(); options.SaveTokens = true; options.ResponseType = "code"; options.Scope.Add("openid"); options.Scope.Add("profile"); options.UsePkce = true; //events }).AddIdentityServerJwt();
MyApp.Server UserClaimsTransformation.cs
public class UserClaimsTransformation : IClaimsTransformation { private readonly UserManager<ApplicationUser> _userManager; public UserClaimsTransformation(UserManager<ApplicationUser> userManager) { _userManager = userManager; } public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { var clone = principal.Clone(); var newIdentity = (ClaimsIdentity)clone.Identity; var identity = principal.Identities.FirstOrDefault(c=>c.IsAuthenticated); var loggedInUserId = principal.Claims.FirstOrDefault(c => c.Type == "emailaddress")?.Value; var user = await _userManager.FindByEmailAsync(loggedInUserId); if (user == null) { return principal; } var roles = await _userManager.GetRolesAsync(user); var userClaims = await _userManager.GetClaimsAsync(user); foreach (var role in roles) { var claim = new Claim(newIdentity.RoleClaimType, role); newIdentity.AddClaim(claim); } foreach (var claim in userClaims) { newIdentity.AddClaim(claim); } return clone; } }
解决方案
1. 禁用自动注册流程,处理未存在用户
通过OIDC的OnTokenValidated事件跳过默认注册流程,无论用户是否存在都直接完成认证:
修改Program.cs中的OIDC配置,添加事件逻辑:
.AddOpenIdConnect("oidc", options => { // 保留原有配置不变 // ... options.Events = new OpenIdConnectEvents { OnTokenValidated = async context => { // 获取外部提供商返回的用户标识(优先取邮箱,无则取sub声明) var email = context.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; var sub = context.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(sub)) { context.Fail("无法获取用户唯一标识"); return; } var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>(); var user = await userManager.FindByEmailAsync(email); // 系统中无此用户时,直接完成认证,不触发注册流程 if (user == null) { // 可在此添加未注册用户的访问日志记录逻辑 return; } // 系统中有此用户时,关联本地用户身份 var localIdentity = await userManager.CreateIdentityAsync(user, context.Scheme.Name); context.Principal.AddIdentity(localIdentity); }, // 注销后直接跳转回应用首页,避免多余跳转 OnRedirectToIdentityProviderForSignOut = context => { context.Response.Redirect(context.Options.SignedOutRedirectUri ?? "/"); context.HandleResponse(); return Task.CompletedTask; } }; })
同时禁用注册页面跳转:
builder.Services.Configure<IdentityOptions>(options => { options.Cookies.ApplicationCookie.Events.OnRedirectToRegister = context => { context.Response.Redirect("/"); context.HandleResponse(); return Task.CompletedTask; }; });
2. 实现单页认证流程
修改客户端(Blazor WASM)的LoginDisplay.razor,直接触发OIDC认证挑战:
<AuthorizeView> <Authorized> <a href="authentication/logout">注销</a> </Authorized> <NotAuthorized> <a href="authentication/login?scheme=oidc">登录</a> </NotAuthorized> </AuthorizeView>
3. 优化声明转换逻辑
修改UserClaimsTransformation.cs,确保未存在用户时返回空声明集合:
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { var email = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; var user = await _userManager.FindByEmailAsync(email); if (user == null) { // 返回仅包含认证类型的空声明主体 var emptyIdentity = new ClaimsIdentity(principal.Identity.AuthenticationType); return new ClaimsPrincipal(emptyIdentity); } // 已有用户的角色与声明添加逻辑 var clone = principal.Clone(); var newIdentity = (ClaimsIdentity)clone.Identity; var roles = await _userManager.GetRolesAsync(user); var userClaims = await _userManager.GetClaimsAsync(user); foreach (var role in roles) { newIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); } foreach (var claim in userClaims) { newIdentity.AddClaim(claim); } return clone; }
4. 彻底禁用本地用户名密码登录
移除默认的本地登录相关配置,仅保留必要的用户存储:
// 替换原有AddDefaultIdentity builder.Services.AddIdentityCore<ApplicationUser>(options => { options.SignIn.RequireConfirmedAccount = false; options.SignIn.RequireConfirmedPhoneNumber = false; options.SignIn.RequireConfirmedEmail = false; options.User.RequireUniqueEmail = true; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // 认证配置保留原有OIDC和Cookie逻辑,无需修改
内容的提问来源于stack exchange,提问作者jason
相关产品推荐
相关产品推荐

