逍遥居

学无止境

导航

关于.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>
        
/// 启动服务
        
/// </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来获得附加的信息,即用户名和客户端地址,然后和已保存的信息来检验,成功后即返回请求的结果.

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);            
        }


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号追加:
经过继续的实践,发现在上面给出的客户端类不够合理.比如,我们需要在不同的客户端使用同一个远程服务,如果按照上面给出的写法,意味着我们必须写两份相同的代码(用来获得远程服务对象的代理对象).如果我们只要在客户端写一份访问代码,客户端其他地方都可以使用,就简化了访问过程.
那么上面的客户端类该如何拆分呢?我们可以采用与远程组件类相同的思想,使用一个提供静态访问方法的客户端抽象工厂类.

客户端抽象工厂类:

/// <summary>
    
/// 返回 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;
            }

        }
      
    }

}

     这样,客户端的远程对象的代理对象的访问的通用性就大的多了!(有点拗口!:)) 补充就结束了!

 

posted on 2007-05-09 22:56  逍遥剑客  阅读(1736)  评论(6编辑  收藏  举报