带安全验证的远程调用(Tcp Channel)
在.net 2.0中,在使用 remoting 的 TCP Channel, 用户认证是安全性问题探讨主题之一.本文将从两个角度来探讨用户认证问题, 并提出一个问题来寻求大家的解决方法!
一、两个通道类的区别
Tcp Channel :
服务器端注册通道
方式一:
(1)注册一个通道
TcpChannel channel = new TcpChannel(8086);
ChannelServices.RegisterChannel(channel, true);
(2)注册多个通道
由于IChannel的ChannelName属性和ChannelPriority属性都是只读的,所以通过下面的方式。
IDictionary props = new Hashtable();
props["name"] = "channelName";
props["port"] = 8086;
IChannel channel = new TcpChannel(props, new BinaryClientFormatterSinkProvider(), new BinaryServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel, true);
方式二:
(1)注册一个通道
TcpServerChannel channel=new TcpServerChannel(8086);
ChannelServices.RegisterChannel(channel, true);
(2)注册多个通道
IDictionary props = new Hashtable();
props["name"] = "channelName";
props["port"] = 8086;
IChannel channel = new TcpChannel(props, new BinaryServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel, true);
(3)注册带客户端验证的通道
IAuthorizeRemotingConnection authorizeCallback = (IAuthorizeRemotingConnection)new AuthorizationModule ();
channel = new TcpServerChannel(props, new BinaryServerFormatterSinkProvider(), authorizeCallback);
ChannelServices.RegisterChannel(channel, true);
客户端注册通道
针对服务器端注册通道方式一的两类情况均采用下面的方式注册即可:
TcpChannel channel = new TcpChannel();
ChannelServices.RegisterChannel(channel, true);
针对服务器端注册通道方式二的采用下面的方式注册即可:
(1)和(3)的对应
TcpClientChannel tcpClientChannel = new TcpClientChannel();
ChannelServices.RegisterChannel(tcpChannel, true);
(2)的对应
TcpClientChannel tcpClientChannel = new TcpClientChannel("channelName", new BinaryClientFormatterSinkProvider());
ChannelServices.RegisterChannel(tcpChannel, true);
从上面的演示可以看出:
(1) TcpChannel类都可以用在客户端和服务器端,它们都实现了IChannelReceiver, IChannelSender。
在客户端使用时,一般采用
TcpChannel channel = new TcpChannel();
TcpChannel () 初始化 TcpChannel 类的新实例,仅激活客户端信道,不激活服务器信道。
在服务器端使用时,一般采用
IChannel channel = new TcpChannel(props, new BinaryClientFormatterSinkProvider(), new BinaryServerFormatterSinkProvider());
可以指定的配置属性和接收器初始化 TcpChannel 类的新实例。IClientChannelSinkProvider 为远程处理消息从其流过的客户端信道创建客户端信道接收器。 IServerChannelSinkProvider 为远程处理消息从其流过的服务器信道创建服务器信道接收器。
(2)
TcpServerChannel 类用在客户端, TcpClientChannel 类用在服务器端。(这句好象是废话:) )
区别:TcpChannel类是一个通用的信道类,在客户端和服务器端都可以使用,使用起来非常方便。TcpServerChannel 类和TcpClientChannel 类需要分别使用,但是如果你要通过编程的方式使用.Net 2.0 中的客户端验证,那就要使用TcpServerChannel 来完成了。当然,也可以通过配置文件的方式来完成对客户端验证。还有一点,使用TcpChannel类可以在服务器指定客户端信道接收器和服务器信道接收器,客户端不用管,而TcpServerChannel 类和TcpClientChannel 类则分别指定自己的信道接收器。(不知道这句话是否正确?:))
二、下面就来从两个角度来探讨一下TCP Channel 用户认证.
方式一:采用编程的方式和配置文件的方式来完成对客户端验证。
先给出授权的类文件代码:
程序集名称为AuthorizationAssemblyName,命名空间为:AuthorizationNameSpace。
class AuthorizationModule : IAuthorizeRemotingConnection
{
public bool IsConnectingEndPointAuthorized(System.Net.EndPoint endPoint)
{
//验证IP地址代码.....
return true;
}
public bool IsConnectingIdentityAuthorized(IIdentity identity)
{
//Windows用户名identity.Name
//Windows用户验证代码.....
return true;
}
}
IAuthorizeRemotingConnection接口有两个方法, IsConnectingEndPointAuthorized 获取一个布尔值,该值指示客户端的网络地址是否已被授权连接至当前信道。IsConnectingIdentityAuthorized 获取一个布尔值,该值指示客户端的用户标识是否已被授权连接至当前信道。
(1) 配置文件的方式:
服务器端的配置文件:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown mode="SingleCall" type="NameSpace.ClassName, AssemblyName" objectUri="server.rem" />
</service>
<channels>
<channel ref="tcp" secure="true" port="8086" impersonate="true" authorizationModule="AuthorizationNameSpace.AuthorizationClassName, AuthorizationAssemblyName, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
(2) 编程的方式:
IAuthorizeRemotingConnection authorizeCallback = (IAuthorizeRemotingConnection)new AuthorizationModule();
IChannel channel = new TcpServerChannel(props, new BinaryServerFormatterSinkProvider(), authorizeCallback);
ChannelServices.RegisterChannel(channel, true);
以上两种验证方式是针对IP地址和Windows用户。只要客户端调用远程服务,就是会自动执行上面接口中两个函数。虽然在一定程度上实现了安全性验证,但是仍然不是我们想要的结果。我们希望的方式是:对于给定的服务(也就是远程对象提供的方法),一小部分不需要授权,比如登录验证服务,大部分服务需要授权验证,比如管理员删除用户;在要授权验证时,采用的自动提交验证信息的方式,比如删除用户,我们希望是只传删除用户的ID,
即 DeleteUserById(string userId), 而不是连管理员的用户密码也一起作为参数传过去,即DeleteUserById(string adminId,adminpwd,string userId)。那么,下面就来探讨一下解决这个问题的答案:在远程处理中,可以通过CallContext类获得。
方式二:在远程处理中,可以通过CallContext类获得方式来完成对客户端验证。
第一步,创建自定义的类 PrincipalStorage,封装需要自动传递的消息。该对象需要序列化,且需要实现ILogicalThreadAffinative接口。
using System;
using System.Runtime.Remoting.Messaging;
using System.Security.Principal;
namespace Novelty.WinServices
{
public class PrincipalStorage : ILogicalThreadAffinative
{
private GenericPrincipal _currentGenericPrincipal;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="currentGenericPrincipal"></param>
public PrincipalStorage(GenericPrincipal currentGenericPrincipal)
{
_currentGenericPrincipal = principal;
}
/// <summary>
/// 用户对象的基本功能
/// </summary>
public GenericPrincipal CurrentGenericPrincipal
{
get
{
return _currentGenericPrincipal;
}
}
}
}
(2)客户端创建该对象并使用CallContext.SetData方法附加到上下文中,它能通过每个请求自动的传输到远程组件中。
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
namespace Novelty.WinServices
{
/// <summary>
/// 客户端
/// </summary>
class RemotingPrincipalClient
{
[STAThread]
static void Main(string[] args)
{
//注册客户端信道和对象等等省略
//RemObject remObject = new RemObject();
GenericIdentity genericIdentity = new GenericIdentity("UserName");
GenericPrincipal genericPrincipal = new GenericPrincipal(genericIdentity, new string[] { "admin", "user" });
PrincipalStorage principalStorage = new PrincipalStorage(genericPrincipal);
// 使用CallContext将.principal 附加到上下文中
CallContext.SetData("Principal", principalStorage);
//调用远程方法,这里不需要显示传递用户信息
remObject.ExampleMethod();
}
}
}
(3)在远程组件中获取信息并验证它
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
namespace Novelty.WinServices
{
/// <summary>
/// 远程类
/// </summary>
public class RemObject : MarshalByRefObject
{
public RemObject()
{
}
public void ExampleMethod()
{
PrincipalStorage principalStorage = CallContext.GetData("Principal") as PrincipalStorage;
if (ppal == null)
{
return;
}
// 获得用户名
string userName = principalStorage.CurrentGenericPrincipal.Identity.Name;
//验证用户信息.....
//具体方法实现
//.......
}
}
}
当然,这个例子不是很恰当,因为只传递了用户名和角色!可以修改自定义的类 PrincipalStorage ,让它仅包含用户名称和密码。或者通过登录验证函数时返回给客户端一个Guid,在服务器端也保存这个Guid,然后通过自动传递Guid来验证。
上面的两种客户端验证可以一起使用,本来写到这里也该结束了,但是我还是有一个疑问:方法一的优点是不需要在具体服务(远程对象的方法)中进行验证,但是缺点是无法针对具体方法进行验证,而且验证的方式有局限性。方法二的优点是针对具体方法进行验证,验证的方式可以自己扩展,但是也存在缺点,那就是必须在需要验证的方法中添加验证。也许有人认为这是句废话,但是我想解释一下。如果能够象ASP.NET 2.0一样,只需要embership的Provider,那么只在登录页面用登录控件,就可以实现对所有的页面进行用户验证,而且在web.config中设置不需要验证的页面。在页面中,看不到任何验证用户的代码。我的意思是,是否可以不在具体服务(远程对象的方法)进行验证,而是通过在使用配置文件或是在某个地编写一小段代码,就可以有选择性的对具体服务(远程对象的方法)进行验证?这样,具体服务(远程对象的方法)的内容与验证就脱离开来,只需要对自己的任务负责,不去管验证的事情。而客户端只需要在登录验证通过后就可以调用其角色授权的方法,而不需要再在调用方法时还先发一下上下文信息。
参考照料:
(1) http://dotnetwithme.blogspot.com/2007/01/how-to-build-authorization-module-for.html
(2)MSDN