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

如何在ASP.NET Core 8中通过LinkedIn OAuth创建并持久化IdentityUser?

解决方案:ASP.NET Core 8 + LinkedIn OAuth + Identity 用户持久化 + JWT

核心逻辑

  • 在LinkedIn OAuth的OnCreatingTicket事件中获取用户信息,查询/创建ApplicationUser并持久化到数据库
  • 替换Cookie认证为JWTBearer,认证成功后生成JWT返回给React SPA
  • 保留Identity的用户管理能力,满足核心功能对用户数据的依赖

步骤1:修改Program.cs配置

1.1 配置Identity与JWT认证

移除Cookie认证,添加JWTBearer配置,同时禁用Identity默认的邮箱/密码端点:

// ... 其他服务配置
builder.Services.AddAuthorization();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});

// 配置Identity,禁用不需要的邮箱/密码相关功能
builder.Services
    .AddIdentityApiEndpoints<ApplicationUser>(options =>
    {
        options.User.RequireUniqueEmail = true;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

// 配置JWT认证
var jwtSettings = builder.Configuration.GetSection("Jwt");
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = "LinkedIn"; // 默认使用LinkedIn发起认证挑战
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = jwtSettings["Issuer"],
        ValidAudience = jwtSettings["Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"]))
    };
})
.AddOAuth("LinkedIn", options =>
{
    options.ClientId = builder.Configuration["LinkedIn:ClientId"];
    options.ClientSecret = builder.Configuration["LinkedIn:ClientSecret"];
    options.AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
    options.TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
    options.CallbackPath = new PathString("/oauth/linkedin-cb");
    options.UserInformationEndpoint = "https://api.linkedin.com/v2/userinfo";
    options.Scope.Add("email");
    options.Scope.Add("profile");
    options.Scope.Add("openid");
    options.SaveTokens = true;

    options.Events = new OAuthEvents
    {
        OnCreatingTicket = async context =>
        {
            // 获取LinkedIn用户信息
            using var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
            using var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
            response.EnsureSuccessStatusCode();
            var userInfo = await response.Content.ReadFromJsonAsync<JsonElement>();

            // 映射基础Claims
            context.RunClaimActions(userInfo);

            // 获取UserManager处理用户持久化
            var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
            var email = userInfo.GetProperty("email").GetString();
            var name = userInfo.GetProperty("name").GetString();
            var linkedInSub = userInfo.GetProperty("sub").GetString();

            // 查询或创建用户
            var user = await userManager.FindByEmailAsync(email);
            if (user == null)
            {
                user = new ApplicationUser
                {
                    UserName = email,
                    Email = email,
                    Name = name,
                    LinkedInSub = linkedInSub // 存储LinkedIn唯一标识,避免重复创建
                };
                var result = await userManager.CreateAsync(user);
                if (!result.Succeeded)
                {
                    throw new InvalidOperationException($"创建用户失败: {string.Join(", ", result.Errors.Select(e => e.Description))}");
                }
            }
            else
            {
                // 更新用户信息(如姓名)
                if (user.Name != name)
                {
                    user.Name = name;
                    await userManager.UpdateAsync(user);
                }
            }

            // 生成关联Identity用户的ClaimsIdentity
            var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
            identity.AddClaim(new Claim(ClaimTypes.Name, user.Name));
            identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));

            context.Principal = new ClaimsPrincipal(identity);
        },
        OnTicketReceived = async context =>
        {
            var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
            var user = await userManager.GetUserAsync(context.Principal);
            if (user == null)
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                return;
            }

            // 生成JWT Token
            var jwtSettings = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>().GetSection("Jwt");
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id),
                new Claim(ClaimTypes.Name, user.Name),
                new Claim(ClaimTypes.Email, user.Email)
            };

            // 添加用户角色(如果需要)
            var roles = await userManager.GetRolesAsync(user);
            claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: jwtSettings["Issuer"],
                audience: jwtSettings["Audience"],
                claims: claims,
                expires: DateTime.Now.AddDays(7),
                signingCredentials: creds);

            var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

            // 跳转前端并携带Token(根据SPA实际地址调整)
            var redirectUri = $"{builder.Configuration["FrontendUrl"]}/login-callback?token={Uri.EscapeDataString(tokenString)}";
            context.Response.Redirect(redirectUri);
        }
    };

    // 配置Claim映射规则
    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
    options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
});

1.2 配置应用管道

// ...
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

// 若不需要Identity默认端点,可注释此行
// app.MapIdentityApi<ApplicationUser>();

app.MapGet("/login", () =>
{
    return Results.Challenge(
        new AuthenticationProperties { RedirectUri = "/oauth/linkedin-cb" },
        authenticationSchemes: ["LinkedIn"]);
});

app.Run();

步骤2:更新ApplicationUser模型

添加字段存储LinkedIn唯一标识,避免重复创建用户:

public class ApplicationUser : IdentityUser
{
    public string Name { get; set; }
    public string LinkedInSub { get; set; } // 存储LinkedIn的sub字段,唯一标识用户
}

执行迁移并更新数据库:

Add-Migration AddLinkedInSubToUser
Update-Database

步骤3:React前端处理

  • 引导用户访问后端/login端点,触发LinkedIn认证流程
  • 在回调页面(如/login-callback)解析URL中的token,存储到localStorage或sessionStorage
  • 后续API请求在请求头中携带JWT:Authorization: Bearer {token}

内容的提问来源于stack exchange,提问作者asd12tgzxvbgt

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 09:13:14