Core3/React 确认电子邮件未发送

Core3/React confirmation email not sent

此问题适用于 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 中的设置。显然,这方面有效或密码重置电子邮件无效。

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

为了查看问题是否是由我的代码的一些无意的副作用引起的,我创建了一个 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;
    }
}

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

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

有没有人遇到过这种可怕的事情?怎么样?


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

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

此代码可从 dotnet 存储库中获得。 克隆 repo https://github.com/dotnet/aspnetcore.git 后,我阅读了 build instructions 并成功构建了 dotnet 5 预览版。然后我 运行 clean 在切换到标记的 b运行ch release/3.1 为 core3.1 构建调试包之前失败了,因为标记的 b运行ch启用一个稍微太旧的 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();
}

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

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

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

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

这毫无意义。

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

        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);

我创建了一个全新的项目并进行了练习。它完美运行。

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

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