如何使用 Dapper Identity 和 Discord Login 设置 .NET 6

Boj*_*jan 6 c# identity discord asp.net-core-6.0 .net-6.0

我正在尝试弄清楚如何在使用 Dapper 作为我的 ORM 的同时通过 Discord Oauth2 设置登录。

微软这里有一个指南,我按照它来设置我的所有商店。我实际上可以打电话CreateAsync()方法并在我的数据库中创建用户,所以我相信事情的一面已经完全设置完毕。

我的问题在于外部登录。下面你会发现我已经尝试过的内容。

程序.cs:

//omitted code that binds interfaces and classes - this code works and is fully tested. it is not related to problem at hand.
builder.Services.AddIdentity<User, Role>()
.AddDefaultTokenProviders();

builder.Services.AddAuthentication()
.AddCookie(options =>
{
    options.LoginPath = "/signin";
    options.LogoutPath = "/signout";
})
.AddDiscord(options =>
{
    options.ClientId = "some id";
    options.ClientSecret = "some secret";
    options.ClaimActions.MapCustomJson("urn:discord:avatar:url", user =>
        string.Format(
            CultureInfo.InvariantCulture,
            "https://cdn.discordapp.com/avatars/{0}/{1}.{2}",
            user.GetString("id"),
            user.GetString("avatar"),
            user.GetString("avatar")!.StartsWith("a_") ? "gif" : "png"));
});

builder.Services.AddRazorPages();

var app = builder.Build();
app.UseDeveloperExceptionPage();

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();

app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");

app.Run();
Run Code Online (Sandbox Code Playgroud)

这是帐户控制器代码:

public class AccountController : Controller
{
    private readonly ISignInService _signInService;
    private readonly IUserService _userService;

    public AccountController(ISignInService signInService, IUserService userService)
    {
        _signInService = signInService;
        _userService = userService;
    }

    [HttpGet("~/signin")]
    public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());

    [HttpPost("~/signin")]
    public async Task<IActionResult> SignIn([FromForm] string provider, string returnUrl)
    {
        if (string.IsNullOrWhiteSpace(provider))
        {
            return BadRequest();
        }

        if (!await HttpContext.IsProviderSupportedAsync(provider))
        {
            return BadRequest();
        }

        var redirectUrl = Url.Action(nameof(LoginCallback), "Account", new { returnUrl });
        var properties = _signInService.ConfigureExternalAuthenticationProperties(provider, redirectUrl, null);
        properties.Items.Add("XsrfKey", "Test");
        
        return Challenge(properties, provider);
    }

    [HttpGet("~/signout")]
    [HttpPost("~/signout")]
    public IActionResult SignOutCurrentUser()
    {
        return SignOut(new AuthenticationProperties {RedirectUri = "/"},
            CookieAuthenticationDefaults.AuthenticationScheme);
    }

    //[HttpGet("~/Account/LoginCallback")]
    [HttpGet]
    public async Task<IActionResult> LoginCallback(string returnUrl = null, string remoteError = null)
    {
        if (remoteError != null)
        {
            return RedirectToAction("Index", "Home");
        }
        var info = await _signInService.GetExternalLoginInfoAsync("Test");
        if (info == null)
        {
            return RedirectToAction("Index", "Home");
        }

        var result = await _signInService.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
        if (result.Succeeded)
        {
            return RedirectToLocal(returnUrl);
        }
        if (result.IsLockedOut)
        {
            return RedirectToAction("Index", "Home");
        }
        else
        {
            // If the user does not have an account, then ask the user to create an account.
            ViewData["ReturnUrl"] = returnUrl;
            ViewData["LoginProvider"] = info.LoginProvider;
            var email = info.Principal.FindFirstValue(ClaimTypes.Email);
            return RedirectToAction("Index", "Home");
        }
    }

    private IActionResult RedirectToLocal(string returnUrl)
    {
        if (Url.IsLocalUrl(returnUrl))
        {
            return Redirect(returnUrl);
        }
        else
        {
            return RedirectToAction(nameof(HomeController.Index), "Home");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

发生的情况如下:

  1. 我点击“通过不和谐登录”按钮。
  2. 我被带到 Discord 网站
  3. 我通过不和谐网站登录
  4. 我被重定向回我的网站
  5. 信息永远不会被检索。var info = await _signInService.GetExternalLoginInfoAsync("Test");该行始终为空。

我一直在努力找出我在设置中忽略的内容,因为我没有任何错误。

我正在使用这个包

Gid*_*aya 2

首先...我们需要看一下 SignInManager.cs 中内部方法GetExternalLoginInfoAsync的实现,并记下所有可能导致返回 null 的条件。

我将在下面的代码中以评论的形式提供我的答案:

    /// <summary>
    /// Gets the external login information for the current login, as an asynchronous operation.
    /// </summary>
    /// <param name="expectedXsrf">Flag indication whether a Cross Site Request Forgery token was expected in the current request.</param>
    /// <returns>The task object representing the asynchronous operation containing the <see name="ExternalLoginInfo"/>
    /// for the sign-in attempt.</returns>
    public virtual async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
    {
        var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
        var items = auth?.Properties?.Items;
        if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
        {
            // What cases can lead us here?
            // * The authentication was unsuccessful maybe due to
            //   - Login cancellation
            //   - Project not running on a secured environment (https)
            //   - SignInScheme property of auth options not
            //     equal to IdentityConstants.ExternalScheme
            return null;
        }

        if (expectedXsrf != null)
        {
            // It is important to note that XsrfKey is a constant
            // declared above in this class whose value is "XsrfId".
            if (!items.ContainsKey(XsrfKey))
            {
              // What cases can lead us here?
              // * You passed an argument for expectedXsrf but
              //   the initialized key-value pairs does not contain
              //   any key for XsrfKey ("XsrfId").
              // In your case the below is wrong:
              // properties.Items.Add("XsrfKey", "Test"); <= remove
              // Pass the value as 3rd parameter in
              // "ConfigureExternalAuthenticationProperties" method call instead
              // _signInService.ConfigureExternalAuthenticationProperties(provider, redirectUrl, "Test")
                return null;
            }
            var userId = items[XsrfKey] as string;
            if (userId != expectedXsrf)
            {
              // What cases can lead us here?
              // * The argument passed for expectedXsrf does not
              //   match the value of initialized key-value pair
              //   for XsrfKey ("XsrfId").
              // Ensure "Test" should go with "XsrfId" as key
              // by passing the value as 3rd parameter in
              // "ConfigureExternalAuthenticationProperties" method call instead.
                return null;
            }
        }

        var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
        var provider = items[LoginProviderKey] as string;
        if (providerKey == null || provider == null)
        {
            return null;
        }

        var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName
                                  ?? provider;
        return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
        {
            AuthenticationTokens = auth.Properties.GetTokens()
        };
    }
Run Code Online (Sandbox Code Playgroud)

因此,从代码审查来看,这些是 null 的一些可能原因:

  1. 身份验证不成功可能是由于

    • 取消登录

    • 项目未在安全环境中运行 (https)

    • StartUp.cs 或 appsettings.json 下的身份验证选项的 SignInScheme 属性不等于 IdentityConstants.ExternalScheme

  2. 您传递了 ExpectedXsrf 的参数,但initialized key-value pair不包含 的任何键XsrfKey ("XsrfId")

    • 在你的情况下,以下是错误的:

      property.Items.Add("XsrfKey", "测试"); <= 删除此行,因为“XsrfKey”未知。

      相反,您可以在“ConfigureExternalAuthenticationProperties”方法调用中将值作为第三个参数传递:

    _signInService.ConfigureExternalAuthenticationProperties(provider, redirectUrl, "Test");