第九篇:WCF安全 - 自定义认证
接着上一篇,我们尝试一下用自定义用户名密码的方式来做安全认证,这样就不用受制于Windows的用户系统了。
首先需要说明的是,使用自定义用户名密码时,由于不能利用Windows用户系统的相关安全机制了,因此必须自己准备数字证书来处理数据加密。
1、准备数字证书
证书要求有可进行密钥交换的私钥,一般用makcert比较方便,自带的,也可以用OpenSSL或OpenSSL.Net一类的。本例中生成一个自签名的根证书,放入系统的受信根证书颁发机构区:
makecert -n "CN=192.168.90.81" -b 01/01/2012 -e 01/01/2050 -r -sky exchange -sr LocalMachine -ss Root -a sha1
参数含义:
-n 指定使用者为192.168.90.81,注意它必须和调用时指定的域名一致
-b 起始日期为2012-1-1
-e 失效日期为2050-1-1
-r 自签名
-sky exchange 指定是密钥交换型而不是签名型
-sr LocalMachine 存入本地计算机
-ss Root 存入受信任根证书颁发机构区
-a sha1 使用SHA1签名
2、服务端
服务端要修改几处,首先是配置文件App.config:
<?xmlversion="1.0"encoding="utf-8"?>
<configuration>
<system.serviceModel>
<services>
<!--在上一篇的基础上加了behaviorConfiguration,内容见后-->
<servicename="Server.DataProvider"behaviorConfiguration="tcpBehavior">
<endpointaddress=""binding="netTcpBinding"contract="Server.IData"bindingConfiguration="tcpBinding"/>
<host>
<baseAddresses>
<addbaseAddress="net.tcp://localhost:8081/wcf"/>
</baseAddresses>
</host>
</service>
</services>
<bindings>
<netTcpBinding>
<bindingname="tcpBinding">
<securitymode="Message">
<!--与上一篇相比,认证类型从Windows改成了UserName-->
<messageclientCredentialType="UserName"/>
</security>
</binding>
</netTcpBinding>
</bindings>
<!--这是新加的节,用于指定用户名密码的验证方式-->
<behaviors>
<serviceBehaviors>
<!--注意这个name是被前面使用的-->
<behaviorname="tcpBehavior">
<serviceCredentials>
<!--指定验证方式为Custom,表示自定义,既然是自定义的,就要指出用哪个类进行用户名密码验证,这里指定了Server程序集中的Server.Validator类,注意这里类完整名称的写法-->
<userNameAuthenticationuserNamePasswordValidationMode="Custom"customUserNamePasswordValidatorType="Server.Validator, Server"/>
<!--指定用于数据加密的证书,LocalMachine表示本地计算机,Root表示受信任根证书颁发机构,192.168.90.81是证书标题(因为做证书时没指定标题,所以使用者默认就是标题),FindBySubjectName表示按标题查找-->
<serviceCertificatestoreLocation="LocalMachine"storeName="Root"findValue="192.168.90.81"x509FindType="FindBySubjectName"/>
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
接下来是自定义的验证类,增加一个Validator类,它要继承System.IdentityModel.Selector.UserNamePasswordValidator基类。
using System;
using System.IdentityModel.Selectors;
using System.ServiceModel;
namespace Server
{
publicclass Validator : UserNamePasswordValidator
{
//重写Validate方法,这里简化处理,直接写死用户名密码,实际应用中应结合DB、配置文件等来做验证
publicoverridevoid Validate(string userName, string password)
{
if(!string.Equals(userName, "root") || !string.Equals(password, "pass"))
thrownew Exception("Access Denied");
}
}
}
最后我们还要小改一下契约接口的实现类,因为之前我们用WindowIdentity来识别登录用户,换用自定义用户名密码后,它不管用了,所以新的实现类是这样的:
using System;
using System.ServiceModel;
namespace Server
{
[ServiceBehavior]
publicclass DataProvider : IData
{
publicstring SayHello()
{
//变化不大,用PrimaryIdentity来代替WindowIdentity
returnstring.Format("Hello {0}", OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name);
}
}
}
OK,运行一下,应该能正常启动,如果失败,仔细看一下提示信息,一般都是证书问题。
3、客户端
变化不大,先来看配置文件App.config:
<?xmlversion="1.0"encoding="utf-8"?>
<configuration>
<system.serviceModel>
<client>
<endpointbinding="netTcpBinding"contract="Server.IData"address="net.tcp://192.168.90.81:8081/wcf"name="DataProvider"bindingConfiguration="tcp"/>
</client>
<bindings>
<netTcpBinding>
<bindingname="tcp">
<securitymode="Message">
<!--只有此处把Windows改成了UserName,和服务端对应-->
<messageclientCredentialType="UserName"/>
</security>
</binding>
</netTcpBinding>
</bindings>
</system.serviceModel>
</configuration>
然后是调用时用户名密码的传递方式变了一点:
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace Client
{
class Program
{
staticvoid Main(string[] args)
{
//创建一个ChannelFactory,指定使用名为DataProvider的配置
var factory = new ChannelFactory<Server.IData>("DataProvider");
//指定用户名、密码,和前一篇的区别是把Windows换成了UserName
factory.Credentials.UserName.UserName = "root";
factory.Credentials.UserName.Password = "pass";
//创建Channel,并调用SayHello方法
var proxy = factory.CreateChannel();
Console.WriteLine(proxy.SayHello());
((IChannel)proxy).Close();
}
}
}
一切就绪,运行一下吧,应该能看到“Hello root”。如果用户名密码错误,会收到Exception。
如果服务端启动失败,请检查:
◇ 证书是否有可交换的密钥◇ 证书是否正确导入了系统◇ 按服务端App.config中指定的证书查找方式是否可找到证书◇ 指定的自定义验证类名称是否错误
如果客户端访问失败,请检查:
◇ 是否提供了正确的用户名密码
◇ 服务端证书有效期是否合法 ◇ 服务端证书的证书链是否完整 ◇ 客户端访问时使用的域名/IP是否与服务端证书的使用者一致OK,安全问题就讲到这里吧,既然是简单教程,就不继续深入了。
本文出自 “” 博客,请务必保留此出处