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 方法的开头设置了一个断点,并重复了该练习。断点未命中。
如果我通过更新数据库手动确认账户,
- 我可以登录了。
- 忘记密码 link 将我带到密码恢复页面并使用它到达我的断点并成功发送一封密码重置电子邮件,其中 link 有效。
很明显,我的 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&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();
}
看起来应该可以。我们知道什么?
- 不会抛出任何未处理的错误,它会通过 RegisterConfirmation,后者会显示一条关于永远不会收到的电子邮件的消息。
CreateUser
被调用并成功。我们知道这一点,因为用户是在数据库中创建的。所以它肯定会过去,这意味着 ModelState
不为空并且 .IsValid
为真。
尽管有上面的代码,- IEmailSender.SendEmailAsync 并没有真正被调用。
- 如果
result.Succeeded
为真,应该有一条日志消息说 "User created an account using Microsoft Account provider"
- 它重定向到
https://localhost:5001/Identity/Account/RegisterConfirmation?Email=accountname@outlook.com
我看到了大多数事情的日志消息。在第一次通过创建用户但未能发送电子邮件后尝试第二次注册,有关 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 之间来回切换了好几次。很明显,它以某种不明显的方式损坏了,这不是问题。
我没有删除整个问题的唯一原因是其他人可能会掉进这个洞。
此问题适用于 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 方法的开头设置了一个断点,并重复了该练习。断点未命中。
如果我通过更新数据库手动确认账户,
- 我可以登录了。
- 忘记密码 link 将我带到密码恢复页面并使用它到达我的断点并成功发送一封密码重置电子邮件,其中 link 有效。
很明显,我的 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&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();
}
看起来应该可以。我们知道什么?
- 不会抛出任何未处理的错误,它会通过 RegisterConfirmation,后者会显示一条关于永远不会收到的电子邮件的消息。
CreateUser
被调用并成功。我们知道这一点,因为用户是在数据库中创建的。所以它肯定会过去,这意味着ModelState
不为空并且.IsValid
为真。
尽管有上面的代码,- IEmailSender.SendEmailAsync 并没有真正被调用。
- 如果
result.Succeeded
为真,应该有一条日志消息说 "User created an account using Microsoft Account provider" - 它重定向到
https://localhost:5001/Identity/Account/RegisterConfirmation?Email=accountname@outlook.com
我看到了大多数事情的日志消息。在第一次通过创建用户但未能发送电子邮件后尝试第二次注册,有关 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 之间来回切换了好几次。很明显,它以某种不明显的方式损坏了,这不是问题。
我没有删除整个问题的唯一原因是其他人可能会掉进这个洞。