【问题标题】:Core3/React confirmation email not sentCore3/React 确认邮件未发送
【发布时间】:2020-03-10 11:52:15
【问题描述】:

此问题适用于具有外部身份提供者的 core3/react 项目,创建如下。

dotnet new react --auth Individual --use-local-db --output conf

并进行了修改以支持外部身份提供者。包已添加

dotnet add package Microsoft.AspNetCore.Authentication.MicrosoftAccount

并且启动被修改

services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
    options.ClientId = Configuration["Authentication:Microsoft:ClientId"];
    options.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"];
    options.CallbackPath = "/signin-microsoft";
})

在关注instructions provided by Microsoft 之后,我通过注册为用户来测试我的工作。没有抛出任何错误,但承诺的确认电子邮件从未到达。

按照说明末尾的故障排除建议,我在我的 IEmailSender 实现的 SendEmailAsync 方法的开头设置了一个断点,并重复了这个练习。没有命中断点。

如果我通过更新数据库手动确认帐户,

  • 我可以登录了。
  • “忘记密码”链接将我带到密码恢复页面,并使用该页面击中我的断点并成功发送包含有效链接的密码重置电子邮件。

显然,我的 IEmailSender 实现可以正常工作并且已正确注册。它与示例代码不完全相同,因为我有自己的 Exchange 服务器并且没有使用 SendGrid,但它成功发送了一封电子邮件以重置密码,我可以毫无障碍地重复此操作任意次数。

考虑到它以某种方式导致问题的可能性很小,这是我的实现

public class SmtpEmailSender : IEmailSender
{
    public SmtpEmailSender(IOptions<SmtpOptions> options)
    {
        this.smtpOptions = options.Value;
    }
    private SmtpOptions smtpOptions { get; }
    public Task SendEmailAsync(string email, string subject, string htmlMessage)
    {
        var smtp = new SmtpClient();
        if (!smtpOptions.ValidateCertificate)
        {
            smtp.ServerCertificateValidationCallback = (s, c, h, e) => true;
        }
        smtp.Connect(smtpOptions.Host, smtpOptions.Port, SecureSocketOptions.Auto);
        if (smtpOptions.Authenticate)
        {
            smtp.Authenticate(smtpOptions.Username, smtpOptions.Password);
        }
        var message = new MimeMessage()
        {
            Subject = subject,
            Body = new BodyBuilder() { HtmlBody = htmlMessage }.ToMessageBody()
        };
        message.From.Add(new MailboxAddress(smtpOptions.Sender));
        message.To.Add(new MailboxAddress(email));
        return smtp.SendAsync(FormatOptions.Default, message).ContinueWith(antecedent =>
        {
            smtp.Disconnect(true);
            smtp.Dispose();
        });
    }
}

startup.cs 中的注册如下所示。

            services.AddTransient<IEmailSender, SmtpEmailSender>();
            services.Configure<SmtpOptions>(Configuration.GetSection("SmtpOptions"));

SmptOptions 只是从 appsettings.json 中提取并注入到 ctor 中的设置。显然,该方面有效,否则密码重置电子邮件将无效。

注册不会有任何问题,因为应用程序停止生成需要阅读并遵循我链接的帐户确认说明的消息。

为了查看问题是否是由我的代码的一些无意的副作用引起的,我创建了一个 IEmailSender 的插桩存根

public class DummyEmailSender : IEmailSender
{
    private readonly ILogger logger;

    public DummyEmailSender(ILogger<DummyEmailSender> logger)
    {
        this.logger = logger;
    }
    public Task SendEmailAsync(string email, string subject, string htmlMessage)
    {
        logger.LogInformation($"SEND EMAIL\r\nemail={email} \r\nsubject={subject}\r\nhtmlMessage={htmlMessage}\r\n{new StackTrace().ToString().Substring(0,500)}");
        return Task.CompletedTask;
    }
}

我还更新了服务注册以匹配。

这是最简单的检测存根,观察到的行为是相同的,它在提交忘记密码表单时调用,而在提交确认注册表单时调用。

有没有人做过可怕的事情?怎么样?


在失败之前,这个 URL https://wone.pdconsec.net/Identity/Account/ExternalLogin?returnUrl=%2Fauthentication%2Flogin&amp;handler=Callback 看起来像这样

检查页面我们发现注册按钮向/Identity/Account/ExternalLogin?returnUrl=%2Fauthentication%2Flogin&amp;amp;handler=Confirmation发布表单

此代码可从 dotnet 存储库获得。 克隆 repo https://github.com/dotnet/aspnetcore.git 后,我阅读了 build instructions 并成功构建了 dotnet 5 预览版。然后我在切换到标记分支release/3.1 之前运行clean 来为core3.1 构建调试包,但这失败了,因为标记分支使用了一个稍微太旧的msbuild 版本并且错误消息建议的补救措施似乎不起作用。由于我对 PowerShell 的控制很弱(构建脚本是 PowerShell),我只能进行代码检查。相关代码如下所示。

public override async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information during confirmation.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await _userManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = userId, code = code },
                        protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (_userManager.Options.SignIn.RequireConfirmedAccount)
                {
                    return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                }

                await _signInManager.SignInAsync(user, isPersistent: false);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    ProviderDisplayName = info.ProviderDisplayName;
    ReturnUrl = returnUrl;
    return Page();
}

看起来应该可以了。我们知道什么?

  • 不会引发未处理的错误,它会传递给 RegisterConfirmation,后者会显示一条关于永远不会收到的电子邮件的消息。
  • CreateUser 被调用并成功。我们知道这一点是因为用户是在数据库中创建的。所以它肯定会过去,这意味着ModelState 不是空的,.IsValid 是真的。
  • IEmailSender.SendEmailAsync 实际上并没有被调用,尽管上面的代码。
  • 如果 result.Succeeded 为真,则应该有一条日志消息,内容类似于“用户使用 Microsoft 帐户提供程序创建了一个帐户”
  • 它重定向到https://localhost:5001/Identity/Account/RegisterConfirmation?Email=accountname@outlook.com

我看到了大多数事情的日志消息。在第一次创建用户后尝试第二次注册但未能发送电子邮件,控制台和事件日志中会出现关于 DuplicateUserName 的警告。直接在数据库中设置确认,我们可以登录,然后交互式删除帐户,并显示这些活动的日志。

但没有显示日志以供确认。 真正让我头疼的是它会重定向到https://localhost:5001/Identity/Account/RegisterConfirmation?Email=accountname@outlook.com

这太疯狂了。为了到达那里,userManager.AddLoginAsync() 必须返回 true,在这种情况下,下一行是写给记录器的关于创建用户帐户的信息。

这毫无意义。

【问题讨论】:

    标签: .net-core asp.net-identity asp.net-core-3.0


    【解决方案1】:

    您应该自己发送确认电子邮件,它不会自动发送。 注册用户后:

            string token = await userManager.GenerateEmailConfirmationTokenAsync(user);
            string urltoken = Base64UrlEncoder.Encode(token);
    
            string link = string.Format(emailOptions.ConfirmationUrl, user.Id, urltoken);
            string body = $"<a href='{link}'>confirm</a>";
            await emailSender.SendEmailAsync(user.Email, "confirmation", body);
    

    【讨论】:

    • 我使用的是 dotnet core 3.1 而不是 2.x,我会更新问题。如果您想看看新玩具,请获取 3.1 SDK 并生成一个项目dotnet new react -au Individual,然后对其进行自定义以添加对外部身份提供者的支持。
    • 更改这两行代码,'await _emailStore.SetEmailAsync(...);' with 'await _userManager.CreateAsync(user);'
    • 如果我可以编译 3.1 分支,我会尝试的。
    【解决方案2】:

    我创建了一个全新的项目并进行了练习。效果很好。

    有什么区别?失败的版本被添加到现有项目中,该项目在解决 CICD 问题的过程中多次在 3.0 和 3.1 之间来回切换。显然它以某种不明显的方式损坏了,这不是问题。

    我没有删除整个问题的唯一原因是其他人可能会掉进这个坑。

    【讨论】:

      猜你喜欢
      • 2015-02-02
      • 2016-09-22
      • 1970-01-01
      • 1970-01-01
      • 2011-11-01
      • 2011-12-14
      • 1970-01-01
      • 1970-01-01
      • 2014-02-01
      相关资源
      最近更新 更多