关于.net 2.0 remoting 中 TCP Channel 用户认证探讨(二)
在关于.net 2.0 remoting 中 TCP Channel 用户认证探讨(一)中,从两个角度来探讨一下TCP Channel 用户认证,但是后者依旧不能满足我们实际的应用。在网上搜索了大量资料,也没有找到详细的例子,但是却有个网友说了句“可以在Facade使用验证”这句提醒了我。怎么提醒的,在结尾再说吧,现在直奔主题。
我们面对的问题:在每个具体的服务(远程对象的方法)中进行验证太烦琐,如何尽量采用少量的验证方式却能完成相同的功能。
下面我来描述一下我的解决方案:
1、用来存储客户端的信息类
// 模块编号:
// 文件名: TicketIdentity.cs
// 描述: 客户端类
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using System.Runtime.Remoting.Messaging;
namespace Novelty.Model
{
/// <summary>
/// 客户端类
/// </summary>
[Serializable]
public class TicketIdentity : ILogicalThreadAffinative
{
内部成员变量
构造函数
属性
}
}
2、远程组件的几个类
(1)客户端身份类,用来存储客户的连接.
// 模块编号:
// 文件名: AuthorizationModule.cs
// 描述: 客户端身份验证类
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Net;
using System.Security.Principal;
using System.Runtime.Remoting.Channels;
namespace Novelty.WinServices
{
/// <summary>
/// 全局客户端身份验证类,属于单件模式(Singleton Pattern)
/// </summary>
public class AuthorizationModule : IAuthorizeRemotingConnection
{
内部成员变量
构造函数
嵌套类
属性
实现接口的方法
}
}
(2)用户验证类.
该类里面使用了数据库访问访问层中对象,这个类没有给出来.用户验证通过后,将用户和客户端地址用上面的客户端身份类存储起来.
// 模块编号:
// 文件名: UserAccountCalling.cs
// 描述: UserAccountCalling 远程访问类
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using System.Reflection;
using Novelty.IWinServices;
using Novelty.IDAL.UserAccountBlcok;
using Novelty.DAL.UserAccountBlcok;
using Novelty.WinServices;
using Novelty.CustomSystem;
using Novelty.CustomSystem.CustomConfig;
using Novelty.Model;
namespace Novelty.WinServices
{
/// <summary>
/// UserAccountCalling 远程访问类
/// </summary>
public class UserAccountCalling : MarshalByRefObject, IUserAccountCalling
{
/// <summary>
/// UserAccount 数据访问对象接口实例
/// </summary>
private readonly IUserAccount dalUserAccount;
public UserAccountCalling()
{
dalUserAccount = (IUserAccount)new UserAccount();
}
/// <summary>
/// 验证用户
/// </summary>
/// <param name="username">用户名</param>
/// <param name="password">密码</param>
/// <param name="clientAddress">IP地址</param>
/// <returns>如果提供的用户名和密码有效,则返回 true;否则返回 false</returns>
public bool ValidateUser(string username, string password, string clientAddress)
{
bool result = false;
try
{
//验证用户
result = dalUserAccount.ValidateUser(username, password);
if (result)
{
//使用排它锁
lock (AuthorizationModule.Instance)
{
//更新用户登录信息
dalUserAccount.UpdateLogonInfo(username, clientAddress);
if (AuthorizationModule.Instance.Tickets.Contains(username))
{
AuthorizationModule.Instance.Tickets.Remove(username);
}
AuthorizationModule.Instance.Tickets.Add(username, clientAddress);
}
}
}
catch (Exception exception)
{
//不记录日志, 抛出异常, 不包装异常
ExceptionFacade.NoLogAndThrowAndNoWrapPolicy(exception);
}
return result;
}
}
}
它实现了下面的接口:
// 模块编号:
// 文件名: IUserAccount.cs
// 描述: UserAccount 远程访问层接口
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
namespace Novelty.IWinServices
{
/// <summary>
/// UserAccount 远程接口
/// </summary>
public interface IUserAccountCalling
{
/// <summary>
/// 验证用户
/// </summary>
/// <param name="username">用户名</param>
/// <param name="password">密码</param>
/// <param name="clientAddress">IP地址</param>
/// <returns>如果提供的用户名和密码有效,则返回 true;否则返回 false</returns>
bool ValidateUser(string username, string password, string clientAddress);
}
}
(3)提供服务类.(远程对象)
给用户提供服务的类.例子选用的是:当通过用户验证后,为客户端提供服务(也就是远程对象的方法)的调用.GetUserAccountInfo(string userName)这个方法就是提供给客户端来返回一个用户对象的.
该类里面也使用了数据库访问访问层中对象,这个类没有给出来.
// 模块编号:
// 文件名: UserAccountModule.cs
// 描述: UserAccountModule 远程访问类
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using System.Web.Security;
using Novelty.IWinServices.UserAccountBlcok;
using Novelty.Model.UserAccountBlcok;
using Novelty.IDAL.UserAccountBlcok;
using Novelty.DAL.UserAccountBlcok;
namespace Novelty.WinServices.UserAccountBlcok
{
/// <summary>
/// UserAccountModule 远程访问类
/// </summary>
public class UserAccountModule : MarshalByRefObject, IUserAccountModule
{
/// <summary>
/// UserAccount 数据访问对象接口实例
/// </summary>
private readonly IUserAccount dalUserAccount;
public UserAccountModule()
{
dalUserAccount = (IUserAccount)new UserAccount();
}
/// <summary>
/// 获得 UserAccountInfo 对象
/// </summary>
///<param name="userName">用户名</param>
/// <returns> UserAccountInfo 对象</returns>
public UserAccountInfo GetUserAccountInfo(string userName)
{
return dalUserAccount.GetUserAccountInfo(userName);
}
}
}
它实现了下面的接口:
// 模块编号:
// 文件名: IUserAccountModule.cs
// 描述: IUserAccountModule 远程访问层接口
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using System.Web.Security;
using Novelty.Model.UserAccountBlcok;
namespace Novelty.IWinServices.UserAccountBlcok
{
/// <summary>
/// IUserAccountModule 远程访问层接口
/// </summary>
public interface IUserAccountModule
{
/// <summary>
/// 获得 UserAccountInfo 对象
/// </summary>
///<param name="userName">用户名</param>
/// <returns> UserAccountInfo 对象</returns>
UserAccountInfo GetUserAccountInfo(string userName);
}
}
给了上面三个类后,插入几句话!
后面的两个类就是提供远程服务类.我们似乎应该接下来在服务器端开始注册这些服务.但是,如果要使用我们提出客户验证,那么现在还不是时候.我们下面继续给出两个抽象工厂类,分别来处理上面的两个远程服务类.
(4) 验证工厂类
// 模块编号:
// 文件名: UserVerfiyFactory.cs
// 描述: UserVerfiyFactory 远程访问层工厂类
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using Novelty.IWinServerFactory;
using Novelty.WinServices;
using Novelty.IWinServices;
namespace Novelty.WinServerFactory
{
/// <summary>
/// UserVerfiyFactory 远程访问层工厂类
/// </summary>
public class UserVerfiyFactory : MarshalByRefObject, IUserVerfiyFactory
{
/// <summary>
/// 创建 UserAccountCalling 远程访问实例
/// </summary>
/// <returns>远程访问实例</returns>
public IUserAccountCalling CreateUserAccount()
{
return new UserAccountCalling();
}
}
}
它实现了下面的接口:
// 模块编号:
// 文件名: IUserVerfiyFactory.cs
// 描述: IUserVerfiyFactory 远程访问层工厂类接口
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using Novelty.IWinServices;
namespace Novelty.IWinServerFactory
{
/// <summary>
/// IUserVerfiyFactory 远程访问层工厂类接口
/// </summary>
public interface IUserVerfiyFactory
{
/// <summary>
/// 创建 UserAccountCalling 远程访问实例
/// </summary>
/// <returns>远程访问实例</returns>
IUserAccountCalling CreateUserAccount();
}
}
(5)用户服务抽象工厂类:
// 模块编号:
// 文件名: UserAccountFactory.cs
// 描述: UserAccountFactory 远程访问层工厂类
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using Novelty.IWinServerFactory;
using Novelty.WinServices.UserAccountBlcok;
using Novelty.IWinServices.UserAccountBlcok;
using Novelty.CustomException;
namespace Novelty.WinServerFactory
{
/// <summary>
/// UserAccountFactory 远程访问层工厂类
/// </summary>
public class UserAccountFactory : MarshalByRefObject, IUserAccountFactory
{
/// <summary>
/// 默认的构造函数
/// </summary>
public UserAccountFactory()
{
}
/// <summary>
/// 创建 UserAccountCalling 远程访问实例
/// </summary>
/// <returns>远程访问实例</returns>
public IUserAccountModule CreateUserAccount()
{
bool result = VerfiyOperation();
if (result)
{
return new UserAccountModule();
}
else
{
throw new InvalidRequestException("不正确的请求服务");
}
}
/// <summary>
/// 验证用户请求操作
/// </summary>
/// <returns></returns>
private bool VerfiyOperation()
{
UserVerfiyOperation userVerfiyOperation = new UserVerfiyOperation();
return userVerfiyOperation.VerfiyUser();
}
}
}
它实现了下面的接口:
// 模块编号:
// 文件名: IUserAccountFactory.cs
// 描述: IUserAccountFactory 远程访问层工厂类接口
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using Novelty.IWinServices.UserAccountBlcok;
namespace Novelty.IWinServerFactory
{
/// <summary>
/// IUserAccountFactory 远程访问层工厂类接口
/// </summary>
public interface IUserAccountFactory
{
/// <summary>
/// 创建 UserAccountCalling 远程访问实例
/// </summary>
/// <returns>远程访问实例</returns>
IUserAccountModule CreateUserAccount();
}
}
(6) 在(5)中,用到了进程验证类.
// 模块编号:
// 文件名: UserVerfiyOperation.cs
// 描述: UserVerfiyOperation 进程验证类
// 作者:ChenJie
// 编写日期:2007-5-6
// Copyright 2007
//-----------------------------------------------------------------------------------------
using System;
using System.Runtime.Remoting.Messaging;
using Novelty.Model;
using Novelty.WinServices;
namespace Novelty.WinServerFactory
{
/// <summary>
/// 进程验证类
/// </summary>
public class UserVerfiyOperation
{
/// <summary>
/// 默认的构造函数
/// </summary>
public UserVerfiyOperation()
{
}
/// <summary>
/// 验证用户的请求服务是否合法
/// </summary>
/// <returns></returns>
public bool VerfiyUser()
{
bool sucess = false;
TicketIdentity ticketIdentity = CallContext.GetData("TicketIdentity") as TicketIdentity;
if (ticketIdentity != null)
{
lock (AuthorizationModule.Instance)
{
if (AuthorizationModule.Instance.Tickets.Contains(ticketIdentity.UserName))
{
string clientAddress = AuthorizationModule.Instance.Tickets[ticketIdentity.UserName].ToString();
if (clientAddress.Equals(ticketIdentity.HostAddress))
{
sucess = true;
}
}
}
}
return sucess;
}
}
}
那么,在这里,远程组件类就结束了!我们要如何扩展上面远程服务呢?正如我们需要的,我们继续写一个或者多个远程服务类,然后在(5)用户服务抽象工厂类为每个类增加一个函数,用来为每个类创建一个对象并通过验证后返回.这里我们的方案就明朗了:用抽象工厂类的方法来创建每个远程服务类的对象,并在抽象工厂类的方法里进行验证,这样,我们就不需要为每个远程服务类的方法来验证.打个比方,一幢宿舍楼,有很多房间,以前是为每个房间加把锁,这样很麻烦,现在是在楼底入口处加把锁而不再为每个房间打锁,大大减轻了验证的工作.那么这个楼底入口处就是我们的抽象工厂类创建对象的方法,而抽象工厂类就是一个大院子,里面可以很多的服务类,就象院子里可以有很多幢楼一样.这样就完成了我们的扩展.原本我想在大院子里加把锁,就是在抽象工厂类的构造函数中来验证用户,可以没有成功,因为CallContext.GetData获得的对象始终为空.
我们继续给出客户端类和服务器端类.
3 服务器端类.
本文只给出一些基本的代码.
/// 启动服务
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnStart_Click(object sender, EventArgs e)
{
ChannelType channelType = SystemConfigFacade.CurrentChannelConfig.CurrentChannelType;
string name = SystemConfigFacade.CurrentChannelConfig.Name;
string port = SystemConfigFacade.CurrentChannelConfig.Port;
RegisteredChannel(channelType, name, port);
RemotingConfiguration.RegisterWellKnownServiceType(typeof(UserVerfiyFactory), ConfigurationManager.AppSettings["UserAccountCalling"], WellKnownObjectMode.Singleton);
RemotingConfiguration.RegisterWellKnownServiceType(typeof(UserAccountFactory), ConfigurationManager.AppSettings["UserAccountBlcok"], WellKnownObjectMode.Singleton);
}
4、客户端类
这里也只给出了核心的代码,他们使用了两个远程服务,一个是用户身份验证,验证正确后将用户名和客户端地址保存起来.验证结束后,如果客户端调用其他远程服务,就需要先将用户名和客户端地址通过CallContext.SetData附加到进程中,然后再调用远程服务.本文的例子就是调用服务来返回一个用户对象,这个请求需要验证.服务器端通过CallContext.SetData来获得附加的信息,即用户名和客户端地址,然后和已保存的信息来检验,成功后即返回请求的结果.
{
/// <summary>
/// UserVerfiyFactory 远程访问实例
/// </summary>
private readonly IUserVerfiyFactory userVerfiyFactory;
/// <summary>
/// 登录通用操作对象
/// </summary>
private readonly LoginOperation loginOperation;
public LoginForm()
{
InitializeComponent();
RegisterChannel();
loginOperation = new LoginOperation();
string userAccountCallingName = ConfigurationManager.AppSettings["UserAccountCalling"];
string userAccountCallingURI = loginOperation.GetServiceAddress(userAccountCallingName);
userVerfiyFactory = (IUserVerfiyFactory)Activator.GetObject(typeof(IUserVerfiyFactory), userAccountCallingURI);
}
private void btnConfirm_Click(object sender, EventArgs e)
{
try
{
IPAddress clinetIP = loginOperation.GetClinetIP();
IUserAccountCalling userAccountCalling = userVerfiyFactory.CreateUserAccount();
// 使用不需要验证的远程服务: 调用远程对象的代理对象来验证用户登陆是否正确
bool result = userAccountCalling.ValidateUser(userName, userPwd, clinetIP.ToString());
if (result)
{
TicketIdentity ticketIdentity = new TicketIdentity(userName, clinetIP.ToString());
// 使用CallContext将 ticketIdentity 附加到上下文中
CallContext.SetData("TicketIdentity", ticketIdentity);
string userAccountBlcokName = ConfigurationManager.AppSettings["UserAccountBlcok"];
string userAccountBlcokURI = loginOperation.GetServiceAddress(userAccountBlcokName);
IUserAccountFactory userAccountFactory = (IUserAccountFactory)Activator.GetObject(typeof(IUserAccountFactory), userAccountBlcokURI);
//保存当前用户信息
IUserAccountModule userAccountModule = userAccountFactory.CreateUserAccount();
// 使用需要验证的远程服务: 调用远程对象的代理对象来获得用户对象
CurrentUserGlobalInfo.Instance.CurrentUserAccountInfo = userAccountModule.GetUserAccountInfo(userName);
}
}
catch (Exception exception)
{
Cursor = Cursors.Default;
//记录日志, 不抛出异常, 包装异常
ExceptionFacade.WinFormsLog(exception);
}
}
}
.net 2.0 remoting 中 TCP Channel 用户认证解决方案就讨论到这里.随便提一下问中开头说的“可以在Facade使用验证”这句是如何提醒了我的.也许大家都明白了,我突然想到为什么不在抽象工厂验证呢?也许那位网友就是这个意思,只是他认为该模式属于Facade(装饰)模式,而我认为是抽象工厂模式.我的理由是因为是该类是为了为各个类创建了各自的对象.而装饰模式一般把总多的类抽象出来成一个简单的类,隐藏复杂性,它不存在创建新的对象.在.Net Pet Shop 4.0 的抽象工厂模式便采用了反射的方式来创建众多类的各个对象的.
本文到这里就结束!
======================================================================
5月10号追加:
经过继续的实践,发现在上面给出的客户端类不够合理.比如,我们需要在不同的客户端使用同一个远程服务,如果按照上面给出的写法,意味着我们必须写两份相同的代码(用来获得远程服务对象的代理对象).如果我们只要在客户端写一份访问代码,客户端其他地方都可以使用,就简化了访问过程.
那么上面的客户端类该如何拆分呢?我们可以采用与远程组件类相同的思想,使用一个提供静态访问方法的客户端抽象工厂类.
客户端抽象工厂类:
/// 返回 UserAccountBlcok 模块提供的远程服务
/// </summary>
public static class UserAccountBlcok
{
/// <summary>
/// 登录通用操作对象
/// </summary>
private static readonly IUserAccountFactory userAccountFactory;
/// <summary>
/// 登录用户名称
/// </summary>
private static string clinetUserName;
/// <summary>
/// 登录通用操作对象
/// </summary>
private static readonly string clinetIPAddress;
/// <summary>
/// 静态构造函数
/// </summary>
static UserAccountBlcok()
{
LoginOperation loginOperation = new LoginOperation();
IPAddress clinetIP = loginOperation.GetClinetIP();
clinetIPAddress = clinetIP.ToString();
string userAccountBlcokName = ConfigurationManager.AppSettings["UserAccountBlcok"];
string userAccountBlcokURI = loginOperation.GetServiceAddress(userAccountBlcokName);
userAccountFactory = (IUserAccountFactory)Activator.GetObject(typeof(IUserAccountFactory), userAccountBlcokURI);
}
/// <summary>
/// 获得 IUserAccountModule 远程访问对象的代理对象
/// </summary>
/// <param name="userName">用户名</param>
/// <returns>代理对象</returns>
public static IUserAccountModule GetUserAccountModule(string userName)
{
IUserAccountModule userAccountModule = null;
try
{
clinetUserName = userName;
CallContextToSetData();
userAccountModule = userAccountFactory.CreateUserAccount();
}
catch (Exception exception)
{
//记录日志, 不抛出异常, 包装异常
ExceptionFacade.NoLogAndThrowAndNoWrapPolicy(exception);
}
return userAccountModule;
}
/// <summary>
/// 使用CallContext将 ticketIdentity 附加到上下文中
/// </summary>
private static void CallContextToSetData()
{
TicketIdentity ticketIdentity = new TicketIdentity(clinetUserName, clinetIPAddress);
CallContext.SetData("TicketIdentity", ticketIdentity);
}
}
客户端类访问远程对象的代理对象的代码就简化成下面的:
namespace Novelty.WinFormsClient
{
public partial class LoginForm : Form
{
/// <summary>
/// UserVerfiyFactory 远程访问实例
/// </summary>
private readonly IUserVerfiyFactory userVerfiyFactory;
/// <summary>
/// 登录通用操作对象
/// </summary>
private readonly LoginOperation loginOperation;
public LoginForm()
{
InitializeComponent();
RegisterChannel();
loginOperation = new LoginOperation();
string userAccountCallingName = ConfigurationManager.AppSettings["UserAccountCalling"];
string userAccountCallingURI = loginOperation.GetServiceAddress(userAccountCallingName);
userVerfiyFactory = (IUserVerfiyFactory)Activator.GetObject(typeof(IUserVerfiyFactory), userAccountCallingURI);
}
/// <summary>
/// 注册通道
/// </summary>
private void RegisterChannel()
{
TcpClientChannel tcpClientChannel = new TcpClientChannel();
ChannelServices.RegisterChannel(tcpClientChannel, true);
}
private void btnConfirm_Click(object sender, EventArgs e)
{
try
{
IPAddress clinetIP = loginOperation.GetClinetIP();
// 使用不需要验证的远程服务: 调用远程对象的代理对象来验证用户登陆是否正确
IUserAccountCalling userAccountCalling = userVerfiyFactory.CreateUserAccount();
//验证当前用户信息
bool result = userAccountCalling.ValidateUser(userName, userPwd, clinetIP.ToString());
Cursor = Cursors.Default;
if (result)
{
// 使用需要验证的远程服务: 调用远程对象的代理对象来获得用户对象
//我们发现,在这里大大简化了访问使用验证的远程服务的代码
//如果客户端其他地方也要用到该远程对象的代理对象,使用方法一样。
IUserAccountModule userAccountModule = UserAccountBlcok.GetUserAccountModule(userName);
//保存当前用户信息
CurrentUserGlobalInfo.Instance.CurrentUserAccountInfo = userAccountModule.GetUserAccountInfo(userName);
}
}
catch (Exception exception)
{
btnConfirm.Enabled = false;
//记录日志, 不抛出异常, 包装异常
ExceptionFacade.WinFormsLog(exception);
Cursor = Cursors.Default;
}
}
}
}
这样,客户端的远程对象的代理对象的访问的通用性就大的多了!(有点拗口!:)) 补充就结束了!