Chinaunix首页 | 论坛 | 博客
  • 博客访问: 88877
  • 博文数量: 20
  • 博客积分: 2010
  • 博客等级: 大尉
  • 技术积分: 200
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-15 10:49
文章分类

全部博文(20)

文章存档

2009年(2)

2008年(18)

我的朋友
最近访客

分类: WINDOWS

2008-12-01 11:54:41

节选自《ASP.NET 2.0揭秘》2.3节

 

使用ASP.NET Membership

通过ASP.NET Membership,我们可以创建用户、删除用户和编辑用户属性。所以这是一个实现登录相关控件的底层框架。

ASP.NET Membership的内容是在Forms鉴别完成后填入的。Forms鉴别提供的是一种验证用户的方法,而ASP.NET Membership的作用是表示用户的信息。

ASP.NET Membership使用的是提供器模式。ASP.NET Framework内包含了两个Membership提供器:

q SqlMembershipProvider——通过微软SQL Server数据库保存用户信息;

q ActiveDirectoryMembershipProvider——通过活动目录或活动目录应用程序模式服务器端保存用户信息。

在本节中,将介绍如何使用ASP.NET Membership API。介绍如何使用Membership类来通过编程方式修改Membership实例中表示的信息。

也还会介绍如何配置SqlMembershipProvider和ActiveDirectoryMembershipProvider。例如,将介绍如何修改有效的Memebership密码的必要条件。

最后,我们将构建一个自定的Membership提供器。这将是一个把成员信息保存在XML文件中的XmlMembershipProvider提供器。

2.3.1  使用Membership API

ASP.NET Membership提供的主要API是Membership类,该类支持下列方法:

q CreateUser——用于创建新用户;

q DeleteUser——用于删除已存在的用户;

q FindUsersByEmail——用于获得使用特定电子邮件地址的所有用户;

q FindUsersByName——用于获得使用特定用户名的所有用户;

q GeneratePassword——用于产生随机密码;

q GetAllUsers——用于获得所有用户;

q GetNumberOfUsersOnline——用于获得所有在线用户的人数;

q GetUser——用于通过用户名获得用户;

q GetUserNameByEmail——用于获得使用特定电子邮件地址的那位用户;

q UpdateUser——用于更新用户信息;

q ValidateUser——用于验证用户名和密码。

该类还支持下列事件:

q ValidatingPassword——当进行用户密码校验时触发,可以通过处理该事件来执行自定义的验证算法。

通过使用Membership类所提供的方法,可以对Web站点中的用户进行管理。例如,代码清单2-15中的页面显示了该应用程序中所有已注册用户的列表(见图2-5)。

图2-5  显示已注册用户

代码清单2-15  ListUsers.aspx

<%@ Page Language="C#" %>

"">

    List Users

   

   

   

   

        id="grdUsers"

        DataSourceID="srcUsers"

        Runat="server" />

   

   

        id="srcUsers"

        TypeName="System.Web.Security.Membership"

        SelectMethod="GetAllUsers"

        Runat="server" />

   

   

   

在代码清单2-15中,ObjectDataSource控件被用来表示Membership类的数据源。通过调用Get- AllUser()方法可以得到所有用户的清单。

通过Membership类的方法可以创建自定义的Login控件。例如,可以通过调用GetNumberOfUser- Online()方法得到当前应用程序的在线用户数。代码清单2-16中的自定义控件显示该调用该方法所返回的值。

注解   在第12章将详细讨论自定义控件的创建问题。

代码清单2-16  UsersOnline.cs

using System;

using System.Web.Security;

using System.Web.UI;

using System.Web.UI.WebControls;

namespace myControls

{

    ///

    /// Displays Number of Users Online

    ///

    public class UsersOnline : WebControl

    {

        protected override void RenderContents(HtmlTextWriter writer)

        {

            writer.Write(Membership.GetNumberOfUsersOnline());

        }

    }

}

代码清单2-17中的页面使用UsersOnline控件来显示了当前应用程序中在线用户的数量(见图2-6)。

图2-6  显示当前在线用户数量

代码清单2-17  ShowUsersOnline.aspx

<%@ Page Language="C#" %>

<%@ Register TagPrefix="custom" Namespace="myControls" %>

"">

    Show UsersOnline

   

   

   

    How many people are online?

   

   

        id="UsersOnline1"

        Runat="server" />

   

   

   

注解   如果某用户名在最近15分钟内被ValidateUser()、UpdateUser()或GetUser()方法所使用,那么我们就认为该用户在线。同时也可以通过修改Web配置文件中membership节点的userIsOnlineTimeWindow属性来更改15分钟这个默认的时间间隔设置。

有些Membership类的方法会返回一个或多个MembershipUser对象。MembershipUser对象用于表示一个特定Web站点成员,同时该类支持下列属性:

q Comment——用于将注解关联到当前用户上;

q CreationDate——用于获取当前用户的创建日期;

q Email——用于获取或设置当前用户的电子邮箱地址;

q IsApproved——用于获取或设置当前用户是否已被核准并且他的账号是否已激活;

q IsLockedOut——用于获取当前用户的锁定状态;

q IsOnline——用于确认当前用户是否在线;

q LastActivityDate——用于获取或设置当前用户最后活动的日期。该日期会在调用CreateUser()、ValidateUser()或GetUser()方法时自动更新;

q LastLockoutDate——用于获取当前用户最后被锁定的日期;

q LastLoginDate——用于获取当前用户最近一次登录该应用程序的时间;

q LastPasswordChangedDate——用于获得当前用户最近一次修改其密码的日期;

q PasswordQuestion——用于获得当前用户密码提示问题;

q ProviderName——用于获得关联到当前用户上的Membership提供器的名称;

q ProviderUserKey——用于获得一个关联于当前用户的唯一键值。在使用SqlMembershipProvider提供器时,该值为一个GUID;

q UserName——用于获得当前用户的用户名。

需要注意的是MembershipUser类中并不包含与用户密码和密码提示问题相关的属性。这是有意如此设计的,如果需要更改用户的密码,那么就需要到用该类所提供的方法。

MembershipUser类支持下列方法:

q ChangePassword——用于更改当前用户的密码;

q ChangePasswordQuestionAndAnswer——用于更改当前用户密码和密码提示问题;

q GetPassword——用于获得该用户的密码;

q ResetPassword——用于将当前用户的密码重置为随机产生的密码;

q UnlockUser——用于解锁被锁定的用户账号。

2.3.2  加密和散列用户密码

使用ASP.NET Framework的两个默认Membership提供器,我们可以有三种方式来保存用户的密码:

q Clear——密码以明文方式进行保存;

q Encrypted——保存密码之前对其进行加密处理;

q Hashed——原始密码不会被保存,而只保存密码的散列值(这是默认设置)。

要配置如何对密码进行保存,需要设置Web配置文件中的passwordFormat属性。例如,代码清单2-18中的Web配置文件设置了SqlMembershipProvider以明文方式保存用户密码。

代码清单2-18  Web.Config

   

     

     

       

         

            name="MyProvider"

            type="System.Web.Security.SqlMembershipProvider"

            passwordFormat="Clear"

            connectionStringName="LocalSqlServer"/>

       

       

       

     

   

属性passwordFormat的默认值是Hashed。在此默认情况下,实际的密码不会保存在应用程序的任何地方。而是根据该密码所生成的散列值被保存了起来。

注解   散列算法会为每一个输入数据生成一个唯一的散列值。另外,散列算法最大的特点是,该过程是单向不可逆的。虽然我们可以很容易地根据任何值来生成其散列值,然而几乎不太可能再从散列值来确定其原始值。

保存密码散列值的优势是,即使黑客入侵了我们的Web站点,他也不能窃取到任何人的密码。然而同时也会有不足,就是谁也不能重新得到用户密码。例如,在这样的系统中,就不能使用PasswordRecovery控件来通过电子邮件向其用户发送原始码。

除了对密码进行散列之外,另一个保护密码的方法就是对其进行加密处理。加密密码的缺点是,它会比散列密码需要更强的运算处理逻辑。不过这样也有优点,可以根据加密信息为用户重新找回其原始密码。

代码清单2-19中的Web配置文件设置SqlMembershipProvider对密码进行加密保存。不过需要注意的是Web配置文件包含了一个machineKey节点,当要对密码加密时,必须为其decryptionKey属性提供一个明确的密钥。

注解   关于machineKey节点的更多信息,见2.1.4节。

代码清单2-19  Web.Config

 

   

   

     

       

          name="MyProvider"

          type="System.Web.Security.SqlMembershipProvider"

          passwordFormat="Encrypted"

          connectionStringName="LocalSqlServer"/>

     

   

   

        decryption="AES"

        decryptionKey="306C1FA852AB3B0115150DD8BA30821CDFD125538A0C606DACA53DBB3C3E0AD2" />

 

注意   确认在使用代码清单2-19中的Web配置文件之前,修改了decryptionKey属性的值。我们可以使用在2.1.4节中讨论的GenerateKeys.aspx页面来生成新的decryptionKey。

2.3.3  修改用户密码条件

          在默认情况下,密码至少需要包含7个字符和1个非文字数字的字符(比如*、_或!等既不是字母也不是数字的字符)。我们可以通过设置以下三个Membership提供器的属性来修改密码设置策略:

q minRequiredPasswordLength——密码最小长度限制(默认值是7);

q minRequiredNonalphanumericCharacters——密码中非文字数字字符的最少个数(默认值是1);

q passwordStrengthRegularExpression——密码必须要正确匹配的正则表达式模式(默认值是一个空字符串)。

属性minRequiredNonalphanumericCharacters常常会使得用户很迷惑,因为大多数Web站点的用户并不熟悉所谓必须输入非文字数字的这一要求。代码清单2-20中的Web配置文件展示了在使用SqlMembershipProvider时如何禁用这一要求。

代码清单2-20  Web.Config

 

   

   

     

       

          name="MyProvider"

          type="System.Web.Security.SqlMembershipProvider"

          minRequiredNonalphanumericCharacters="0"

          connectionStringName="LocalSqlServer"/>

     

   

 

2.3.4  锁定坏用户

在默认情况下,如果有用户在10分钟内连续5次输入了错误的密码,那么该账号将被自动锁定。也就是说,该账号就不能正常登录了。

同样,如果用户在10分钟内连续5次输入了错误的密码提示问题,那么该账号也将被锁定。也就是说,在一定的时间间隔内每位用户最多只能进行5次密码和5次密码提示的确认尝试(这两个限制的次数是独立进行计数的)。

下面两个配置选项将控制账号在什么条件下会被锁定:

q maxInvalidPasswordAttempts——在一定时间间隔内允许用户最多可以输入的错误密码和错误密码提示问题的次数(默认值是5);

q passwordAttemptWindow——用于设置以分钟为单位的时间间隔,在该时间间隔内用户输入了过量的错误密码或错误的密码提示问题,那么该账号就将被锁定。

代码清单2-21  Web.Config

 

   

   

     

       

          name="MyProvider"

          type="System.Web.Security.SqlMembershipProvider"

          maxInvalidPasswordAttempts="3"

          passwordAttemptWindow="60"

          connectionStringName="LocalSqlServer"/>

     

   

 

当一个用户被锁定后,必须调用MembershiptUser.UnlockUser()方法来重新启用该用户的账号。代码清单2-22中的页面用于输入特定用户名并对其进行解除锁定(见图2-7)。

图2-7  移除对账号的锁定

代码清单2-22  RemoveLock.aspx

<%@ Page Language="C#" %>

"">

    Remove Lock

   

   

   

   

        id="lblUserName"

        Text="User Name:"

        AssociatedControlID="txtUserName"

        Runat="server" />

   

        id="txtUserName"

        Runat="server" />

   

        id="btnRemove"

        Text="Remove Lock"

        Runat="server" OnClick="btnRemove_Click" />

   

   

        id="lblMessage"

        EnableViewState="false"

        Runat="server" />

   

   

2.3.5  配置SQLMembershipProvider提供器

SQLMembershipProvider是默认的Membership提供器。除非额外进行配置,否则在应用程序的App_Data文件夹中的微软SQL Server精简版数据库ASPNETDB.mdf将用来保存成员信息。该数据库第一次使用在Membership时自动创建。

如果希望将成员信息保存到其他的微软SQL Server数据库中,那么需要执行以下两个步骤:

q 将必要的数据库对象添加到微软SQL Server数据库中;

q 配置应用程序使用新的数据库。

为了完成第一个步骤,需要使用aspnet_regsql命令行工具。该工具位于如下文件夹中:

\WINDOWS\Microsoft.NET\Framework\v2.0.50727

注解   如果直接打开了SDK命令行工具,那么在使用aspnet_regsql命令之前就不再需要手动定位到Microsoft .NET文件夹了。

如果不带参数执行aspnet_regsql命令,那么将会启动ASP.NET SQL Server安装向导(见图2-8)。通过该向导可以选择数据库并自动安装Membership对象。

此外,还可以选择通过执行下列两个SQL批处理文件来代替使用aspnet_regsql工具,以安装Membership相关数据库项:

\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallCommon.sql

\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallMembership.sql

如果你不希望在你的数据库服务器端上安装.NET Framework,那么就可以执行这些SQL批处理文件。

当数据库已配置来支持ASP.NET Membership之后,还必须配置应用程序以使其在使用Membership时能连接到该数据库上。代码清单2-23中的Web配置文件用于连接到位于MyServer服务器端上名为MyDatabase的数据库。

图2-8  使用ASP.NET SQL安装向导

代码清单2-23  Web.Config

 

    Catalog=MyDatabase"/>

 

 

   

   

     

       

          name="MyMembershipProvider"

          type="System.Web.Security.SqlMembershipProvider"

          connectionStringName="MyConnection" />

     

   

 

在代码清单2-23中,配置了一个名为MyMembershipProvider的新Membership默认提供器。新的成员提供器使用的连接字符串名值为MyConnection。而MyConnection被定义在配置文件的上部的connectionStrings节点中。该连接字符串表示了连接到位于MyServer服务器端上名为MyDatabase的数据库。

2.3.6  配置ActiveDirectoryMembershipProvider

ActiveDirectoryMembershipProvider是包含在ASP.NET Framework中的另一个Membership提供器。通过这个提供器,我们可以将用户的信息保存在活动目录或ADAM(应用模式活动目录)。

ADAM是活动目录的一个轻量级版本实现。我们可以从微软Web站点( com/adam)上下载ADAM。ADAM兼容于微软Windows Server 2003和微软Windows XP Professional(Service Pack1)这两类操作系统。

如果要在ADAM上使用ASP.NET Membership,那么必须要完成以下两个步骤:

(1) 创建ADAM实例和必须的相关类。

(2) 配置应用程序使用ActiveDirectoryMembershipProvider,并将其连接到ADAM实例上。

在接下来的部分中,将按这些步骤依次进行检验。

1. 配置ADAM

首先,需要安装ADAM新实例。在下载并安装好ADAM后,请遵循以下步骤进行设置:

(1) 通过从程序组ADAM菜单中选择创建ADAM实例,将会运行应用模式活动目录安全向导(见图2-9)。

(2) 在安装程序的选项设置步骤中,选择创建唯一实例选项。

(3) 在实例名设置步骤中,输入名称:WebUsersInstance。

(4) 在端口设置步骤中,使用默认的LDAP和SSL端口号(389和636)。

(5) 在活动目录分区设置步骤中,创建一个名为O=WebUsersDirectory的新目录应用分区。

(6) 在文件位置设置步骤中,使用默认的数据文件位置。

(7) 在运行服务账号选取步骤中,选择账号Network Service。

(8) 在ADAM管理设置步骤中,为管理员账号选择Currently Logged on User选项。

(9) 在导入LDIF文件步骤中,选择MS-AZMan.ldf、MS-InetOrgPerson.ldf、MS-User.ldf和MS-UserProxy.ldf。

当完成了上这些步骤之后,一个名为WebUsersInstance的新ADAM实例就创建好了。下一步是配置ADAM管理员账号,遵循下列步骤。

图2-9  创建新的ADAM实例

注意   如果使用Windows XP系统,SSL证书默认并未安装,那么就需要再执行一些额外的安装步骤。否则,当尝试重置用户密码是会收到一个错误提示。
在默认情况下,通过非安全连接方式连接到ADAM实例将不能执行与密码相关的操作。可以通过ADAM自带工具dsmgmt.exe来解除该限制。打开ADAM命令行工具并输入如下一系列命名:

(1) 输入dsmgmt。

(2) 输入ds behaior。

(3) 输入connections。

(4) 输入connect to server localhost:389。

(5) 输入quit。

(6) 输入allow passwd op on unsecured connection。

(7) 输入quit。

如果没有使用SSL连接,那么密码将以明文方式传输。需要注意的是,一定不要在实际的产品中使用这样的部署方式。

(1) 通过程序组ADAM菜单运行ADAM ADSI Edit应用程序(见图2-10)。

(2) 通过选择菜单选项:Action(动作)→Connect to(连接到),打开连接设置对话框。

(3) 在连接设置对话框中,通过使用特别的名称并输入名称O=WebUsersDirectory,来选择连接到节点的选项。然后点击OK。

(4) 展开新连接并选中O=WebUsersDiretory节点。

(5) 选择菜单选项:Action(动作)→New(新建)→Object(对象)。

(6) 在创建对象对话框中,选择organizationalUnit类并将新类命名为WebUsers。

(7) 选中OU=WebUsers节点,并选择菜单选项:Action(动作)→New(新建)→Object(对象)。

(8) 在创建对象对话框中,选择用户类并将新类命名为ADAMAdministrator。

(9) 选中CN=ADAMAdministrator,并选择菜单选项:Action(动作)→Reset Password(重置密码),然后输入密码secret_。

(10) 选中CN=Roles节点,并双击CN-Administrators节点。

(11) 最后用鼠标双击Member属性,并同时为ADAM账号ADAMAdministrator ( CN=ADAMAdmini- strator, OU=WebUsers, O=WebUsersDirectory ) 添加特殊名称。

图2-10  使用ADAM ADSI编辑器

完成了这一系列的设置步骤之后,ADAMAdministrator账号就配置完毕了。当应用程序从ActiveDirectoryMembershipProvider连接到ADAM实例时,将需要使用这个账号。

2. 配置ActiveDirectoryMembershipProvider

下一步是配置应用程序来使用ActiveDirectoryMembershipProvider。可以使用代码清单2-24中的Web配置文件来完成该配置。

代码清单2-24  Web.Config

 

   

      name="ADAMConnection"

      connectionString="LDAP://localhost:389/OU=WebUsers,O=WebUsersDirectory"/>

 

 

   

   

     

       

          name="MyMembershipProvider"

          type="System.Web.Security.ActiveDirectoryMembershipProvider"

          connectionStringName="ADAMConnection"

          connectionProtection="None"

          connectionUsername="CN=ADAMAdministrator,OU=WebUsers,O=WebUsersDirectory"

          connectionPassword="secret_"

          enableSearchMethods="true" />

     

   

 

代码清单2-24中的Web配置文件配置了一个名为MyMembershipProvider的新默认Membership提供器。该提供器是ActiveDirectoryMembershipProvider类的实例。

对于使用ActiveDirectoryMembershipProvider提供器所提供的一些属性需要额外的解释。其中connectionStringName属性所指的是定义在connectionStrings节点中的连接字符串。该连接字符串用于连接在端口389上进行侦听的本地ADAM实例。

需要注意的是connectionProtection属性值被设置为了None。如果不修改这个属性,那么就需要使用SSL连接。如果确认需要使用SSL连接,那么还必须修改连接字符串中的端口号(典型设置是使用端口636)。

在前一节的配置文件中,connectionUsername和connectionPassword属性使用的是ADAMAdmini- strator账号。如果不使用SSL连接,那么就必须提供connectionUsername和connectionPassword属性。

最后,需要注意的是在该提供器中定义了enableSearchMethods属性。如果要通过使用Web站点管理工具来对用户进行配置,那么就必须要包含该属性。

ActiveDirectoryMembershipProvider类支持以下几个操作活动目录的属性:

q connectionStringName——定义在connectionStrings节点中,用于指定连接到活动目录服务器端的连接的名称;

q connectionUsername——用于指定连接到活动目录服务的活动目录账号;

q connectionPassword——用于指定连接到活动目录服务的活动目录账号的密码;

q connectionProtection——用于指定是否对连接进行加密。可能的取值是None和Secure;

q enableSearchMethods——用于使ActiveDirectoryMembershipProvider类可以使用附加方法。在使用Web站点管理工具时必须启用这个属性;

q attributeMapPasswordQuestion——用于将Membership安全提示问题映射到活动目录的相应属性上;

q attributeMapPasswordAnswer——用于将Membership安全提示问题答案映射到活动目录的相应属性上;

q attributeMapFailedPasswordAnswerCount——用于将Membership MaxInvalidPasswordAttempts属性映射到活动目录的相应属性上;

q attributeMapFailedPasswordAnswerTime —— 用于将Membership PasswordAttemptWindow属性映射到活动目录的相应属性上;

q attributeMapFailedPasswordAnswerLockoutTime——用于将Membership PasswordAnswer- AttemptLockoutDuration属性映射到活动目录的相应属性上。

在完成了以上配置步骤后,就可以像使用SqlMembershipProvider提供器那样来使用ActiveDirectoryMembershipProvider。当使用Login控件时,用户将通过活动目录进行验证。而当使用CreateUserWizard控件时,新用户将被创建到活动目录服务器端中。

2.3.7  创建自定义Membership提供器

由于ASP.NET Membership使用提供器模式,所以可以很容易的通过创建自定义Membership提供器来对ASP.NET Membership进行扩展。这里主要有两类需要创建自定义Membership提供器的情形。

第一类,假设你已经拥有了一个ASP.NET 1.x或传统ASP实现的应用程序。并且当前的成员信息保存在完全自定义的数据库表结构中。此外,该数据库的表结构很难和SqlMembershipProvider所使用的数据库表结构进行映射关联。

在这种情况下,创建自定义Membership提供器来读取已有的数据库表结构是很有意义的。创建了这样的自定义Membership提供器后,就可以在已有数据库表结构上使用ASP.NET Membership。

第二类,假设你需要将用户信息保存在除了微软SQL Server数据库或活动目录服务器端以外的其他数据存储环境中。例如,有些公司或组织可能是会使用Oracle或DB2这类数据库服务器端。这样一来,就需要创建自定义Membership提供器来访问自定义的数据存储环境。

在本节中,我们会创建一个简单的自定义Membership提供器:将用户信息保存在XML文档中的XmlMembershipProvider。

不幸的是,如果将实现XmlMembershipProvider的所有代码都放在这里将会太占篇幅。不过这些代码已放置在本书随书附带资源中,该代码文件名为XmlMembershipProvider.cs,并位于App_Code文件夹中。

XmlMembershipProvider类继承至MembershipProvider抽象类。该抽象类提供了超过25个属性和方法需要实现。

例如,ValidateUser()方法需要被实现。因为Login控件在对用户名和密码进行校验时需要调用这个方法。

CreateUser()方法也需要被实现。当CreateUserWizard控件创建新用户时会调用这个方法。

用来配置XmlMembershipProvider的Web配置文件包含在代码清单2-25中。

代码清单2-25  Web.Config

   

     

     

     

       

         

            name="MyMembershipProvider"

            type="AspNetUnleashed.XmlMembershipProvider"

            dataFile="~/App_Data/Membership.xml"

            requiresQuestionAndAnswer="false"

            enablePasswordRetrieval="true"

            enablePasswordReset="true"

            passwordFormat="Clear" />   

       

     

     

   

需要注意的是XmlMembershipProvider支持许多的属性。例如,它支持passwordFormat属性,该属性用于指定密码是以散列值形式还是以明文形式保存(该属性不支持对密码进行加密设置)。

该XmlMembershipProvider提供器将成员信息保存在一个名为Membership.xml的XML文件中,且该文件位于App_Data文件夹。如果需要,也可以手动将用户添加到该文件中。另外,还可以通过CreateUserWizard控件或Web站点管理工具来创建新用户。

代码清单2-26中包含了一个Membership.xml文件的示例。

代码清单2-26  App_Data\Membership.xml

 

 

在随书资源提供的示例代码中,包括了一个Register.aspx页面、一个Login.aspx页面和一个ChangePassword.aspx页面。通过使用这些页面,可以对XmlMembershipProvider所提供的各种功能进行方便的试验。

注意   动态XPath查询可能会带来XPath注入攻击,就像动态SQL查询可能会带来SQL注入攻击一样。在编写XmlMembershipProvider类时,我们应该尽量避免使用诸如SelectSingleNode()这样的方法来避免XPath注入攻击,即时使用这些方法可能会得到更简单和高效的代码。但大多数时候,代码的安全性比快速开发和运行更为重要。

阅读(1926) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~