WCF安全之EndPointIdentity
最近在做一个项目,应用了WCF进行分布式开发,中间还涉及到消息路由器等,好在有WCF提供了强大的基础支持,当然,本身也作了不少的扩展,实际,我 最关心的是WCF的安全问题,网上不少朋友介绍的WCF的安全也是少得可怜,微软发布的WCF Security GUID好像讲得也只是入门级别的教程,离真正应用到项目中还是有很大的距离,这也让我萌发了分享的想法,今天先放出来占个位置吧,有反对的朋友砖头轻 点,呵~,可以告诉你,WCF的安全里,有很多的小秘密,当然还是要告诉你,并且有此小秘密是要自己去体验后才知道,在博客排版方面,李会军(军哥)让人 感觉最舒服,在解说方面,军哥也是以简洁著称,我在这里也学习一下,一起简洁吧,我希望以后的WCF安全探讨里,一次只讲一个小内容好了~
概述
Windows Communication Foundation (WCF) 是 Microsoft 为构建面向服务的应用程序而提供的统一编程模型(摘自MSDN),在分布式环境下的安全问题尤为重要,如果你觉得使用了WCF默认的安全措施可以让你高枕 无忧,那明天你可就以回家种田了,当然,对于学习来说,足够了~,但我们讲的是真正的项目应用,WCF在各种协议下的安全提供和保证是不尽相同的。
背景
故事发生在一个阳光明媚的下午,一名女子为了混入某小区行窃,将上次偷到的管道维修工作牌别在胸前,当她走近管理员身边时,被管理员一把抓个正着,原来这小区从上次失窃事件后,已经将维修队解散,现在维修都是由管理员联系外部人员,自然也不用别什么工作牌了。
问题呈现
1、许多朋友对这个EndPointIdentity相当的不屑顾,千万不要小看它呀,有时候你被wcf弄生弄死的时候还不知道为什么,这次你应该看清楚了。当你新建一个WCF服务类库时,正确的EndPointIdentity声明如下
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
说实现,EndPointIdentity这东西在革命初期(wcf初建立时),我觉得它就像是人一盲肠,多了它也没啥用,少了它也不觉得碍事,你不信?删了试试,你要真删除了其实也没什么。
2、客户端如果引用了服务元数据,生成的EndPointIdentity和服务器端的一模一样,不信你自己看,实际上,你也可以把它删除了(革命初期),对服务调用没啥影响。
3、在你的绑定中,安全选项为None的时候,你想怎么弄它就怎么弄它,不要紧,随便改。如下
<security mode="None">
<transport clientCredentialType="Windows" protectionLevel="EncryptAndSign"/>
<message clientCredentialType="Certificate"/>
</security>
</binding>
4、一旦你将安全策略调整为:Transport(传输安全)
<transport clientCredentialType="Windows" protectionLevel="EncryptAndSign"/>
<message clientCredentialType="Certificate"/>
</security>
5、如果你的服务还是像初期一样没啥改动,仅是启用了传输安全,clientCredentialType的凭据类型也只是windows,没有使用证书,那可以肯定,你的服务没啥问题出现。
6、不过这时候,你有一天不小心的将EndpointIdentity中的dns元素值误删除了1个字 ,我敢肯定,你的恶梦才刚开始,这时候,你再调用服务,将会收到一个异常。
================
未处理 System.ServiceModel.Security.SecurityNegotiationException
Message="服务器已拒绝客户端凭据。"
=================
非常限的提示,完全找不到任何线索,可能那时你还以为是不小心设置了某种安全策略哩或者是证书什么的没匹配呢。
正题
IdentityElement类
表示一个配置元素,该配置元素使其他终结点在与该终结点交换消息时可以对其进行身份验证。此类不能被继承。
EndpointIdentity类
一个 abstract 类,实现此类时可提供一个标识,与终结点交换消息的客户端可使用该标识对终结点进行身份验证。
1、这两兄弟其实是同一人,起到的作用都是一样,只不过一个是作用于配置文件,一个作用于托管代码中。利用EndpointIdentity来向服务器标识自己是合法的调用,共有6种方式。
(1) 分别是dns、certificate、rsa、servicePrincipalName(spn)、userPrincipalName(upn)、certificateReference(x509证书标识),通常情况下,只采用一种,也是最常用的dns标识方式,当然,我不反对你6种方式一起使用,如何你有需要。
服务建立时默认的标识符为:dns,并且其值为:localhost。
(2)如果你的服务器dns值为localhost,客户端的dns与之不匹配,所抛出的异常描述就是上面的红色字体内容。
(3)如果服务器dns值已被更改(这个更改当然不是你改改配置文件中的值就行的),通常情况下,抛出的异常都会告诉你明确的不匹配,不匹配在哪里,预期是什么,实际是什么,那你改起来就费事了。
2、现在我们再把安全策略调整到消息级别,试试。
//客户端配置
<binding name="EndpointBinding">
<security mode="Message">
<transport clientCredentialType="Windows" protectionLevel="EncryptAndSign"/>
<message clientCredentialType="Certificate"/>
</security>
</binding>
</netTcpBinding>
</bindings>
<client>
<endpoint address ="net.tcp://localhost:8799/UserService/UserData"
binding="netTcpBinding"
contract="Client.References.IUserData"
bindingConfiguration="EndpointBinding"
behaviorConfiguration="UserDataBehavior">
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
</client>
//服务器配置
<binding name="EndpointBinding">
<security mode="Message">
<transport clientCredentialType="Windows" protectionLevel="EncryptAndSign"/>
<message clientCredentialType="Certificate"/>
</security>
</binding>
</netTcpBinding>
</bindings>
<services>
<service name="UserService.UserData" behaviorConfiguration="UserDataBehavior">
<host>
<baseAddresses>
<add baseAddress = "net.tcp://localhost:8799/UserService" />
</baseAddresses>
</host>
<endpoint address ="UserData"
binding="netTcpBinding"
contract="UserService.IUserData"
bindingConfiguration="EndpointBinding">
</endpoint>
</service>
看看配置文件,你发现了什么?是的,服务器端的标识被删除,客户端的标识还是dns并且值为localhost,调用服务抛出异常:
==========================
传出消息标识检查失败。所预期的远程终结点的 DNS 标识为“localhost”,但是远程终结点提供的 DNS 请求为“192168168151service”。如果此远程终结点合法,您可以通过在创建通道代理时明确地将 DNS 标识“192168168151service”指定为 EndpointAddress 的“标识”属性来解决此问题。
==========================
在这里,我们忽略了一个事实,当你在服务中将安全策略调整了消息级别安全时,服务必须配置x509证书,正所谓你叫天不应,叫地不灵啊,这时候EndpointIdentity跑出来搞乱了,明明服务器默认是dns标识,值为:loclahost。为什么突然跑出来个“192168168151service”呀?我也很想知道,原来,在服务配置证书后,默认的dns将被替换为证书主题,只要你把dns配置改回来,一切又没问题了。
新问题
这时候突然冒出来一个新的问题,如果有多个服务器的时候怎么办呀?多个服务器,多半会伴随着路由器的出现(这只是一种假设,与业务有关),我也很想知道,有多个的时候的情况。
解决它
答案是通过代码动态创建一个EndpointIdentity,代码比较简单,如下:
EndpointIdentity identity = EndpointIdentity.CreateDnsIdentity("192168168151service");
AddressHeaderCollection headers = client.Endpoint.Address.Headers;//如果需要,还可以在这里加入自定义的头消息
Uri uri = client.Endpoint.Address.Uri;
EndpointAddress remoteaddress = new EndpointAddress(uri, identity, headers.ToArray());
UserDataClient newClient = new UserDataClient(client.Endpoint.Binding, remoteaddress);
newClient.ClientCredentials.ClientCertificate.Certificate = client.ClientCredentials.ClientCertificate.Certificate;
client.Abort();//关闭旧通道,当然,这里你还可以用通道工厂创建对象,实际上也很简单
string msg = newClient.GetData(50);
===========================================================
后话