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

如何将外部OAuth2认证用户注册到本地自定义IdentityContext?

方案说明

你完全不需要手动创建ServiceProvider副本,OAuthEvents上下文关联的HttpContext已经内置了当前请求的作用域服务容器RequestServices,可以直接从中获取你注册的UserManager<LookForJobUser>、LookForJobIdentityContext等服务,完全符合DI规范,没有多余副本。

具体实现步骤

  1. 修正原有代码中await和.Result混用的潜在死锁问题
  2. 从context.HttpContext.RequestServices获取UserManager实例操作本地用户
  3. 按邮箱查询本地用户,不存在则自动创建
  4. 将本地用户ID存入Claims供后续业务使用

修改后的代码示例

.AddOAuth("LinkedIn", scheme =>
{
    scheme.ClientId = Configuration.GetValue<string>("LinkedIn:ClientId");
    scheme.ClientSecret = Configuration.GetValue<string>("LinkedIn:ClientSecret");
    scheme.Scope.Add("r_liteprofile");
    scheme.Scope.Add("r_emailaddress");
    scheme.CallbackPath = new PathString("/signin-linkedin");
    scheme.AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
    scheme.TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
    scheme.UserInformationEndpoint = "https://api.linkedin.com/v2/me?projection=(id,firstName,lastName)";
    scheme.Events = new OAuthEvents()
    {
        OnCreatingTicket = async context =>
        {
            // 请求用户基本信息
            var userDataRequest = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
            userDataRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
            userDataRequest.Headers.Add("x-li-format", "json");
            var userDataResponse = await context.Backchannel.SendAsync(userDataRequest, context.HttpContext.RequestAborted);
            userDataResponse.EnsureSuccessStatusCode();

            // 请求用户邮箱信息
            var userEmailRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))");
            userEmailRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
            userEmailRequest.Headers.Add("x-li-format", "json");
            var userEmailResponse = await context.Backchannel.SendAsync(userEmailRequest, context.HttpContext.RequestAborted);
            userEmailResponse.EnsureSuccessStatusCode();

            // 解析返回信息
            var userData = JObject.Parse(await userDataResponse.Content.ReadAsStringAsync());
            var userEmail = JObject.Parse(await userEmailResponse.Content.ReadAsStringAsync());
            string linkedInUserId = userData.Value<string>("id");
            string lastName = userData["lastName"]?["localized"]?.Value<string>("en_US");
            string firstName = userData["firstName"]?["localized"]?.Value<string>("en_US");
            string email = userEmail["elements"]?[0]?["handle~"]?.Value<string>("emailAddress");

            if (string.IsNullOrEmpty(email))
            {
                throw new InvalidOperationException("无法获取LinkedIn账号邮箱");
            }

            // 从当前请求服务容器获取UserManager,无需创建ServiceProvider副本
            var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<LookForJobUser>>();
            var localUser = await userManager.FindByEmailAsync(email);

            // 本地不存在该用户则自动创建
            if (localUser == null)
            {
                localUser = new LookForJobUser
                {
                    UserName = email,
                    Email = email,
                    FirstName = firstName,
                    LastName = lastName,
                    LinkedInOpenId = linkedInUserId // 建议扩展用户实体增加该字段,后续可直接用LinkedInID匹配用户
                };
                // 生成随机初始密码,外部登录用户无需使用密码登录
                var createResult = await userManager.CreateAsync(localUser, Guid.NewGuid().ToString("N"));
                if (!createResult.Succeeded)
                {
                    throw new InvalidOperationException("本地用户创建失败:" + string.Join(",", createResult.Errors.Select(e => e.Description)));
                }
                // 可在此处添加默认角色分配逻辑
            }

            // 构造Claims,优先使用本地用户ID作为身份标识
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, localUser.Id.ToString(), ClaimValueTypes.String, context.Options.ClaimsIssuer),
                new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, context.Options.ClaimsIssuer)
            };
            if (!string.IsNullOrEmpty(lastName))
                claims.Add(new Claim(ClaimTypes.Name, lastName, ClaimValueTypes.String, context.Options.ClaimsIssuer));
            if (!string.IsNullOrEmpty(firstName))
                claims.Add(new Claim(ClaimTypes.GivenName, firstName, ClaimValueTypes.String, context.Options.ClaimsIssuer));
            if (!string.IsNullOrEmpty(linkedInUserId))
                claims.Add(new Claim("LinkedInUserId", linkedInUserId, ClaimValueTypes.String, context.Options.ClaimsIssuer));

            var claimsIdentity = new ClaimsIdentity(claims, context.Scheme.Name);
            await context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
            context.RunClaimActions();
        }
    };
});

更解耦的可选方案

如果你的认证逻辑比较复杂,还可以把OnCreatingTicket的业务逻辑抽成独立的服务:

  • 定义ILinkedInAuthHandler接口和对应实现类,把用户信息解析、本地用户匹配创建的逻辑都放到服务中
  • 将该服务注册为Scoped生命周期
  • 在OnCreatingTicket中直接从RequestServices获取该服务调用即可,Startup配置代码会更简洁,逻辑也更方便单元测试。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 03:27:01