前言
话说又来需求了,之前对于在SelfHost中需要嵌套页面并操作为非正常需求,这回来正常需求了,客户端现在加了https,老大过来说WebAPi访问不了了,这是什么情况,我去试了试,还真是这个情况,不知道如何下手啊,最终为了解决这个问题,漫长的探索之旅就这样开始了,希望给需要在SelfHost下启动Https的童鞋一点启示和帮助。
话题引入
当在客户端发送请求发送不过去时看了看谷歌控制台显示的消息大意是不能混合使用即客户端为https,则WebAPi不能为http,估计是为了安全的问题,于是乎问题就比较明朗了,只需要将WebAPi修改成https即可,直接将SelfHost中地址修改为Https显然是不行的必须要借助证书并开启对应的端口才行,既然我们已经分析完毕,接下来我们解决WebAPi启动Https的问题即可。开始想到去申请的一个免费的证书就行,但是由于是将WebAPi安装在本地Windows服务中且ip不固定,所以免费的证书则不再可取,只能自行创建证书来解决这个问题。但是我对于证书几乎从未接触过,一无所知,不知从哪里下手,这一漫长的过程就从此开始。
解决SelfHost启动Https
在园中搜索有关自创建证书的答案,基本要么是IIS,要么是WCF创建证书,不过还是有了一点思路,提供了自创建证书需要的命令,开始有一点思绪。当看到如下一条命令时我要惊呆了,难道就这样解决了吗,so easy!
第一次尝试
http add urlacl url=https://+:port/ user=""
结果发现这只是url的保留项而已。
第二次尝试
我们通过VS开发命令来创建证书
第一步:
MakeCert -sv d:DevelopmentCA.pvk -n "CN=WebAPi CA" d:DevelopmentCA.cer -b 09/20/2016 -e 12/31/2050 -r
【注意】:该命令最后一个-r不能缺少,-r是创建自创建证书的标识,否则当利用Pvk2pfx创建私钥时则会出现【ERROR: File not found.(Error Code = 0x80070002)】
结果变成如下:
第二步:
打开MMC将创建的证书导入到受信任的颁发证书机构中,则变成如下这样:
第三步:
拿到该证书的指纹并监听ip以及端口,通过如下命令进行:
http add sslcert ipport=0.0.0.0:8084 certhash=996645BAB7169F2AFD7599A696DA2586862843C6 appid={41992502-E5D4-4794-BB01-D4A7414480CC}
一切都是如此的完美,最后看下结果,再次让我大失所望:
此时我已经处在崩溃的边缘,搜索这个原因的答案,都无法解决,此时只能求助万能的stackoverflow,看到这个答案令人惊喜一番:
http://stackoverflow.com/questions/13076915/ssl-certificate-add-failed-when-binding-to-port
大意是将自创建的证书要通过MMC首先导入到【个人】证书中才行,于是乎将其拖到个人证书中看看。
结果依然是这个错误,当回过来再看上述所有回答答案时,下面一个回答简直是拯救了我,这个问题困扰我一天,让我无比激动。请看这句话:
You can easily check if your certificate has private a key as so:
mmc
-certificates
-local machine
-personal
. Look at the icon of the certificate - it MUST have key sign on the icon.
还要在本地计算中的个人证书中看自创建的证书左上角是否有个小钥匙,这个钥匙也就是私钥,我们还得创建私钥才行,通过如下命令进行。
第四步:
创建证书的私钥
Pvk2pfx -pvk DevelopmentCA.pvk -spc DevelopmentCA.cer -pfx DevelopmentSSL.pfx -po password
接下来我们再来运行第三步则成功如下:
我们接下来运行程序https:localhost:8084来验收成果,结果如下:
那么这个问题又该如何解决呢?可能有些人就得说了,点击下面的高级直接前往不就可以了吗,虽然这样式可以接受,但是在我们项目中,是通过【设备】去访问WebAPi,基于这点绝对不可行,so must kill it。当我各种尝试后发现这样做却可以。我们将 MakeCert -sv d:DevelopmentCA.pvk -n "CN=WebAPi CA" d:DevelopmentCA.cer -b 09/20/2016 -e 12/31/2050 -r 中的CN修改为localhost即颁发给localhost时却可以,重复性的动作则不再演示,给演示最终结果如下:
注意:当到这里时如果你还是发现证书是无效时请进行如下操作,参考资料来源于搜索时来自于youtube的一段视频演示,下面请看:
将演示最后中【允许将标识符用于受保护内容(可能需要重新重启计算机)】去掉即可。
到了这里关于在WebAPi之SelfHost启动Https的问题基本上解决了一大半,对于我来说,对于你来说可能已经Over,但是在实际场景中却还没完成。当在测试时发现如上压根不会请求到WebAPi,此时在谷歌控制台却出现如下的错误:
::Net_Error_Response
第三次尝试
解决客户端无法访问localhost。
百思不得其解,搜索资料时以为是跨域的问题,结果却不是,在客户端那边是用的WebAPi的本地IP来访问,所以想想是不是localhost不行呢。本想偷点懒,在Hosts文件夹里对localhost进行映射,结果依然不行,于是乎访问地址变成:https:192.168.3.6:8084最终完事。
到了这里也就完成了90%,个人还不是很满足,寻思着虽然有一点是绕不过去,那就是首先得将证书导入【受信任的颁发机构中】,但是还需要通过MMC将证书导入个人证书,这一点是我无法接受,我继续开始探索之旅。通过代码的形式将证书导入到MMC的【个人】证书中。
第四次尝试
通过代码形式将证书导入到【个人】证书中,而非通过MMC导入寻求解决方案。
通过运行如下代码来尝试创建访问https证书:
var fileName = AppDomain.CurrentDomain.BaseDirectory + "DevelopmentSSL.pfx"; var cert = new X509Certificate2(fileName, "password"); var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); store.Open(OpenFlags.ReadWrite); store.Add(cert); store.Close(); Process p = new Process(); p.StartInfo.FileName = "netsh.exe"; p.StartInfo.Arguments = string.Format("http add sslcert ipport=0.0.0.0:8084 certhash={0} appid={1}", cert.GetCertHashString(), "{" + "41992502-E5D4-4794-BB01-D4A7414480CC"+"}"); p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.Start(); p.Close();
此时通过如下命令来查看https是否已经开启:
netsh http show sslcert
结果未看到显示开启的端口,此时想或许是否是权限不够呢?于是将启动进程的身份运行以【管理员】身份运行,在上述启动进程中添加如下代码:
p.StartInfo.Verb = "runas";
此时并未有什么影响,需要开启的端口依然岿然不动,将WebAPi寄宿在Windows服务中,我们是利用批处理来进行,于是尝试利用批处理部署一下来看看,看下演示结果:
利用批处理则圆满完成任务并满足要求,不知道为何直接并用管理员身份运行不可,或许是权限还是不够吧,至少还是找到了一种可行的解决方案!
无法正确返回结果
当一切准备就绪时,前端又反应返回的结果不正确不能解析,一波未平一波又起,继续fighting,在控制台如下显示:
这个结果闻所未闻,居然出现一个【 k__BackingField 】 字段,经过查询相关资料得出:在WebAPi中默认是利用JSON.Net中的【 DefaultContractResolver 】来解析对象,当对解析对象上添加【 Serializable 】对象时则会造成上述原因,例如:
[Serializable] public class Person { public int Age { get; set; } }
此时应将【Serializable】特性去除或者在全局配置添加如下语句才能正确显示需要的结果,来忽略默认特性:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new DefaultContractResolver { IgnoreSerializableAttribute = true };
【建议】:无论是有无添加上述序列化特性,建议都在全局配置添加上述忽略默认序列化特性。
参考资料:
http://stackoverflow.com/questions/29701891/k-backingfield-remove-in-c-sharp-seen-via-swashbuckle-swagger
http://stackoverflow.com/questions/35259333/jsonmediatypeformatter-formatting-with-k-backingfield
这一切是不是就这样完了呢?任务算是完成了,为了对证书有更加深入的理解,我们来拓展一下证书知识。
证书知识扩展
两个服务器之间是如何利用证书来进行相互之间的信任呢?请看下图:
在如上图中,管理员通过交换双方之间的公钥中的指纹来建立二者服务器之间的信任关系。对于证书上述我们也已经演示在.NET中通过【 X509Certificate2 】来实现,下面我们来看看这个类。
第一点:证书和PKCS #12/PFX文件的不同
X509Certificate2此类有两个属性即Public Key(公钥)和Private Key(私钥),当我们导入证书时可以是否导出该私钥,在Windows中典型的证书是以扩展名.cer结尾,当然它没有包含私钥。
下面我们可以这样导出一个证书:
var fileName = AppDomain.CurrentDomain.BaseDirectory + "DevelopmentCA.cer"; var cert = new X509Certificate2(fileName); File.WriteAllBytes(@"d:Hello.cer", cert.Export(X509ContentType.Cert));
有时我们需要导出私钥,此时私钥及扩展名.Pfx结尾在另外一个文件,可以通过如下导出:
File.WriteAllBytes("Hello.pfx", cert.Export(X509ContentType.Pkcs12, (string)null));
Hello.pfx实际上就是一个PKCS#12文件,它可以作为一个单独文件来储存需要加密对象,做普遍的用途当然也就是用X509Certificate2来存储私钥,有关更多知识请参考:
https://en.wikipedia.org/wiki/PKCS_12
第二点:证书存储
关于这点上述也已经演示,我们通过MMC打开的是控制台证书管理器,可以将当前用户或本地计算机账户导入其中,若只是想看当前用户证书则可以通过certmgr.msc来打开。那么通过代码形式如何进行呢?如下:
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); store.Add(certificate); store.Close();
可以将证书导入到通过StoreLocation.CurrentUser映射到当前用户,通过StoreName.My映射到当前用户个人证书中,也可以是本机计算机账户中的其他机构中通过上述枚举即可。此时则会添加如下注册表中
HKEY_CURRENT_USER\SOFTWARE\Microsoft\SystemCertificates
或者通过桌面路径
C:\Users\username\AppData\Roaming\Microsoft\SystemCertificates\My\Certificates
当然在本地计算中则是如下路径:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SystemCertificates
第三点:理解私钥
我们上述已经叙述过证书中是不带私钥,私钥时单独作为一个文件来使用,那么私钥到底存储在什么地方呢?我们给出如下代码:
var fileName = AppDomain.CurrentDomain.BaseDirectory + "DevelopmentSSL.pfx"; var cert = new X509Certificate2(fileName,"password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); store.Open(OpenFlags.ReadWrite); store.Add(cert); store.Close();
此时私钥会存储在如下注册表中:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SystemCertificates\MY\Keys
如果我们进行如下修改将MachineKeySet修改为UserKeySet:
var fileName = AppDomain.CurrentDomain.BaseDirectory + "DevelopmentSSL.pfx"; var cert = new X509Certificate2(fileName,"password", X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); store.Open(OpenFlags.ReadWrite); store.Add(cert); store.Close();
此时则会存储在如下地方:
C:\Users\username\AppData\Roaming\Microsoft\SystemCertificates\My\Keys\
此时可能会出现一点问题,当将证书导入并供本地计算机去使用,但是此时私钥却个人用户文件夹中,要是计算机中的其他账户想要访问这个私钥可能没有这个权限或者说得不到这个私钥。
关于枚举X509KeyStorageFlags的几点说明:
(1)Exportable :创建证书时指定它可以用于备份私钥。
(2)PersistKeySet:创建证书时指定它可以导入一次并使用多次。
(3)UserKeySet:创建证书时指定它可以在另外一个账户使用它。
(4)MachineKeySet:创建证书时指定它可能导致其他账户没有权限或者访问不到私钥导致该私钥不存在。
第四点:创建证书时谨慎
谨慎导出证书利用字节数组 var certificate = new X509Certificate2(bytes); 此时会将文件写到临时文件夹中,此时有可能临时文件夹中有关此文件不会得到有效的清理,导致临时文件夹膨胀。
工具介绍
如下网址这里可以看到通过MakeCert程序来创建证书已经被弃用(在PowerShell4.0之前我们可以下载MakeCert来自创建证书)。
https://msdn.microsoft.com/library/windows/desktop/aa386968.aspx
现在创建的证书可以通过PowerShell来进行(不过系统在Windows 8 或者Windows Server 2012或者Windows 8.1或者Windows Server 2012 R2),有关此命令的介绍详情请见如下网址:
https://technet.microsoft.com/library/hh848633
看起来好像很难,实则比MakeCert命令更加简洁明了,我们来看看(需要切换到PowerShell)。
(1)利用如下命令来创建证书并获取到其指纹
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname localhost
颁发给localhost,并将其保存到本地计算机中的【个人】证书下,结果得到如下:
(2)需要导出证书时,需要用一个变量来保存密码
$pwd = ConvertTo-SecureString -String "Pa$$w0rd" -Force -AsPlainText
(3)导出pfx,指定第一步获取到的指纹和第二步保存的密码
Export-PfxCertificate -cert cert:\localMachine\my\CE0976529B02DE058C9CB2C0E64AD79DAFB18CF4 -FilePath d:cert.pfx -Password $pwd
总结
本节到这里算是完全结束了,将在WebAPi使用过程中遇到的问题到一并叙述了一遍,其中整个做完花费了三天时间,写这篇博客花费了一天,很久没有坐着花费接近一天的精力来写一篇博客,不过确确实实涨了不少知识,文中有关内容若有不妥之处,欢迎批评,同时也为了后续让其他需要用到的童鞋少走点坑也是值得的,当然这里在WebAPi中我们也需要认证客户端是否已经采用ssl加密证书,通过继承DelegatingHandler来进行处理,例如如下:
var uri = new UriBuilder(request.RequestUri); uri.Scheme = Uri.UriSchemeHttps; uri.Port = _httpsPort; //TO DO
在这里非常感谢园友【幻天芒】,遇到难题解决不了或是没什么思路都在向他请教,感谢他的不厌其烦和指导,再次表示感谢。在这里也提前预祝各位园友国庆快乐。
参考资料
http://stackoverflow.com/questions/29701891/k-backingfield-remove-in-c-sharp-seen-via-swashbuckle-swagger
http://stackoverflow.com/questions/779228/the-parameter-is-incorrect-error-using-netsh-http-add-sslcert?answertab=votes
http://southworks.com/blog/2014/06/16/enabling-ssl-client-certificates-in-asp-net-web-api/
http://stackoverflow.com/questions/28854466/makecert-exe-error
http://stackoverflow.com/questions/6307886/how-to-create-pfx-file-from-certificate-and-private-key/18704221#18704221
http://paulstovell.com/blog/x509certificate2
http://stackoverflow.com/questions/35259333/jsonmediatypeformatter-formatting-with-k-backingfield
http://www.thewindowsclub.com/disable-insecure-content-warning-chrome
http://windowsitpro.com/blog/creating-self-signed-certificates-powershell