如何将外部OAuth2认证用户注册到本地自定义IdentityContext?
方案说明
你完全不需要手动创建ServiceProvider副本,OAuthEvents上下文关联的HttpContext已经内置了当前请求的作用域服务容器RequestServices,可以直接从中获取你注册的UserManager<LookForJobUser>、LookForJobIdentityContext等服务,完全符合DI规范,没有多余副本。
具体实现步骤
- 修正原有代码中
await和.Result混用的潜在死锁问题 - 从
context.HttpContext.RequestServices获取UserManager实例操作本地用户 - 按邮箱查询本地用户,不存在则自动创建
- 将本地用户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
相关产品推荐
相关产品推荐

