更新到 net core 3.1 后 SMTP 客户端停止工作
SMTP client stopped working after updating to netcore 3.1
将我的 asp.net-core web API 项目从 2.2 更新到 3.1 后,当我尝试通过 System.Net.Mail.SmtpClient
发送电子邮件时出现以下异常:
- 2020-02-09 14:40:26.8163 15 ( SmtpClient.OnSendCompleted => <>c__DisplayClass78_0.<SendMailAsync>b__0 => SmtpClient.HandleCompletion ) - Error Failed to send E-Mail. -- ( Exception: System.Net.Mail.SmtpException: Failure sending mail.
---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.
at System.Net.Security.SslStream.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, ExceptionDispatchInfo exception)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.PartialFrameCallback(AsyncProtocolRequest asyncRequest)
--- End of stack trace from previous location where exception was thrown ---
at System.Net.Security.SslStream.ThrowIfExceptional()
at System.Net.Security.SslStream.InternalEndProcessAuthentication(LazyAsyncResult lazyResult)
at System.Net.Security.SslStream.EndProcessAuthentication(IAsyncResult result)
at System.Net.Security.SslStream.EndAuthenticateAsClient(IAsyncResult asyncResult)
at System.Net.Mail.SmtpConnection.ConnectAndHandshakeAsyncResult.TlsStreamAuthenticateCallback(IAsyncResult result)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw(Exception source)
at System.Net.Mail.SmtpConnection.ConnectAndHandshakeAsyncResult.End(IAsyncResult result)
at System.Net.Mail.SmtpTransport.EndGetConnection(IAsyncResult result)
at System.Net.Mail.SmtpClient.ConnectCallback(IAsyncResult result)
--- End of inner exception stack trace ---
API 的端点是通过 appsettings.json
中的 Kestrel 部分配置的
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://myapidomain:80"
},
"Https": {
"Url": "https://www.myapidomain:443",
"Certificate": {
"Path": <path to pfx file>,
"Password": <password>
}
}
}
},
我就是这样使用 SmtpClient
:
using (var client = new SmtpClient(_options.MailServiceHost, _options.MailServicePort))
{
client.EnableSsl = true;
client.ClientCertificates.Add(_certificate);
client.DeliveryMethod = SmtpDeliveryMethod.Network;
client.UseDefaultCredentials = false;
client.Credentials = new NetworkCredential(_options.MailAccountName, _options.MailAccountPassword);
var mail = new MailMessage
{
From = new MailAddress(author),
Body = body,
Subject = subject,
Sender = new MailAddress(author),
SubjectEncoding = Encoding.UTF8,
BodyEncoding = Encoding.UTF8,
HeadersEncoding = Encoding.UTF8
};
mail.Headers.Add("Content-Type", "content=text/html; charset=\"UTF-8\"");
mail.To.Add(receiver);
mail.ReplyToList.Add(author);
_logger.LogInformation($"Sending mail to: {receiver}");
await client.SendMailAsync(mail);
return true;
}
以上代码在我切换回 aspnetcore2.2
版本时有效,所以我怀疑这是证书本身的问题(我对两者使用完全相同的 .pfx 文件)。另一个重要的提示可能是,当在 Windows 机器上使用自签名开发证书进行本地调试时,它实际上可以在 aspnetcore3.1
上运行。在生产环境中,如果上述代码失败,我会 运行 在 Ubuntu 上设置 API。
我的猜测是 asp.net-core 3.x 中证书的处理方式发生了变化。或者 Windows 库的行为不同。如果需要,我还可以 post 两个 Startup.cs 文件。
是否有关于实际原因或如何解决此问题的更多详细信息的建议?
更新
按照 Adam 的建议,我已切换到 MailKit 的 SmtpClient
实现。该错误在生产中仍然存在,但我设法使用 client.ServerCertificateValidationCallback
获得了有关该错误的更多详细信息。在检查回调的 ChainStatus
属性:
时,我得到了以下日志打印
X509ChainStatusFlags: RevocationStatusUnknown
StatusInformation: unable to get certificate CRL
有人可以解释为什么无法获得证书 CRL 会导致我的连接失败吗?我该如何解决这个问题?
更新 2
此外,当我设置 client.CheckCertificateRevocation = false
时出现以下错误:
X509ChainStatusFlags: PartialChain
StatusInformation: unable to get local issuer certificate
Subject: CN=smtp.gmail.com, O=Google LLC, L=Mountain View, S=California, C=US
Issuer: CN=GTS CA 1O1, O=Google Trust Services, C=US
更新 3
似乎 netcore 框架找不到所需的 ca 证书,因为当我在 Ubuntu 机器上 运行 执行命令时,我收到与更新 2 中完全相同的错误消息:
openssl s_client -connect smtp.gmail.com:465
--> Verify return code: 20 (unable to get local issuer certificate)
但是,当我明确指定 ca 证书存储路径时验证通过。
openssl s_client -CApath /etc/ssl/certs/ -connect smtp.gmail.com:465
--> Verify return code: 0 (ok)
所以我想最后一个问题是:如何让我的 aspnet-core web API 找到 Ubuntu 的 ca 证书存储?
更新 4
我已经报告了这个问题 here,也许 dotnet 团队最终会解决这个问题。作为(希望)临时解决方法,我最终手动验证了证书的指纹。
因此,如果您查看 SmtpClient() 的文档,您会发现它现在已标记为已过时。我真的很惊讶你没有得到编译器错误,你应该有,但这可能是明天的问题。
Microsoft 推荐使用 MailKit 库。链接:Nuget - GitHub
我鼓励您使用 Mailkit 库而不是使用 System.Net.Mail.SmtpClient()
重新实现您的代码。除了不会过时之外,MailKit 还是一个非常易于使用的库,开发人员和贡献者投入了大量时间和思考。
使库过时并推荐 MailKit 的原因是
SmtpClient and its network of types are poorly designed
有趣的事实:.Net Core 1.0 无法访问 System.Net.Mail
命名空间,并且在之后添加了支持,但是当 System.Net.Mail
在 .NET Core 中可用时,MailKit
是 .NET Core 早期采用者的首选库。
我终于弄明白为什么它在 Linux 上停止工作了。 netcore3.1框架不是从OS的ca-certificates文件夹中读取的,而是从
中读取的
~/.dotnet/corefx/cryptography/x509stores/ca
因此该应用不信任任何证书。所以我从 this page 下载了 Google 的示例 PEM 文件,将其转换为 P7B 文件以确保整个证书链保持完整
openssl crl2pkcs7 -nocrl -certfile google-roots.pem -out google-roots.p7b
并在应用程序启动期间将整个链添加到框架的证书存储
using (var store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
options.CertificateChainFiles.ToList().ForEach(file =>
{
var collection = new X509Certificate2Collection();
collection.Import(file);
store.AddRange(collection);
});
}
将我的 asp.net-core web API 项目从 2.2 更新到 3.1 后,当我尝试通过 System.Net.Mail.SmtpClient
发送电子邮件时出现以下异常:
- 2020-02-09 14:40:26.8163 15 ( SmtpClient.OnSendCompleted => <>c__DisplayClass78_0.<SendMailAsync>b__0 => SmtpClient.HandleCompletion ) - Error Failed to send E-Mail. -- ( Exception: System.Net.Mail.SmtpException: Failure sending mail.
---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.
at System.Net.Security.SslStream.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, ExceptionDispatchInfo exception)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.PartialFrameCallback(AsyncProtocolRequest asyncRequest)
--- End of stack trace from previous location where exception was thrown ---
at System.Net.Security.SslStream.ThrowIfExceptional()
at System.Net.Security.SslStream.InternalEndProcessAuthentication(LazyAsyncResult lazyResult)
at System.Net.Security.SslStream.EndProcessAuthentication(IAsyncResult result)
at System.Net.Security.SslStream.EndAuthenticateAsClient(IAsyncResult asyncResult)
at System.Net.Mail.SmtpConnection.ConnectAndHandshakeAsyncResult.TlsStreamAuthenticateCallback(IAsyncResult result)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw(Exception source)
at System.Net.Mail.SmtpConnection.ConnectAndHandshakeAsyncResult.End(IAsyncResult result)
at System.Net.Mail.SmtpTransport.EndGetConnection(IAsyncResult result)
at System.Net.Mail.SmtpClient.ConnectCallback(IAsyncResult result)
--- End of inner exception stack trace ---
API 的端点是通过 appsettings.json
中的 Kestrel 部分配置的 "Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://myapidomain:80"
},
"Https": {
"Url": "https://www.myapidomain:443",
"Certificate": {
"Path": <path to pfx file>,
"Password": <password>
}
}
}
},
我就是这样使用 SmtpClient
:
using (var client = new SmtpClient(_options.MailServiceHost, _options.MailServicePort))
{
client.EnableSsl = true;
client.ClientCertificates.Add(_certificate);
client.DeliveryMethod = SmtpDeliveryMethod.Network;
client.UseDefaultCredentials = false;
client.Credentials = new NetworkCredential(_options.MailAccountName, _options.MailAccountPassword);
var mail = new MailMessage
{
From = new MailAddress(author),
Body = body,
Subject = subject,
Sender = new MailAddress(author),
SubjectEncoding = Encoding.UTF8,
BodyEncoding = Encoding.UTF8,
HeadersEncoding = Encoding.UTF8
};
mail.Headers.Add("Content-Type", "content=text/html; charset=\"UTF-8\"");
mail.To.Add(receiver);
mail.ReplyToList.Add(author);
_logger.LogInformation($"Sending mail to: {receiver}");
await client.SendMailAsync(mail);
return true;
}
以上代码在我切换回 aspnetcore2.2
版本时有效,所以我怀疑这是证书本身的问题(我对两者使用完全相同的 .pfx 文件)。另一个重要的提示可能是,当在 Windows 机器上使用自签名开发证书进行本地调试时,它实际上可以在 aspnetcore3.1
上运行。在生产环境中,如果上述代码失败,我会 运行 在 Ubuntu 上设置 API。
我的猜测是 asp.net-core 3.x 中证书的处理方式发生了变化。或者 Windows 库的行为不同。如果需要,我还可以 post 两个 Startup.cs 文件。
是否有关于实际原因或如何解决此问题的更多详细信息的建议?
更新
按照 Adam 的建议,我已切换到 MailKit 的 SmtpClient
实现。该错误在生产中仍然存在,但我设法使用 client.ServerCertificateValidationCallback
获得了有关该错误的更多详细信息。在检查回调的 ChainStatus
属性:
X509ChainStatusFlags: RevocationStatusUnknown
StatusInformation: unable to get certificate CRL
有人可以解释为什么无法获得证书 CRL 会导致我的连接失败吗?我该如何解决这个问题?
更新 2
此外,当我设置 client.CheckCertificateRevocation = false
时出现以下错误:
X509ChainStatusFlags: PartialChain
StatusInformation: unable to get local issuer certificate
Subject: CN=smtp.gmail.com, O=Google LLC, L=Mountain View, S=California, C=US
Issuer: CN=GTS CA 1O1, O=Google Trust Services, C=US
更新 3
似乎 netcore 框架找不到所需的 ca 证书,因为当我在 Ubuntu 机器上 运行 执行命令时,我收到与更新 2 中完全相同的错误消息:
openssl s_client -connect smtp.gmail.com:465
--> Verify return code: 20 (unable to get local issuer certificate)
但是,当我明确指定 ca 证书存储路径时验证通过。
openssl s_client -CApath /etc/ssl/certs/ -connect smtp.gmail.com:465
--> Verify return code: 0 (ok)
所以我想最后一个问题是:如何让我的 aspnet-core web API 找到 Ubuntu 的 ca 证书存储?
更新 4
我已经报告了这个问题 here,也许 dotnet 团队最终会解决这个问题。作为(希望)临时解决方法,我最终手动验证了证书的指纹。
因此,如果您查看 SmtpClient() 的文档,您会发现它现在已标记为已过时。我真的很惊讶你没有得到编译器错误,你应该有,但这可能是明天的问题。
Microsoft 推荐使用 MailKit 库。链接:Nuget - GitHub
我鼓励您使用 Mailkit 库而不是使用 System.Net.Mail.SmtpClient()
重新实现您的代码。除了不会过时之外,MailKit 还是一个非常易于使用的库,开发人员和贡献者投入了大量时间和思考。
使库过时并推荐 MailKit 的原因是
SmtpClient and its network of types are poorly designed
有趣的事实:.Net Core 1.0 无法访问 System.Net.Mail
命名空间,并且在之后添加了支持,但是当 System.Net.Mail
在 .NET Core 中可用时,MailKit
是 .NET Core 早期采用者的首选库。
我终于弄明白为什么它在 Linux 上停止工作了。 netcore3.1框架不是从OS的ca-certificates文件夹中读取的,而是从
中读取的~/.dotnet/corefx/cryptography/x509stores/ca
因此该应用不信任任何证书。所以我从 this page 下载了 Google 的示例 PEM 文件,将其转换为 P7B 文件以确保整个证书链保持完整
openssl crl2pkcs7 -nocrl -certfile google-roots.pem -out google-roots.p7b
并在应用程序启动期间将整个链添加到框架的证书存储
using (var store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
options.CertificateChainFiles.ToList().ForEach(file =>
{
var collection = new X509Certificate2Collection();
collection.Import(file);
store.AddRange(collection);
});
}