You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor WASM Core Hosted .NET7 Identity配置:取消注册并优化OIDC登录

Blazor WASM Core Hosted .NET7 外部OIDC认证配置问题

需求与现状

  • 基于.NET7的Blazor WASM Core Hosted架构应用,创建时选择Individual Accounts模板
  • 要求通过外部OIDC身份提供商完成认证,禁用本地用户名密码登录,用户角色与声明由应用内部管理
  • 内部系统,仅允许管理员创建用户,禁止用户自行注册,无需账号确认流程
  • 当前存在两个核心问题:
    1. 用户首次通过外部认证后,会被强制引导至邮箱注册页面,不符合需求;未在系统中的用户应直接进入应用,携带空声明集合
    2. 登录流程需跳转至内置登录页再次点击登录,需改为单页直接触发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 13:17:33