如何在Blazor Web App中实现Steam与Discord第三方账号关联
实现Steam注册与Discord账号关联功能
我需要实现用户通过Steam注册,并能关联其Discord账号的功能。目前已完成Steam(或Google等第三方提供商)的单账号登录功能,但找不到账号关联(比如Google关联Discord)的相关文档。当前已实现的代码如下:
Program.cs
builder.Services.AddAuthentication(opt => { opt.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; opt.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; opt.DefaultChallengeScheme = DiscordAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(opt => { opt.LoginPath = "/signin"; opt.LogoutPath = "/signout"; } ) .AddDiscord(x => { x.ClientId = builder.Configuration.GetSection("Keys")["ClientID"]; x.ClientSecret = builder.Configuration.GetSection("Keys")["SecretID"]; x.SaveTokens = true; }) .AddSteam(x => { x.ApplicationKey = builder.Configuration.GetSection("Keys")["SteamID"]; x.SaveTokens = true; });
AuthController.cs
[HttpPost("~/signin")] public async Task<IActionResult> SignIn([FromForm] string provider) { if (string.IsNullOrWhiteSpace(provider)) { return BadRequest(); } if (!await HttpContext.IsProviderSupportedAsync(provider)) { return BadRequest(); } // 指示对应请求的外部身份提供程序中间件将用户代理重定向到其自身的授权端点 // 注意:authenticationScheme参数必须与配置的值匹配 return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider); } [HttpGet("~/signout")] [HttpPost("~/signout")] public IActionResult SignOutCurrentUser() { // 指示Cookie中间件删除用户代理从外部身份提供程序成功认证后创建的本地Cookie return SignOut(new AuthenticationProperties { RedirectUri = "/" }, CookieAuthenticationDefaults.AuthenticationScheme); }
HttpContextExtensions.cs
此类中的方法用于判断用户想要使用的提供商是否受支持。
public static class HttpContextExtensions { public static async Task<AuthenticationScheme[]> GetExternalProvidersAsync(this HttpContext context) { ArgumentNullException.ThrowIfNull(context); var schemes = context.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>(); return (from scheme in await schemes.GetAllSchemesAsync() where !string.IsNullOrEmpty(scheme.DisplayName) select scheme).ToArray(); } public static async Task<bool> IsProviderSupportedAsync(this HttpContext context, string provider) { ArgumentNullException.ThrowIfNull(context); return (from scheme in await context.GetExternalProvidersAsync() where string.Equals(scheme.Name, provider, StringComparison.OrdinalIgnoreCase) select scheme).Any(); } }
依赖说明
我使用AspNet.Security.OAuth.Discord和AspNet.Security.OpenId.Steam进行身份验证,上述大部分代码来自AspNet.Security.OAuth.Providers的示例。
账号关联功能实现方案
核心逻辑是:用户已登录某第三方账号(如Steam)时,发起另一提供商(如Discord)的认证流程,认证成功后将两个账号的唯一标识绑定到同一用户记录上。
步骤1:扩展认证事件处理
修改Program.cs中第三方认证的配置,通过OnCreatingTicket事件获取用户唯一标识,并处理关联逻辑:
.AddDiscord(x => { x.ClientId = builder.Configuration.GetSection("Keys")["ClientID"]; x.ClientSecret = builder.Configuration.GetSection("Keys")["SecretID"]; x.SaveTokens = true; x.Events.OnCreatingTicket = async context => { var discordUserId = context.User.GetProperty("id").GetString(); if (discordUserId == null) return; // 用户已登录则关联账号 if (context.HttpContext.User.Identity?.IsAuthenticated ?? false) { var currentUserId = context.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); // 调用业务逻辑绑定Discord账号到当前用户 // await _userService.LinkDiscordAccount(currentUserId, discordUserId); } else { // 用户未登录时,检查是否已有绑定该Discord账号的用户,存在则直接登录 var existingUser = await _userService.GetUserByDiscordId(discordUserId); if (existingUser != null) { context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, existingUser.Id.ToString())); } } }; }) .AddSteam(x => { x.ApplicationKey = builder.Configuration.GetSection("Keys")["SteamID"]; x.SaveTokens = true; x.Events.OnCreatingTicket = async context => { var steamUserId = context.User.GetProperty("steamid").GetString(); if (steamUserId == null) return; if (context.HttpContext.User.Identity?.IsAuthenticated ?? false) { var currentUserId = context.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); // 绑定Steam账号到当前用户 // await _userService.LinkSteamAccount(currentUserId, steamUserId); } else { var existingUser = await _userService.GetUserBySteamId(steamUserId); if (existingUser != null) { context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, existingUser.Id.ToString())); } } }; });
步骤2:添加账号关联接口
在AuthController中新增关联接口,仅允许已登录用户触发:
[HttpPost("~/link-account")] [Authorize] public async Task<IActionResult> LinkAccount([FromForm] string provider) { if (string.IsNullOrWhiteSpace(provider)) { return BadRequest(); } if (!await HttpContext.IsProviderSupportedAsync(provider)) { return BadRequest(); } var properties = new AuthenticationProperties { RedirectUri = "/account/linked-success", Items = { ["isLinking"] = "true" } // 标记当前为关联流程 }; return Challenge(properties, provider); }
步骤3:统一认证回调处理
新增回调Action处理登录和关联两种场景:
[HttpGet("~/signin-{provider}")] public async Task<IActionResult> ExternalLoginCallback(string provider, string returnUrl = "/") { var cookieAuthResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); var externalAuthResult = await HttpContext.AuthenticateAsync(provider); if (!externalAuthResult.Succeeded) { return BadRequest("外部认证失败"); } var externalUserId = externalAuthResult.Principal.FindFirstValue(ClaimTypes.NameIdentifier); if (cookieAuthResult.Succeeded) { // 已登录状态,执行账号绑定 var currentUserId = cookieAuthResult.Principal.FindFirstValue(ClaimTypes.NameIdentifier); // await _userService.LinkAccount(currentUserId, provider, externalUserId); return Redirect("/account/linked-success"); } else { // 未登录状态,检查是否已有绑定该外部账号的用户 var existingUser = await _userService.GetUserByExternalProvider(provider, externalUserId); if (existingUser == null) { // 新建用户 existingUser = await _userService.CreateUserFromExternalProvider(provider, externalUserId); } // 登录用户 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(externalAuthResult.Principal.Claims, CookieAuthenticationDefaults.AuthenticationScheme))); return Redirect(returnUrl); } }
步骤4:数据库设计
需新增外部账号关联表,存储用户与第三方账号的绑定关系,示例结构:
| 字段名 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| UserId | Guid | 关联用户表主键 |
| ProviderName | varchar(50) | 提供商名称(如Steam/Discord) |
| ProviderUserId | varchar(255) | 提供商返回的用户唯一标识 |
| CreatedAt | datetime | 创建时间 |
内容的提问来源于stack exchange,提问作者NotS1mp
相关产品推荐
相关产品推荐

