背景
因为本地开发时,内网的 https 是不安全的 https。
会导致 js 发不出请求。
为了让 https 安全,这里实现了本地 localhost 自动生成证书以及安装的过程。
写代码
生成证书使用的是 .NET 自带的库,不需要引入第三方包。
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
生成证书的方法参考 https://github.com/dotnetcore/FastGithub 项目。
第一步是编写一个证书生成器,其中,代码直接从这里复制: https://github.com/dotnetcore/FastGithub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGithub.HttpServer/Certs/CertGenerator.cs
然后,定义管理生成证书的服务,原版作者使用的是 .NET 7,而且当前稳定版本是 .NET 6,很多 API 不能使用,因此需要对其改造。原版地址:
https://github.com/dotnetcore/FastGithub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGithub.HttpServer/Certs/CertService.cs
定义证书位置和名称:
private const string CACERT_PATH = "cacert";
/// <summary>
/// 获取证书文件路径
/// </summary>
public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/https.crt" : $"{CACERT_PATH}/https.cer";
/// <summary>
/// 获取私钥文件路径
/// </summary>
public string CaKeyFilePath { get; } = $"{CACERT_PATH}/https.key";
这里涉及到两个文件,客户端证书和私钥。
.key
是私钥,可以通过私钥来生成服务端证书和客户端证书,因此这里只需要保存 .key
私钥,不需要导出服务器证书。
.csr
、.cer
是客户端证书,在 Windows 下可以使用 .cer
格式。导出客户端证书的原因是要安装证书,而且安装一次即可,不需要动态生成。
证书管理服务的规则是,如果 ssl
目录下没有证书,那么就生成并安装;如果发现文件已经存在,则加载文件到内存,不会重新安装。
完整代码如下:
/// <summary>
/// 证书生成服务
/// </summary>
internal class CertService
{
private const string CACERT_PATH = "cacert";
/// <summary>
/// 获取证书文件路径
/// </summary>
public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/https.crt" : $"{CACERT_PATH}/https.cer";
/// <summary>
/// 获取私钥文件路径
/// </summary>
public string CaKeyFilePath { get; } = $"{CACERT_PATH}/https.key";
private X509Certificate2? caCert;
/*
本地会生成 cer 和 key 两个文件,cer 文件导入到 Window 证书管理器中。
key 文件用于每次启动时生成 X509 证书,让 Web 服务使用。
*/
/// <summary>
/// 生成 CA 证书
/// </summary>
public bool CreateCaCertIfNotExists()
{
if (!Directory.Exists(CACERT_PATH)) Directory.CreateDirectory(CACERT_PATH);
if (File.Exists(this.CaCerFilePath) && File.Exists(this.CaKeyFilePath))
{
return false;
}
File.Delete(this.CaCerFilePath);
File.Delete(this.CaKeyFilePath);
var notBefore = DateTimeOffset.Now.AddDays(-1);
var notAfter = DateTimeOffset.Now.AddYears(10);
var subjectName = new X500DistinguishedName($"CN=痴者工良");
this.caCert = CertGenerator.CreateCACertificate(subjectName, notBefore, notAfter);
var privateKeyPem = ExportRSAPrivateKeyPem(this.caCert.GetRSAPrivateKey());
File.WriteAllText(this.CaKeyFilePath, new string(privateKeyPem), Encoding.ASCII);
var certPem = ExportCertificatePem(this.caCert);
File.WriteAllText(this.CaCerFilePath, new string(certPem), Encoding.ASCII);
return true;
}
/// <summary>
/// 获取颁发给指定域名的证书
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
public X509Certificate2 GetOrCreateServerCert(string? domain)
{
if (this.caCert == null)
{
using var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(this.CaKeyFilePath));
this.caCert = new X509Certificate2(this.CaCerFilePath).CopyWithPrivateKey(rsa);
}
var key = $"{nameof(CertService)}:{domain}";
var endCert = GetOrCreateCert();
return endCert!;
// 生成域名的1年证书
X509Certificate2 GetOrCreateCert()
{
var notBefore = DateTimeOffset.Now.AddDays(-1);
var notAfter = DateTimeOffset.Now.AddYears(1);
var extraDomains = GetExtraDomains();
var subjectName = new X500DistinguishedName($"CN={domain}");
var endCert = CertGenerator.CreateEndCertificate(this.caCert, subjectName, extraDomains, notBefore, notAfter);
// 重新初始化证书,以兼容win平台不能使用内存证书
return new X509Certificate2(endCert.Export(X509ContentType.Pfx));
}
}
private static IEnumerable<string> GetExtraDomains()
{
yield return Environment.MachineName;
yield return IPAddress.Loopback.ToString();
yield return IPAddress.IPv6Loopback.ToString();
}
internal const string RasPrivateKey = "RSA PRIVATE KEY";
private static string ExportRSAPrivateKeyPem(RSA rsa)
{
var key = rsa.ExportRSAPrivateKey();
var chars = PemEncoding.Write(RasPrivateKey, key);
return new string(chars);
}
private static string ExportCertificatePem(X509Certificate2 x509)
{
var chars = PemEncoding.Write(PemLabels.X509Certificate, x509.Export(X509ContentType.Cert));
return new string(chars);
}
/// <summary>
/// 安装ca证书
/// </summary>
/// <exception cref="Exception">不能安装证书</exception>
public void Install( )
{
var caCertFilePath = CaCerFilePath;
try
{
using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
var caCert = new X509Certificate2(caCertFilePath);
var subjectName = caCert.Subject[3..];
foreach (var item in store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false))
{
if (item.Thumbprint != caCert.Thumbprint)
{
store.Remove(item);
}
}
if (store.Certificates.Find(X509FindType.FindByThumbprint, caCert.Thumbprint, true).Count == 0)
{
store.Add(caCert);
}
store.Close();
}
catch (Exception ex)
{
throw new Exception($"请手动安装CA证书{caCertFilePath}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”" + ex);
}
}
}
}
在 ASP.NET Core 中使用
在 ASP.NET Core 中加载服务端证书(每次启动时生成 X509 证书)。
var sslService = new CertService();
if(sslService.CreateCaCertIfNotExists())
{
try
{
sslService.Install();
}
catch (Exception)
{
}
}
var webhost = WebHost.CreateDefaultBuilder()
.UseStartup<Startup>()
.UseKestrel(serverOptions =>
{
serverOptions.ListenAnyIP(39999,
listenOptions =>
{
var certificate = sslService.GetOrCreateServerCert("localhost");
listenOptions.UseHttps(certificate);
});
})
.Build();
await webhost.RunAsync();
文章评论