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

时间:2020-03-10 11:52:15

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

此问题适用于具有外部身份提供者的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&handler=Callback看起来像这样

enter image description here

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

可从dotnet存储库中获得有关此代码。 克隆仓库https://github.com/dotnet/aspnetcore.git之后,我读了build instructions,并成功构建了dotnet 5预览。然后我运行了clean,然后切换到带标签的分支release/3.1来为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不为null,而.IsValid为true。
  • 尽管有上面的代码,但实际上并未调用
  • IEmailSender.SendEmailAsync。
  • 如果result.Succeeded为true,则应该显示一条日志消息,内容为“用户使用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,在这种情况下,下一行是写给记录器的关于创建用户帐户的信息。

这没有道理。

2 个答案:

答案 0 :(得分:0)

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

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

答案 1 :(得分:0)

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

有什么区别?在对CICD问题进行故障排除的过程中,该失败版本已添加到一个现有项目中,该项目在3.0和3.1之间来回震荡了几次。显然,它以某种不明显的方式损坏了,这不是问题。

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