为什么要用x.509证书?
WCF的服务端和客户端之间,如果不作任何安全处理(即服务端的<security mode="None">),则所有传输的消息将以明文方式满天飞,在internet/intranet环境下无疑是很不安全的,这就是用证书的目的。(当然WCF还有其它安全机制,比如最常见的UserName方式,但通常每次都要从数据库读取用户名/密码信息进行验证,比较麻烦,开销也大,个人觉得还是证书最为方便)--关于x.509证书
的基本知识,可参见http://www.cnblogs.com/yjmyzz/archive/2008/08/19/1271171.html
大致原理(个人理解,可能不太准确):
正确设置服务端与客户端证书后,WCF的服务端启动时,需要利用服务端证书验证,如果验证通过将正常启动,否则报异常,同时客户端调用服务端方法时,也需要提供客户端证书,服务端接受到客户端证书后,验证客户端证书的有效性,如果通常,则允许客户端正常调用。
下面将逐步讲解如何使用:
1.制作证书
先进入到vs2008的命令行状态,即:
开始-->程序-->Microsoft Visual Studio 2008-->Visual Studio Tools-->Visual Studio 2008 命令提示
键入:
makecert -r -pe -n
"
CN=MyServer
"
-ss My -sky exchange
解释一下:makecert.exe是一个专门用来制作证书的小工具,上面一行的意思就是制作一个CN=MyServer的服务器证书,默认存储在CurrentUser\My这个位置,同时这个证书标识为可导出。(详细的
MakeCert参数可参见http://msdn.microsoft.com/zh-cn/bfsktky3(vs.80).aspx)
再输入:
makecert -r -pe -n
"
CN=MyClient
"
-ss My -sky exchange
生成客户端证书,证书生成好以后,可以在IE里查看到,IE-->工具-->Internet选项-->内容-->证书
同时如何管理已经安装的证书,可参见http://www.cnblogs.com/yjmyzz/archive/2008/08/20/1272128.html
2.wcf服务端
vs.net2008启动后,新建一个控制台应用程序-->(右击)添加-->新建项-->WCF服务-->命名为MyService.cs-->保存
保存后,系统会自动生成一个接口文件IMyService.cs
二个文件的内容如下:
IMyService.cs
using
System;
using
System.ServiceModel;
namespace
Server
{
//
注意: 如果更改此处的接口名称 "IMyService",也必须更新 App.config 中对 "IMyService" 的引用。
[ServiceContract]
public
interface
IMyService
{
[OperationContract]
string
Test();
}
}
MyService.cs
using
System;
using
System.ServiceModel;
namespace
Server
{
//
注意: 如果更改此处的类名 "MyService",也必须更新 App.config 中对 "MyService" 的引用。
public
class
MyService : IMyService
{
public
string
Test()
{
Console.WriteLine(
"
服务端输出:\n
"
+
ServiceSecurityContext.Current.PrimaryIdentity.AuthenticationType);
Console.WriteLine(ServiceSecurityContext.Current.PrimaryIdentity.Name);
return
"
服务端时间:
"
+
DateTime.Now.ToString();
}
}
}
再来新建一个cs文件:CustomX509CertificateValidator.cs
内容先贴在下面
using
System;
using
System.Security.Cryptography.X509Certificates;
using
System.IdentityModel.Tokens;
using
System.IdentityModel.Selectors;
namespace
Server
{
public
class
CustomX509CertificateValidator : X509CertificateValidator
{
public
override
void
Validate(X509Certificate2 certificate)
{
Console.WriteLine(certificate.Subject);
Console.WriteLine(certificate.Thumbprint);
if
(certificate.Thumbprint
!=
"3E4D4B64A90810B6CFF9B1DD2390D8C9488747BF
"
)
throw
new
SecurityTokenException(
"
Certificate Validation Error!
"
);
}
}
}
注意:项目必须先添加对System.IdentityModel的引用
这里我们可以把证书编写放在数据库中或CONFING文件 这样写的更好一点。。
解释一下:
这个文件的用户是:客户端要调用服务端方法,并提供客户端证书时,用来验证客户端证书的有效性。注意里面的if (certificate.Thumbprint != "3E4D4B64A90810B6CFF9B1DD2390D8C9488747BF")这一句,大家调试的时候,里面的3E4D4B64A90810B6CFF9B1DD2390D8C9488747BF要换成你自己的客户端证书的信息(每一个证书对应的这一串字符都是唯一的),可通过在IE浏览器里,查看MyClient证书的详细信息得到,见下图:
同时注意配置文件App.Config,内容如下
<?
xml version
=
"
1.0
"
encoding
=
"
utf-8
"
?>
<
configuration
>
<
system.serviceModel
>
<
behaviors
>
<
serviceBehaviors
>
<
behavior name
=
"
Server.MyServiceBehavior
"
>
<
serviceMetadata httpGetEnabled
=
"
true
"
httpGetUrl
=
"
http://localhost:8080
"
/>
<
serviceDebug includeExceptionDetailInFaults
=
"
true
"
/>
<
serviceCredentials
>
<
clientCertificate
>
<
authentication certificateValidationMode
=
"
Custom
"
customCertificateValidatorType
=
"
Server.CustomX509CertificateValidator,Server
"
/>
</
clientCertificate
>
<
serviceCertificate findValue
=
"
MyServer
"
storeLocation
=
"
CurrentUser
"
x509FindType
=
"
FindBySubjectName
"
/>
</
serviceCredentials
>
</
behavior
>
</
serviceBehaviors
>
</
behaviors
>
<
bindings
>
<
netTcpBinding
>
<
binding name
=
"
NewBinding0
"
>
<
security mode
=
"
Transport
"
>
<
transport clientCredentialType
=
"
Certificate
"
/>
</
security
>
</
binding
>
</
netTcpBinding
>
</
bindings
>
<
services
>
<
service behaviorConfiguration
=
"
Server.MyServiceBehavior
"
name
=
"
Server.MyService
"
>
<
endpoint address
=
"
net.tcp://localhost:8081
"
binding
=
"
netTcpBinding
"
contract
=
"
Server.IMyService
"
bindingConfiguration
=
"
NewBinding0
"
/>
</
service
>
</
services
>
</
system.serviceModel
>
</
configuration
>
解释一下:
<authentication certificateValidationMode="Custom" customCertificateValidatorType="Server.CustomX509CertificateValidator,Server"/></clientCertificate>
这一行的意思就是通知WCF服务端,验证客户端证书的模式为自定义,验证时调用Server.CustomX509CertificateValidator这个类来完成验证
<serviceCertificate findValue="MyServer" storeLocation="CurrentUser" x509FindType="FindBySubjectName" />
这一行的意思是WCF服务端验证证书时,到CurrentUser这个位置查询CN=MyServer的证书。CN=就是通过FindBySubjectName它来指定的。
httpGetUrl="http://localhost:8080" 通过HTTP来获取元数据。。 可是我觉得通过终结点也一样的。有点搞不太明白。。
最后在Program.cs里启用WCF,内容如下:
using
System;
using
System.ServiceModel;
namespace
Server
{
class
Program
{
static
void
Main(
string
[] args)
{
ServiceHost host
=
new
ServiceHost(
typeof
(MyService));
host.Open();
Console.ReadKey();
}
}
}
build一下,如果编译无错的话,服务端完工,可以运行一下,将弹出一个DOS命令窗口(不过什么输出也没有),只要不报错,就表明Ok了,先不要急着关,尝试浏览一下:
http://localhost:8080/(这个地址哪里来的?回头看下App.config,里面有一行<serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost:8080/" />,呵呵,明白了吧) 正常的话,应该类似下图所示:
3.客户端调用
下面生成服务端的代理和配置文件,客户端开发将用到这二个文件,同样先进入vs2008的命令行状态,输入:
svcutil.exe http://localhost:8080/ /d:c:\123\
注意:输入这一行命令的时候,请确保服务端程序正在运行。这一句的意思就是在c:\123\目录下输出WCF的代理文件和配置文件
打开vs.net2008,再新建一个控制台应用程序,可以命名为Client
把c:\123\下生成的二个文件MyService.cs,output.config添加到Client项目中,同时将output.config改名为App.Config
Progam.cs代码内容如下:
using
System;
namespace
Client
{
class
Program
{
static
void
Main(
string
[] args)
{
using
(MyServiceClient client
=
new
MyServiceClient())
{
Console.WriteLine(
"
客户端输出:
"
);
Console.WriteLine(client.Test());
}
Console.ReadKey();
}
}
}
同时,参考下面的内容手动修改一下App.Config文件
<?
xml version="1.0" encoding="utf-8"
?>
<
configuration
>
<
system.serviceModel
>
<
behaviors
>
<
endpointBehaviors
>
<
behavior
name
="NewBehavior"
>
<
clientCredentials
>
<
clientCertificate
findValue
="MyClient"
x509FindType
="FindBySubjectName"
/>
<
serviceCertificate
>
<
authentication
certificateValidationMode
="None"
/>
</
serviceCertificate
>
</
clientCredentials
>
</
behavior
>
</
endpointBehaviors
>
</
behaviors
>
<
bindings
>
<
netTcpBinding
>
<
binding
name
="NetTcpBinding_IMyService"
closeTimeout
="00:01:00"
openTimeout
="00:01:00"
receiveTimeout
="00:10:00"
sendTimeout
="00:01:00"
transactionFlow
="false"
transferMode
="Buffered"
transactionProtocol
="OleTransactions"
hostNameComparisonMode
="StrongWildcard"
listenBacklog
="10"
maxBufferPoolSize
="524288"
maxBufferSize
="65536"
maxConnections
="10"
maxReceivedMessageSize
="65536"
>
<
readerQuotas
maxDepth
="32"
maxStringContentLength
="8192"
maxArrayLength
="16384"
maxBytesPerRead
="4096"
maxNameTableCharCount
="16384"
/>
<
reliableSession
ordered
="true"
inactivityTimeout
="00:10:00"
enabled
="false"
/>
<
security
mode
="Transport"
>
<
transport
clientCredentialType
="Certificate"
protectionLevel
="EncryptAndSign"
/>
<
message
clientCredentialType
="Windows"
/>
</
security
>
</
binding
>
</
netTcpBinding
>
</
bindings
>
<
client
>
<
endpoint
address
="net.tcp://localhost:8081/"
binding
="netTcpBinding"
bindingConfiguration
="NetTcpBinding_IMyService"
contract
="IMyService"
name
="NetTcpBinding_IMyService"
behaviorConfiguration
="NewBehavior"
>
<
identity
>
<
dns
value
="MyServer"
/>
</
identity
>
</
endpoint
>
</
client
>
</
system.serviceModel
>
</
configuration
>
主要是增加了一个节点
<behaviors>
<endpointBehaviors>
<behavior name="NewBehavior">
<clientCredentials>
<clientCertificate findValue="MyClient" x509FindType="FindBySubjectName"/>
<serviceCertificate>
<authentication certificateValidationMode="None" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
上面红色的行,就是表明客户端调用时,将用MyClient证书来验证
同时<endpoint address="net.tcp://localhost:8081/" binding="netTcpBinding"
bindingConfiguration="NetTcpBinding_IMyService" contract="IMyService"
name="NetTcpBinding_IMyService" behaviorConfiguration="NewBehavior">这一句,增加了behaviorConfiguration="NewBehavior"
好了,Build一下,没有问题的话,开发完成
4.测试:
先启动服务端,再启动客户端,运行结果如下:
(转贴请注明来自"菩提树下的杨过") http://www.cnblogs.com/yjmyzz/archive/2008/08/20/1272550.html
注意服务端server.exe输出的信息中3E4D4B64A90810B6CFF9B1DD2390D8C9488747BF与客户端证书完全吻合
最后来谈谈分发问题,上面这一系列测试都是在同一台机器完成的,客户端总不可能总是跟服务端在一台机器上,这个好办,在IE里把MyClient证书导出,注意导出时要选择"是,导出私钥",然后把导出的pfx文件连同客户端程序一起分发到目标客户机即可,这里要注意几点:
a.客户端上的App.config里,要把<endpoint address="net.tcp://localhost:8081/" 中的localhost换成服务端的Ip地址
b.注意防火墙参数设置(本例中,即要把tcp:8081端口打开)