实战WCF:软件远程认证实例
本文通过实战软件远程认证为例,来展示如何使用WCF/TCP模式下 Username/Password 验证方式,以及使用过程中可能遇到的一些问题。
(本实例是之前的一个实例,时间稍久,有些回忆错误的地方望见谅)
1、实例预览
服务端用户管理界面,主要用来管理远程客户端的认证信息、认证状态
2、客户端如何通过服务端认证
我们知道如果客户端需要进行网络验证首先需要提供一组认证信息,用户名/密码 是比较常用的方式,有时候我们也需要让客户端绑定指定的机器,这时候就需要客户端提供的认证信息是与客户端机器相关的唯一性的信息;
本例中使用的方式就是后一种,要求客户端只能运行在绑定硬件信息的电脑中,至于怎么获取客户端电脑唯一信息网上有很多介绍,一般我会使用以下两种方式:
1)通过加壳软件IntelliLock之类的SDK获取硬件唯一信息;
2)客户端首次启动时生成一个GUID存放在固定位置,以后通过这个GUID进行唯一性认证 (这种方式要求保证GUID的安全,我的方法是通过DPAPI加密保护)
现在我们先通过使用加壳软件的方式来获取硬件唯一信息:
using IntelliLock.Licensing;
public static string GetHardwareID()
{
return HardwareID.GetHardwareID(true, true, false, true, true, false);
}
有了认证信息,我们就需要与服务端进行通讯认证,这里我们就正式开始WCF实战。
第一步:定义契约
namespace Contract
{
[ServiceContract(Name = "HandRegister", Namespace = "net.tcp://espier.cc")]
public interface IContract
{
[OperationContract(Name="CheckUser")]
CheckResult CheckUser(string name);
[OperationContract(Name="Upload")]
void Upload(string text);
[OperationContract(Name="GetValid")]
string GetValid(string args);
}
}
namespace Contract
{
[DataContract]
public class CheckResult
{
[DataMember]
public bool IsValid
{
get;
set;
}
[DataMember]
public string Context
{
get;
set;
}
}
}
namespace Service
{
public class Service:IContract
{
public CheckResult CheckUser(string name)
{
CheckResult result=new CheckResult();
var query = Cacher.UsersCache
.Where(p => p.UserName.Equals(name));
if (query.Count() > 0)
{
User pUser=query.FirstOrDefault();
if (!pUser.Enable)
{
result.IsValid = false;
result.Context = "用户被禁止使用!";
return result;
}
if (pUser.ExpiredTime <= DateTime.Now)
{
result.IsValid = false;
result.Context = "用户已过期!";
return result;
}
result.IsValid = true;
result.Context = string.Format("剩余使用天数:{0}",
pUser.ExpiredTime.Subtract(DateTime.Now).Days);
}
else
{
result.IsValid = false;
result.Context = "验证失败,用户不存在!";
}
return result;
}
public void Upload(string text)
{
Console.WriteLine(text);
}
public string GetValid(string args)
{
string tm = Convert.ToBase64String(Encoding.Default.GetBytes(GetUtcTime()));
return string.Format("ua={0}|ma=VJpK1iWRUo0=|br=cT0uW9fzK/9XRVDMdH+1w4wW+DUjWjQR|pr=VJpK1iWRUo0=|id=VJpK1iWRUo0=|mc=VtEP031H7lxve2vwbW7chVyzmXQNuAvhN5xLn05XFgE=|ks=+iyYeBhfRhieJBRrKJOJOXwpQCu/tSD2eeJlkc+kaiVfozW2zDN1A7wowUlBMKfoKdYddwYFIwrpiKEAlXOqhTSr8xiU6paQTU0dASu8PFhecSb9iWbztiH4XcS+ukSG",
tm);
}
public static string GetUtcTime()
{
TimeSpan tspan = DateTime.Now - Convert.ToDateTime("1970-01-01");
Int64 interval = Convert.ToInt64((tspan.TotalMilliseconds)) - 8 * 3600;
return interval.ToString();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using Service;
using System.IO;
using Service.Crypt;
namespace Service
{
public class Cacher
{
static readonly string DATA_FILE_NAME = "Users.dat";
static readonly string CRYPT_BASE64_KEY = "uOLFLnlJzOB2oqhB1BfJCiVfgH6lDhDYo9PYW3YmotE=";
static UserCollection _users = new UserCollection();
public static UserCollection UsersCache
{
get
{
return _users;
}
}
static IList<string> _blacks = new List<string>();
public static IList<string> BlackList
{
get
{
return _blacks;
}
}
public static void LoadBlackList()
{
string blackfile = Path.Combine(Helper.AppPath(),
"config.bin");
if (File.Exists(blackfile))
{
using (StreamReader reader = new StreamReader(blackfile, Encoding.Default))
{
string srLine = reader.ReadLine();
while (srLine != null)
{
if (!string.IsNullOrEmpty(srLine))
{
_blacks.Add(srLine.Trim());
}
srLine = reader.ReadLine();
}
}
}
}
public static void LoadUsers()
{
string fileName = Path.Combine(Helper.AppPath(),
Cacher.DATA_FILE_NAME);
if (File.Exists(fileName))
{
Rijndael rj = Rijndael.Create();
try
{
using (SymmetricCryptographer crypt = new SymmetricCryptographer(rj.GetType()))
using (FileStream reader = new FileStream(fileName, FileMode.Open, FileAccess.Read))
{
byte[] buff = new byte[reader.Length];
reader.Read(buff, 0, buff.Length);
byte[] key = Convert.FromBase64String(Cacher.CRYPT_BASE64_KEY);
byte[] decode = crypt.Decrypt(buff, key);
_users = Helper.XmlDeserialize<UserCollection>(Encoding.Default.GetString(decode));
}
}
catch { }
finally
{
rj.Dispose();
}
}
}
public static void SaveUsers()
{
string fileName = Path.Combine(Helper.AppPath(),
Cacher.DATA_FILE_NAME);
Rijndael rj = Rijndael.Create();
try
{
string xml = Helper.XmlSerialize<UserCollection>(_users);
using (SymmetricCryptographer crypt = new SymmetricCryptographer(rj.GetType()))
using (FileStream writer = new FileStream(fileName, FileMode.Create, FileAccess.Write))
{
byte[] key = Convert.FromBase64String(Cacher.CRYPT_BASE64_KEY);
byte[] buff = crypt.Encrypt(Encoding.Default.GetBytes(xml), key);
writer.Write(buff, 0, buff.Length);
}
}
catch { }
finally
{
rj.Dispose();
}
}
}
}
剩下的逻辑就比较简单了,简单判断 name 的认证状态,这里就不在解释;
其他的契约服务也不讲解了,实际上是与客户端业务相关的远程服务;
3、WCF应用
通过前面了解了简单的认证逻辑,接下来我们就正式开始WCF编程了;
首先谈几点涉及到的问题:
1)binding选择:我们选择TcpBinding, 这是与客户端需求有关,因为这里的客户端需要频繁Upload信息到服务端,所以我们选择Net.Tcp方式作为连接;
2)验证方式:WCF的验证方式主要包括:
无验证、Windows验证、Username/password验证、X509证书、自定义机制、颁发的票据
根据实际需求我们需要使用Username/Password验证方式;
3)授权:这里授权要分具体情况来处理了,前面我们描述过服务的实现,那么CheckUser服务作为客户端认证的入口,肯定是不需要授权就允许调用的了,而相对于认证服务,其他服务或许就需要在验证通过的前提下授权使用了(这里对应前面服务中的Upload 、GetValid方法,大家自行对照前面的代码看下)
4)传输安全:为什么要提到传输安全,这也是由我们的实际应用决定的,认证服务本身就应该是一种安全传输服务,反之认证就没有任何意义了,因为我们同样可以通过技术手段伪造认证,传输安全就是保证我们的认证过程不被伪造的前提;
5)其他:剩下的就是使用过程中的细节问题,我们将会在下面遇到时再做解释;
下面我们先看2张表格:
1)绑定和传输安全模式
2)绑定和传输模式下安全客户端凭据
以上2副表格是《WCF服务编程中的》,这里截图来自http://www.cnblogs.com/wangshuai/archive/2010/06/02/1750101.html
根据以上表格和我们上面描述的4点问题,我们会发现我们选择NetTcpBinding 在传输安全模式下竟然不能支持UserName/Password验证方式,这就有点为难了,我们怎么保证即要认证过程安全又能够使用Username/Password验证呢?
好在Message安全模式支持Username/Password验证,我们看下表关系:
Message传输安全模式可以简单的加密消息本身;通过加密的消息可以的消息能够在非传输安全的协议上传输,如HTTP协议上,因此Message安全模式实现了端到端的安全,不论中间媒介多少,也不论通道是否安全。
Ok,这里就已经为我们解决了一个问题了,这个问题也是经常会遇到的问题,究其原因一方面是我们对WCF不是非常非常熟悉与了解(它的方方面面),二是WCF体系相对来说比较庞大,在支持多种应用的时候对于不同应用的配置会完全不一样;
现在我们就可以写出第一小段代码了:
NetTcpBinding binding = new NetTcpBinding(SecurityMode.Message);
binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
接着我们看下验证的实现(这里需要自定义验证的实现):
//自定义验证
host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNameValidator();
CustomUserNameValidator类通过集成UserNamePasswordValidator实现Validate方法;
public class CustomUserNameValidator : UserNamePasswordValidator
{
public override void Validate(string name, string pass)
{
}
}
奇怪的是这里我为什么没有实现Validate方法呢,其实通过自定义验证我们并不是需要在此对客户端认证信息进行一个判断,我们实际的判断是在CheckUser服务中;
好,验证完毕紧接着就是授权,实现授权和验证的例子网上也比较多,但很多都不是特别完整,或者不涉及到本例中的一些内容,这里先贴出授权部分代码,下面再一步步讲解可能遇到的问题:
//自定义授权管理
IList<IAuthorizationPolicy> policys = new List<IAuthorizationPolicy>();
policys.Add(new CustomAuthorizationPolicy());
host.Authorization.ExternalAuthorizationPolicies = new ReadOnlyCollection<IAuthorizationPolicy>(policys);
host.Authorization.ServiceAuthorizationManager = new CustomServiceAuthorizationManager();
授权实现:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IdentityModel.Policy;
using System.IdentityModel.Claims;
using Service;
namespace Server.Security
{
public class CustomAuthorizationPolicy : IAuthorizationPolicy
{
string id = string.Empty;
public CustomAuthorizationPolicy()
{
id = new Guid().ToString();
}
public bool Evaluate(EvaluationContext Context, ref object ObjectContext)
{
bool r_object = false;
if (ObjectContext == null)
{
ObjectContext = r_object ;
}
else {
r_object = Convert.ToBoolean(ObjectContext);
}
if (!r_object)
{
List<Claim> claims = new List<Claim>();
foreach (ClaimSet cs in Context.ClaimSets)
{
foreach (Claim claim in cs.FindClaims(ClaimTypes.Name, Rights.PossessProperty))
{
Console.WriteLine("用户 : {0}", claim.Resource);
foreach (string str in GetOperationList(claim.Resource.ToString()))
{
claims.Add(new Claim("net.tcp://espier.cc/HandRegister/",str,Rights.PossessProperty));
Console.WriteLine("授权的资源:{0}", str);
}
}
}
Context.AddClaimSet(this, new DefaultClaimSet(Issuer, claims));
}
return true;
}
private static IEnumerable<string> GetOperationList(string name)
{
List<string> rights = new List<string>();
var query = Cacher.UsersCache
.Where(p => p.UserName.Equals(name));
if (query.Count() > 0)
{
User user = query.FirstOrDefault();
if(user.Enable
|| user.ExpiredTime > DateTime.Now)
{
//授权的资源
rights.Add(string.Format("net.tcp://espier.cc/HandRegister/GetValid"));
rights.Add(string.Format("net.tcp://espier.cc/HandRegister/Upload"));
}
}
//公开资源
rights.Add(string.Format("net.tcp://espier.cc/HandRegister/CheckUser"));
return rights;
}
public ClaimSet Issuer
{
get { return ClaimSet.System; }
}
public string Id
{
get { return id; }
}
}
}
上面这段代码网络上比较多,但大都是演示通过Windows认证,其中实体Claim获取到的资源都是windows用户或组信息;
本例中的授权获取方法GetOperationList主要就是通过"用户认证信息"/也就是我们的Username/password
明显的可以看出被允许的用户拥有GetValid和Upload服务授权;
授权管理实现:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ServiceModel;
using System.IdentityModel.Claims;
namespace Server.Security
{
public class CustomServiceAuthorizationManager : ServiceAuthorizationManager
{
protected override bool CheckAccessCore(OperationContext Context)
{
string action = Context.RequestContext.RequestMessage.Headers.Action;
foreach (ClaimSet cs in Context.ServiceSecurityContext.AuthorizationContext.ClaimSets)
{
if (cs.Issuer == ClaimSet.System)
{
foreach (Claim c in cs.FindClaims("net.tcp://espier.cc/HandRegister/", Rights.PossessProperty))
{
Console.WriteLine("正在比较权限:" + action + "和" + c.Resource.ToString());
if (action == c.Resource.ToString())
return true;
}
}
}
return false;
}
}
}
那授权管理其实就是判断客户端请求的action是否与该用户被授权的服务action相同,客户端是否被允许访问,这里就不在细说咯!
完成了上面这些之后,如果大家认为我们的服务端可以正常运行了,那么就错了,由于前面我们使用Message安全代替了传输安全,那么Message安全要求的加密就必须要通过x509证书来完成了,
所以在创建ServerHost的时候如果没有添加Message的证书那就要报错~~~
完整正确的做法如下:
ServiceHost host = null;
public bool Start(out string errmsg)
{
errmsg = string.Empty;
try
{
Uri uri = new Uri(string.Format("net.tcp://{0}:{1}", _address, _port));
NetTcpBinding binding = new NetTcpBinding(SecurityMode.Message);
binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
host = new ServiceHost(typeof(Service.Service));
host.AddServiceEndpoint(typeof(Contract.IContract),
binding,
uri);
//必须设置证书才可以使用 UserName模式
host.Credentials.ServiceCertificate.SetCertificate(StoreLocation.CurrentUser, StoreName.My, X509FindType.FindByIssuerName, "HandRegisterCretificate");
//自定义授权管理
IList<IAuthorizationPolicy> policys = new List<IAuthorizationPolicy>();
policys.Add(new CustomAuthorizationPolicy());
host.Authorization.ExternalAuthorizationPolicies = new ReadOnlyCollection<IAuthorizationPolicy>(policys);
host.Authorization.ServiceAuthorizationManager = new CustomServiceAuthorizationManager();
//自定义验证
host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNameValidator();
host.Open();
return true;
}
catch (Exception ex)
{
errmsg = ex.Message;
}
return false;
}
这里证书的查找就依照大家的使用具体自行去实现吧,我这里是自己生成了一个证书导入到个人证书去了,
为了方便大家完全可以随便使用一个在服务端中的证书(证书主要是用于消息加密)
//必须设置证书才可以使用 UserName模式
host.Credentials.ServiceCertificate.SetCertificate(StoreLocation.CurrentUser, StoreName.My, X509FindType.FindByIssuerName, "HandRegisterCretificate");
这样,我们的服务端就算基本完成了;下面提供服务端部分的完整代码打包:
下面继续介绍客户端部分,由于服务端的实现涉及到了Message 安全,Username/password验证,并且使用了证书,那么客户端部分就需要一些相应的处理;
先简单看下客户端的Channel创建部分:
string name = Helper.GetHardwareID();
ChannelFactory<IContract> proxy = new ChannelFactory<IContract>("HandRegister_Client");
proxy.Credentials.UserName.UserName = name;
_context = proxy.CreateChannel();
CheckResult result = _context.CheckUser(name);
客户端的具体参数并没有像服务端一样自实现,而是写在配置文件里了,这里比较简单,主要就是通过
Credentials来设置username/password凭证,把凭证的username作为客户端唯一信息去调用Checkuser服务验证;
客户端配置文件为:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address="net.tcp://121.10.107.115:8866" binding="netTcpBinding"
bindingConfiguration="HandRegister_Binding" contract="Contract.IContract"
behaviorConfiguration="ClientBehavior" name="HandRegister_Client">
<identity>
<dns value="HandRegisterCretificate"/>
</identity>
</endpoint>
</client>
<bindings>
<netTcpBinding>
<binding name="HandRegister_Binding">
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>
</netTcpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="ClientBehavior">
<clientCredentials>
<serviceCertificate>
<authentication certificateValidationMode="Custom" customCertificateValidatorType="RegisteKey.CustomX509CertificateValidator,RegisteKey" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
其中值得注意的点有2处:
1)客户端证书验证,这里为了跳过客户端证书验证,我们需要自定义X509CertificateValidator实现并让Validate不做任何逻辑判断:
<authentication certificateValidationMode="Custom" customCertificateValidatorType="RegisteKey.CustomX509CertificateValidator,RegisteKey" />
2)这里为什么要添加这项呢,如果不添加会如何呢?
<identity>
<dns value="HandRegisterCretificate"/>
</identity>
大家可以自行试试如果不添加会怎样,具体的原因和处理方法网上有介绍。
到这里我们的客户端也算完成了,总体来说,整个实现看似简单却包含了诸多知识点,如果没有亲自实战你可能就不会发现下面这些问题:
1)当使用NetTcpbinding绑定时 为什么不能使用username/password验证?
2)当选用了Meesage安全代替传输安全并保证nettcpbinding允许使用username/password验证时,没有证书引发的异常?
3)对于username/password验证的实现到底怎么和授权联系起来?
4)客户端通过了证书验证之后为什么不添加dns配置就会错误?
等等这些问题也是我在完整本示例时的真实反映,现在记录下来分享给同样受此困惑的同学;
最后附加简单的客户端验证部分源码: