品高工作流的SOA服务使用WCF技术部署,使用Windows Services宿主,本文主要是讲消息安全模式下的自定义UserNamePassword身份验证方式,基于wsHttpBinding绑定协议。主要内容:制作证书、服务验证代码、服务端配置、客户端配置、证书放置位置详解、总结等6个部分。
序、消息安全模式之UserName客户端身份验证(参考:老徐的博客):
消息安全模式之UserName客户端身份验证需要服务器需要一个有效的可用于TLS 加密和向客户端验证服务身份的 X.509 证书,并且客户端必须信任此服务器证书。 这里使用http协议。建议安全上下文以后,使用共享安全上下文对SOAP消息进行加密和签名。但是采用UserName客户端身份验证。也就是客户端提供用户名和密码才可以访问此服务。
1.身份验证(服务器):提供证书,(使用 HTTP)用于初始会话协商和证明服务身份。
2.身份验证(客户端):客户端使用用户名和密码进行身份验证
WCF消息安全模式之UserName客户端身份验证的架构如下:
客户端建立TLS安全上下文以后,会使用商定的密码对消息签名,客户端使用证书加密数据,服务端使用证书解密数据,保证数据的安全和机密性,消息签名放置被篡改。
这里客户端提供的是有效的自定义的用户名和密码,服务器端进行验证。
下面需要制作一个服务端使用的证书,用来建立TLS传输安全层会话使用。
一、制作证书:
(1)使用makecert 工具:Microsoft Visual Studio 2008-->Visual Studio Tools-->Visual Studio 2008 命令提示行。输入:makecert -r -pe -n "CN=WCFServerCA" -sr LocalMachine -ss My -sky exchange
(2)开始--运行--MMC--控制台--添加删除单元--证书--当前用户和计算机各添加一个。能查看和管理CurrentUser和LocalMachine的所有证书。如图:
(3)将个人里面的WCFServerCA证书用MMC的导出功能导出为带私钥的pfx文件,保存的时候需要输入证书的密码,然后再一一从pfx导入受信任的根证书颁发机构和受信任人每次导入都需要输入密码(注:一定要从pfx文件导入,不能使用mmc的复制粘贴证书功能,不然Windows Services服务启动WCF的时候会异常,异常信息如下:
System.InvalidOperationException: Cannot find the X.509 certificate using
the following search criteria: StoreName 'My', StoreLocation 'LocalMachine',
FindType 'FindBySubjectName', FindValue 'WCFServerCA'.
二、服务验证代码:
证书制作完整以后,就需要来实现自定义用户名和密码的验证程序。这里要重写MyUserNamePasswordValidator类的Validate(string userName, string password)方法。具体代码如下:
public class MyUserNamePasswordValidator : UserNamePasswordValidator
{
public override void Validate( string userName, string password)
{
if (userName != " lefay " || password != " 12345678 " )
{
Console.WriteLine( " UserNamePassword Validatation is failed !:{0} " , userName);
throw new SecurityTokenException( " Unknown Username or Password " );
}
else
{
Console.WriteLine( " UserNamePassword Validatation is sucessfully !:{0} " , userName);
}
}
}
这里假定用户名是lefay,密码是12345678。如果不对就直接抛出异常,验证失败,实际应用我们可以到身份验证数据库查询数据,来判定用户名和密码的有效性。
三、服务端配置:
托管宿主的配置较为复杂,除了要配置服务端证书,还要配置绑定的安全模式,以及自定义身份验证程序集。
(1)配置服务端证书:
设置刚才制作的证书,配置文件如下:
< serviceCredentials >
< serviceCertificate storeName ="TrustedPeople" x509FindType ="FindBySubjectName"findValue ="WCFServerCA" storeLocation ="LocalMachine" />
< clientCertificate >
< authentication certificateValidationMode ="PeerTrust" />
</ clientCertificate >
< userNameAuthentication userNamePasswordValidationMode ="Custom"
customUserNamePasswordValidatorType ="WCFService.MyUserNamePasswordValidator,WCFService" />
</ serviceCredentials >
注:我们将从受信任人(TrustedPeople)里面去查找证书,certificateValidationMode设置为PeerTrust(如果证书位于被信任的人的存储区中,则有效)用于Windows Services宿主运行时查找证书的安全策略。
根据主题名称到当前用户的查找证书。还可以根据证书指纹等方式查找。
(2)配置绑定的安全模式:
这里使用的绑定要支持消息安全模式的自定义用户名和密码,配置代码如下:
< wsHttpBinding >
< binding name ="MessageAndUserName" >
< security mode ="Message" >
< transport clientCredentialType ="None" />
< message clientCredentialType ="UserName" />
</ security >
</ binding >
</ wsHttpBinding >
消息安全模式启用UserName验证方式。
(3)配置自定义身份验证程序集:
下面我们来配置一下自定义实现的身份验证程序集,配置代码如下:
< userNameAuthentication userNamePasswordValidationMode ="Custom"customUserNamePasswordValidatorType ="WCFService.MyUserNamePasswordValidator,WCFService" />
这里要注意,前面是命名空间和类,逗号后面就是服务名称。
(4)配置服务端service节点:
< service behaviorConfiguration ="WCFService.WCFServiceBehavior"
name ="WCFService.WCFService" >
< endpoint
address ="" bindingNamespace ="http://www.bingosoft.net/lefay/"
binding ="wsHttpBinding" bindingConfiguration ="MessageAndUserName" contract ="WCFService.IWCFService" >
< identity >
<!-- dns value的值设置为:WCFServerCA,就是证书的名称 -->
< dns value ="WCFServerCA" />
</ identity >
</ endpoint >
< endpoint address ="mex" binding ="mexHttpBinding" contract ="IMetadataExchange" />
< host >
< baseAddresses >
< add baseAddress ="http://localhost:8001/WCFService" />
</ baseAddresses >
</ host >
</ service >
注:endpoint节点下我们增加了identity节点,dns值为证书的名称,如果删除identity节点,则客户端更新服务应用的时候,会自动生成证书的base64编码,这里我们使用了简短的证书名称。
(5)服务代码:
这里的服务代码很简单,string HelloMessageSecurity(string name);会根据传入的名称来答应消息。代码如下:
// 1.服务契约
[ServiceContract(Namespace = " http://www.bingosoft.net/lefay/ " )]
public interface IWCFService
{
// 操作契约
[OperationContract]
string HelloMessageSecurity( string name);
}
// 2.服务类,继承接口。实现服务契约定义的操作
// 2.服务类.单调服务
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, Namespace = " http://www.bingosoft.net/lefay/ " )]
public class WCFService : IWCFService
{
// 实现接口定义的方法
public string HelloMessageSecurity( string name)
{
// 提供方法执行的上下文环境
OperationContext context = OperationContext.Current;
// 获取传进的消息属性
MessageProperties properties = context.IncomingMessageProperties;
// 获取消息发送的远程终结点IP和端口
RemoteEndpointMessageProperty endpoint = properties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
Console.WriteLine( string .Format( " Hello {0}, You are from {1}:{2} " , name, endpoint.Address, endpoint.Port));
return string .Format( " Hello {0},You are from {1}:{2} " , name, endpoint.Address, endpoint.Port);
}
}
四、客户端配置:
由于需要客户端免装证书,所以需要增加behavior配置,behavior配置代码如下所示:
< behaviors >
< endpointBehaviors >
< behavior name ="ClientWithoutCABehavior" >
< clientCredentials >
< serviceCertificate >
<!-- certificateValidationMode设置为None,测试过PeerTrust,会异常 -->
< authentication certificateValidationMode ="None" />
</ serviceCertificate >
</ clientCredentials >
</ behavior >
</ endpointBehaviors >
</ behaviors >
对应的endpoint配置代码如下所示:
< endpoint address ="http://localhost:8001/WCFService" behaviorConfiguration ="ClientWithoutCABehavior"
binding ="wsHttpBinding" bindingConfiguration ="WSHttpBinding_IWCFService"
contract ="ClientProxy.IWCFService" name ="WSHttpBinding_IWCFService" >
< identity >
<!-- dns value设置为WCFServerCA,需要和服务端的证书名称一样 -->
< dns value ="WCFServerCA" />
</ identity >
</ endpoint >
注:这里的dns值需要和服务端的证书名称一致(服务端设置好dns为证书名称时,更新服务引用时将会自动生成该值),如果需要跨机器访问服务端,需要将address的ip地址换成服务端的ip地址,如:http://192.168.0.119:8001/WCFService。
运行托管宿主,客户端添加服务引用,直接生成相关代码文件。我们直接添加代码测试,这里要测试两种情况,正确的用户名和密码,错误的用户名或者密码。前者成功。客户端测试代码如下:
// 1.WSHttpBinding_IWCFService
using (ClientProxy.WCFServiceClient ClientProxy = new ClientProxy.WCFServiceClient( " WSHttpBinding_IWCFService " ))
{
string strUserName = " lefay " ;
string strMessage = "" ;
// 消息安全模式之自定义用户名密码需要以下两个属性中输入正确的用户名和密码
ClientProxy.ClientCredentials.UserName.UserName = " lefay " ;
ClientProxy.ClientCredentials.UserName.Password = " 12345678 " ;
strMessage = ClientProxy.HelloMessageSecurity(strUserName);
Console.WriteLine(strMessage);
}
// For Debug
Console.WriteLine( " Press any key to continue... " );
Console.Read();
启动托管宿主,然后启动客户端调用服务进行测试,运行结果如图:
五、证书存储位置选择详解 (摘自:CrazyBird_技术点滴)
有朋友也许认为,证书存储位置一般不都是照搬微软的那一套嘛,其实不然,有以下几种情况是我们在真正的项目应用中不容忽视的。
1、开发模式:开发模式时,为方便我们一般都采用控制台作为WCF服务的宿主,意味着当前运行的帐户权限为当前账户,估计多半是管理员权限了,这个时候你是不用担心你的证书存储在哪里的,只要你的证书生成了就好,实际真的就是这么回事,当你将你的WCF安全配置更改一下,看代码了:
< serviceCredentials >
< clientCertificate >
< authentication certificateValidationMode ="PeerTrust" />
< clientCertificate >
< serviceCredentials >
如果你的证书现在不受信任,如果你还过得了服务,你就牛X了。 证书authentication (服务器与客户端)共有四种:
None = 未执行任何证书验证
PeerTrust = 如果证书位于被信任的人的存储区中,则有效
ChainTrust = 如果该链在受信任的根存储区生成证书颁发机构,则证书有效
PeerOrChainTrust = 如果证书位于被信任的人的存储区或该链在受信任的根存储区生成证书颁发机构,则证书有效
Custom = 用户必须插入自定义
实际上,只要你把信任模式更改为:None,证书放在哪里都无所谓了。
2、部署模式
CrazyBird强烈推荐以Windows Service作为WCF宿主,不论从效率或者安全及稳定性上来说,Windows Service都是最棒的,特别是你的WCF服务还是以TCP命名管道作为主要通信手段的时候,Windows Service就更值得你考虑了,先不要说IIS6.0不支持TCP协议先。
在以Windows Service方式运行WCF host的时候,你首先会碰上第一个问题,找不到证书,一般都会提示你,不能以下列搜索标准找到证书,该进程必须具有对私钥的访问权限,在这个时候,就千万不要相信人云亦云的通过微软的FindPrivateKey工具找到证书文件,给LOCAL SERVICE访问权限,不信去试试,问题依旧。
实际上产生这个问题的原因很搞笑,只是因为你在开发时总是使用同一个证书,想当然的部署的时候只是用一个证书,如果你一开始就用两个证书,客户端和服务器之间利用证书时行相互认证和加密,那应该是没有问题的,以下第一条是很严肃的原则。
六、总结
(1):使用 .NET Framework 3.5 版 或更高版本时,可将自定义用户名和密码验证程序与消息和传输安全一起使用。使用 .NET Framework 3.0 时,自定义用户名和密码验证程序只能与消息安全一起使用。
(2):代码示例重写的 Validate 方法。如果在实际的项目里可以替换为您的自定义用户名和密码验证方案,从数据库检索用户名和密码来进行比较。你可以访问数据库或者其它的账号安全验证的数据进行验证。也是最灵活的方式之一,用户可以根据需要来定义身份验证机制。
(3):使用Windows Service宿主的时候,需要在服务端配置serviceCertificate的storeName="TrustedPeople" 以及 authentication的certificateValidationMode="PeerTrust",并且证书需要手动导入证书的pfx文件到受信任的根以及受信任人两个位置中去。
(4):提供本文的Demo程序的下载,
Message_CustomUserNamePassword_wsHttpBinding.rar
参考文章:
1.WCF分布式安全开发实践(10):消息安全模式之自定义用户名密码:Message_UserNamePassword_WSHttpBinding
2.WCF分布式安全开发实践(8):消息安全模式之用户名身份验证:Message_UserName_WSHttpBinding