恢复和更改密码 (C#)

作者 :Scott Mitchell

注意

自本文撰写以来,ASP.NET 成员资格提供程序已被 ASP.NET Identity 取代。 强烈建议更新应用以使用 ASP.NET 标识 平台,而不是本文撰写时介绍的成员资格提供程序。 ASP.NET 标识比 ASP.NET 成员身份系统具有许多优势,包括:

  • 性能更好
  • 改进了可扩展性和可测试性
  • 支持 OAuth、OpenID Connect 和双因素身份验证
  • 基于声明的标识支持
  • 更好地与 ASP.Net Core 的互操作性

ASP.NET 包括两个 Web 控件,用于帮助恢复和更改密码。 PasswordRecovery 控件使访问者能够恢复其丢失的密码。 ChangePassword 控件允许用户更新其密码。 与我们在本教程系列中看到的其他与登录相关的 Web 控件一样,PasswordRecovery 和 ChangePassword 控件可在后台使用成员资格框架来重置或修改用户的密码。

简介

在我的银行、公用事业公司、电话公司、电子邮件帐户和个性化 Web 门户的网站之间,我和大多数人一样,有几十个不同的密码需要记住。 现在有这么多凭据要记住,人们忘记密码的情况并不少见。 为此,提供用户帐户的网站需要包括一种用户恢复其密码的方法。 此过程通常涉及生成新的随机密码,并将其通过电子邮件发送到用户存档的电子邮件地址。 在收到新密码后,大多数用户会返回网站,并将其密码从随机生成的密码更改为更令人难忘的密码。

ASP.NET 包括两个 Web 控件,用于帮助恢复和更改密码。 PasswordRecovery 控件使访问者能够恢复其丢失的密码。 ChangePassword 控件允许用户更新其密码。 与我们在本教程系列中看到的其他与登录相关的 Web 控件一样,PasswordRecovery 和 ChangePassword 控件可在后台使用成员资格框架来重置或修改用户的密码。

在本教程中,我们将检查这两个控件的用法。 我们还将了解如何通过 MembershipUserChangePassword 的 和 方法以编程方式更改和 ResetPassword 重置用户的密码。

步骤 1:帮助用户恢复丢失的密码

支持用户帐户的所有网站都需要为用户提供恢复忘记的密码的某种机制。 好消息是,由于 PasswordRecovery Web 控件,在 ASP.NET 中实现此类功能非常简单。 PasswordRecovery 控件呈现一个界面,提示用户输入其用户名,并在需要时提示其安全问题的答案。 然后,它会向用户发送其密码的电子邮件。

注意

由于电子邮件以纯文本方式通过网络传输,因此通过电子邮件发送用户密码存在安全风险。

PasswordRecovery 控件由三个视图组成:

  • UserName - 提示访问者输入其用户名。 这是初始视图。
  • 问题 - 将用户的用户名和安全问题显示为文本,以及供用户输入其安全问题答案的 TextBox。
  • 成功 - 显示一条消息,通知用户其密码已通过电子邮件发送。

PasswordRecovery 控件显示的视图和执行的操作取决于以下成员身份配置设置:

  • RequiresQuestionAndAnswer
  • EnablePasswordRetrieval
  • EnablePasswordReset

成员资格框架的设置 RequiresQuestionAndAnswer 指示用户在注册帐户时是否必须指定安全问题和答案。 正如我们在创建用户帐户教程中讨论的,如果 RequiresQuestionAndAnswer 为 True (默认) 则 CreateUserWizard 的界面包含用于新用户的安全问题和答案的 TextBox 控件;如果 RequiresQuestionAndAnswer 为 False,则不会收集此类信息。 同样,如果 RequiresQuestionAndAnswer 为 True,则 PasswordRecovery 控件会在用户输入用户名后显示“问题”视图;仅当用户输入正确的安全答案时,才会恢复密码。 但是,如果 RequiresQuestionAndAnswer 为 False,则 PasswordRecovery 控件将直接从“用户名”视图移动到“成功”视图。

在用户提供用户名或用户名和安全答案后,如果 RequiresQuestionAndAnswer 为 True,则 PasswordRecovery 会向用户发送密码。 如果选项 EnablePasswordRetrieval 设置为 True,则会向用户发送其当前密码的电子邮件。 如果它设置为 False 且 EnablePasswordReset 设置为 True,则 PasswordRecovery 控件会为用户生成新的随机密码,并向其发送此新密码的电子邮件。 如果 和 EnablePasswordReset 均为 EnablePasswordRetrieval False,则 PasswordRecovery 控件将引发异常。

注意

回想一下, SqlMembershipProvider 以以下三种格式之一存储用户的密码:清除、哈希 (默认) 或加密。 使用的存储机制取决于成员资格配置设置;演示应用程序使用哈希密码格式。 使用哈希密码格式时, EnablePasswordRetrieval 该选项必须设置为 False,因为系统无法从数据库中存储的哈希版本确定用户的实际密码。

图 1 说明了 PasswordRecovery 的接口和行为如何受到成员资格配置的影响。

RequiresQuestionAndAnswer、EnablePasswordRetrieval 和 EnablePasswordReset 影响 PasswordRecovery 控件的外观和行为

图 1:、 RequiresQuestionAndAnswerEnablePasswordRetrievalEnablePasswordReset 影响 PasswordRecovery 控件的外观和行为 (单击以查看全尺寸图像)

注意

SQL Server 中创建成员资格架构教程中,我们通过将 设置为 RequiresQuestionAndAnswer True、EnablePasswordRetrieval设置为 False 和 EnablePasswordReset True 来配置成员资格提供程序。

使用 PasswordRecovery 控件

让我们看看在 ASP.NET 页中使用 PasswordRecovery 控件。 打开 RecoverPassword.aspx PasswordRecovery 控件并将其从工具箱拖放到Designer;将其ID设置为 RecoverPwd。 与 Login 和 CreateUserWizard Web 控件一样,PasswordRecovery 控件的视图呈现一个丰富的复合界面,其中包括标签、文本框、按钮和验证控件。 可以通过控件的样式属性或通过将视图转换为模板来自定义视图的外观。 我留给感兴趣的读者做练习。

当用户访问此页面时,她将输入其用户名,然后单击“提交”按钮。 由于我们已 RequiresQuestionAndAnswer 在成员资格配置设置中将 属性设置为 True,因此 PasswordRecovery 控件将显示问题视图。 用户输入正确的安全答案并单击“提交”后,PasswordRecovery 控件会将用户的密码更新为随机生成的密码,并将此密码通过电子邮件发送到文件上的电子邮件地址。 所有这些操作都是可能的,无需编写一行代码!

在测试此页面之前,需要执行最后一项配置:我们需要在 中 Web.config指定邮件传递设置。 PasswordRecovery 控件依赖于这些设置来发送电子邮件。

邮件传递配置是通过 <system.net> 元素<mailSettings> 元素指定的。 <smtp>使用 元素指示传递方法和默认的 From 地址。 以下标记将邮件设置配置为使用端口 25 上名为 smtp.example.com 的网络 SMTP 服务器,并使用用户名和密码的用户名/密码凭据。

注意

<system.net> 是根 <configuration> 元素的子元素和 的 <system.web>同级元素。 因此,不要将 <system.net> 元素放在 元素中 <system.web> ;而是将其置于同一级别。

<configuration>
 ...
 <system.net>
 <mailSettings>
 <smtp deliveryMethod="Network" from="youraddress@example.com">
 <network
 host="smtp.example.com"
 userName="username"
 password="password"
 port="25" />
 </smtp>
 </mailSettings>
 </system.net>
</configuration>

除了在网络上使用 SMTP 服务器外,还可以指定应将要发送的电子邮件存放在的取件目录。

配置 SMTP 设置后,通过浏览器访问 RecoverPassword.aspx 页面。 首先尝试输入用户存储中不存在的用户名。 如图 2 所示,PasswordRecovery 控件显示一条消息,指示无法访问用户信息。 可以通过 控件 UserNameFailureText 的 属性自定义消息的文本。

如果输入的用户名无效,将显示错误消息

图 2:如果输入了无效用户名,则显示错误消息 (单击以查看全尺寸图像)

现在输入用户名。 将系统中帐户的用户名与可以访问的电子邮件地址以及你知道其安全答案的电子邮件地址一起使用。 输入用户名并单击“提交”后,PasswordRecovery 控件会显示其问题视图。 与“用户名”视图一样,如果输入错误的答案,PasswordRecovery 控件会显示错误消息 (请参阅图 3) 。 QuestionFailureText使用 属性自定义此错误消息。

如果用户输入无效的安全答案,则显示错误消息

图 3:如果用户输入无效的安全答案 (单击以查看全尺寸图像)

最后,输入正确的安全答案,然后单击“提交”。 在后台,PasswordRecovery 控件生成一个随机密码,将其分配给用户帐户,发送电子邮件通知用户其新密码 (见图 4) ,然后显示“成功”视图。

向用户发送具有新密码的Email

图 4:向用户发送了一个具有新密码的Email (单击以查看全尺寸图像)

自定义Email

PasswordRecovery 控件发送的默认电子邮件相当沉闷 (见图 4) 。 消息从主题为 Password 和纯文本正文的 元素的 from 属性中指定的<smtp>帐户发送:

请返回站点并使用以下信息登录。

用户名: 用户名

密码: 密码

可以通过 PasswordRecovery 控件事件的SendingMail事件处理程序以编程方式自定义此消息,或通过 属性以声明方式MailDefinition自定义此消息。 让我们了解这两个选项。

事件 SendingMail 在发送电子邮件之前触发,是我们最后一次以编程方式调整电子邮件的机会。 引发此事件时,事件处理程序将传递类型 MailMessageEventArgs为 的对象,其 Message 属性包含对即将发送的电子邮件的引用。

SendingMail 事件创建事件处理程序,并添加以下代码,这些代码以编程方式添加到 webmaster@example.com 抄送列表中。

protected void RecoverPwd_SendingMail(object sender, MailMessageEventArgs e)
{
    e.Message.CC.Add("webmaster@example.com");
}

还可以通过声明性方式配置电子邮件。 PasswordRecovery 的 MailDefinition 属性是 类型的 MailDefinition对象。 类MailDefinition提供大量与电子邮件相关的属性,包括 FromCC、、PrioritySubjectIsBodyHtmlBodyFileName、 等。 对于初学者,请将 属性设置为Subject比默认使用的更描述性的内容 (密码) ,例如你的密码已重置...

若要自定义电子邮件的正文,我们需要创建一个单独的电子邮件模板文件,其中包含正文的内容。 首先在名为 EmailTemplates的网站中创建一个新文件夹。 接下来,将名为 的新文本文件添加到此文件夹 PasswordRecovery.txt ,并添加以下内容:

Your password has been reset, <%UserName%>!

According to our records, you have requested that your password be reset. Your new
password is: <%Password%>

If you have any questions or trouble logging on please contact a site administrator.

Thank you!

请注意占位符 <%UserName%><%Password%>的用法。 在发送电子邮件之前,PasswordRecovery 控件会自动将这两个占位符替换为用户的用户名和恢复的密码。

最后,将 MailDefinitionBodyFileName 属性 指向我们刚刚创建的电子邮件模板 (~/EmailTemplates/PasswordRecovery.txt) 。

进行这些更改后, RecoverPassword.aspx 请重新访问页面并输入用户名和安全答案。 应会收到如图 5 所示的电子邮件。 请注意, webmaster@example.com 已抄送,主题和正文已更新。

主题、正文和抄送列表已更新

图 5:“主题”、“正文”和“抄送”列表已更新 (单击以查看全尺寸图像)

若要发送 HTML 格式的电子邮件设置为 IsBodyHtml True (默认值为 False) 并更新电子邮件模板以包含 HTML。

属性 MailDefinition 对于 PasswordRecovery 类不是唯一的。 如步骤 2 中所示,ChangePassword 控件还提供 属性 MailDefinition 。 此外,CreateUserWizard 控件包括这样的属性,你可以将该属性配置为自动向新用户发送欢迎电子邮件。

注意

目前,左侧导航中没有用于访问 RecoverPassword.aspx 页面的链接。 如果用户无法成功登录网站,则仅对访问此页面感兴趣。 因此,更新 Login.aspx 页面以包含指向该 RecoverPassword.aspx 页面的链接。

以编程方式重置用户密码

重置用户的密码时,PasswordRecovery 控件调用对象的 MembershipUserResetPassword 方法。 此方法有两个重载:

  • ResetPassword - 重置用户的密码。 如果 RequiresQuestionAndAnswer 为 False,请使用此重载。
  • ResetPassword(securityAnswer) - 仅当提供的 securityAnswer 正确时重置用户的密码。 如果 RequiresQuestionAndAnswer 为 True,请使用此重载。

这两个重载都返回随机生成的新密码。

与成员资格框架中的其他方法一样, ResetPassword 方法委托给配置的提供程序。 调用SqlMembershipProvideraspnet_Membership_ResetPassword存储过程,传入用户的用户名、新密码和提供的密码答案等字段。 存储过程确保密码答案匹配,然后更新用户的密码。

几个低级别的实现说明:

  • 锁定的用户无法重置其密码。 但是,未经批准的用户可能会。 我们将在解锁和批准用户帐户教程中更详细地讨论锁定状态和已批准状态。
  • 如果密码答案不正确,则用户的失败密码应答尝试计数将递增。 如果在指定的时间范围内发生指定数量的无效安全应答尝试,则用户将被锁定。

有关如何生成随机密码的Word

图 4 和图 5 中电子邮件中显示的随机生成的密码由 Membership 类的 GeneratePassword 方法创建。 此方法接受两个整数输入参数 ( lengthnumberOfNonAlphanumericCharacters )并返回 长度至少为 字符的字符串,该字符串长度至少为 numberOfNonAlphanumericCharacters 数目的非字母数字字符。 从 Membership 类或与登录相关的 Web 控件中调用此方法时,这两个参数的值由成员资格配置的 MinRequiredPasswordLengthMinRequiredNonalphanumericCharacters 属性确定,我们分别将这两个参数设置为 7 和 1。

方法 GeneratePassword 使用加密强随机数生成器来确保所选的随机字符没有偏差。 此外, GeneratePasswordpublic,这意味着如果需要生成随机字符串或密码,可以直接从 ASP.NET 应用程序中使用它。

注意

SqlMembershipProvider 始终生成长度至少为 14 个字符的随机密码,因此,如果 MinRequiredPasswordLength 小于 14,则忽略其值。

步骤 2:更改密码

随机生成的密码很难记住。 请考虑图 4 中显示的密码: WWGUZv(f2yM:Bd。 尝试将它提交到内存! 不用说,在向用户发送随机生成的此类密码后,她会希望将密码更改为更令人难忘的密码。

使用 ChangePassword 控件创建一个界面,供用户更改其密码。 与 PasswordRecovery 控件非常类似,ChangePassword 控件包含两个视图:更改密码和成功。 “更改密码”视图提示用户输入其新旧密码。 提供正确的旧密码和满足最小长度和非字母数字字符要求的新密码后,ChangePassword 控件将更新用户的密码并显示“成功”视图。

注意

ChangePassword 控件通过调用 MembershipUser 对象的 ChangePassword 方法修改用户的密码。 ChangePassword 方法接受两个 string 输入参数- oldPasswordnewPassword,并使用 newPassword 更新用户帐户(假设提供的 oldPassword 正确)。

打开页面, ChangePassword.aspx 将 ChangePassword 控件添加到页面,将其 ChangePwd命名为 。 此时,“设计”视图应显示“更改密码”视图 (请参阅图 6) 。 与 PasswordRecovery 控件一样,可以通过控件的智能标记在视图之间切换。 此外,可以通过各种样式属性或将其转换为模板来自定义这些视图的外观。

将 ChangePassword 控件添加到页面

图 6:将 ChangePassword 控件添加到 Page (单击以查看全尺寸图像)

ChangePassword 控件可以更新当前登录用户的密码 另一个指定用户的密码。 如图 6 所示,默认的“更改密码”视图只呈现三个 TextBox 控件:一个用于旧密码,两个用于新密码。 此默认接口用于更新当前登录用户的密码。

若要使用 ChangePassword 控件更新其他用户的密码,请将控件的 DisplayUserName 属性 设置为 True。 这样做会将第四个 TextBox 添加到页面,提示输入要更改其密码的用户的用户名。

如果要让注销的用户更改其密码而无需登录,则设置为 DisplayUserName True 非常有用。 就我个人而言,我认为要求用户在允许她更改密码之前登录没有任何问题。 因此,保留 DisplayUserName 设置为 False (其默认) 。 但是,在做出此决定时,我们实质上是禁止匿名用户访问此页面。 更新站点的 URL 授权规则,以拒绝匿名用户访问 ChangePassword.aspx。 如果需要刷新 URL 授权规则语法上的内存,请参阅 基于用户的授权 教程。

注意

属性似乎 DisplayUserName 可用于允许管理员更改其他用户的密码。 但是,即使 DisplayUserName 设置为 True,也必须知道并输入正确的旧密码。 我们将在步骤 3 中讨论允许管理员更改用户密码的技术。

ChangePassword.aspx通过浏览器访问页面并更改密码。 请注意,如果输入的新密码不符合成员资格配置中指定的密码长度和非字母数字字符要求,将显示错误消息 (请参阅图 7) 。

如果输入的新密码不符合密码长度和非字母数字字符要求,将显示错误消息。

图 7:将 ChangePassword 控件添加到页面 (单击以查看全尺寸图像)

输入正确的旧密码和有效的新密码后,登录用户的密码将更改,并显示“成功”视图。

发送确认Email

默认情况下,ChangePassword 控件不会向其密码刚刚更新的用户发送电子邮件。 如果要发送电子邮件,只需配置 控件的 MailDefinition 属性。 让我们配置 ChangePassword 控件,以便向用户发送包含其新密码的 HTML 格式的电子邮件。

首先在名为 ChangePassword.htm的文件夹中创建EmailTemplates一个新文件。 添加以下标记:

<html>
 <body>
 <h2>Your Password Has Been Changed!</h2>
 <p>
 This email confirms that your password has been changed.
 </p>
 <p>
 To log on to the site, use the following credentials:
 </p>
 <table>
 <tr>
 <td>
 <b>Username:</b>
 </td>
 <td>
 <%UserName%>
 </td>
 </tr>
 <tr>
 <td>
 <b>Password:</b>
 </td>
 <td>
 <%Password%>
 </td>
 </tr>
 </table>
 <p>
 If you have any questions or encounter any problems logging in,
 please contact a site administrator.
 </p>
 </body>
</html>

接下来,将 ChangePassword 控件的 MailDefinitionBodyFileNameIsBodyHtmlSubject 属性分别设置为 ~/EmailTemplates/ChangePassword.htm、True 和密码已更改!。

进行这些更改后,请重新访问页面并再次更改密码。 这一次,ChangePassword 控件将自定义的 HTML 格式的电子邮件发送到文件中用户的电子邮件地址 (请参阅图 8) 。

Email消息通知用户其密码已更改

图 8:Email消息通知用户其密码已更改 (单击以查看全尺寸图像)

步骤 3:允许管理员更改用户密码

支持用户帐户的应用程序中的一个常见功能是管理用户能够更改其他用户的密码。 有时,此功能是必需的,因为系统无法让用户更改自己的密码。 在这种情况下,用户恢复忘记的密码的唯一方法是管理员为其分配新密码。 但是,使用 PasswordRecovery 和 ChangePassword 控件时,管理用户无需忙于更改用户的密码,因为用户能够自行执行此操作。

但是,如果客户端坚持管理用户应能够更改其他用户的密码,该怎么办? 遗憾的是,添加此功能可能需要一点工作。 若要更改用户的密码,必须将旧密码和新密码提供给 MembershipUser 对象的 ChangePassword 方法,但管理员不必知道用户的密码即可对其进行修改。

一种解决方法是先重置用户的密码,然后使用如下所示的代码将其更改为新密码:

MembershipUser usr = Membership.GetUser(username);
string resetPwd = usr.ResetPassword();
usr.ChangePassword(resetPwd, newPassword);

此代码首先检索 有关用户名的信息,用户名是管理员希望更改其密码的用户。 接下来, ResetPassword 调用 方法,为用户分配新的随机密码。 此随机生成的密码由 方法返回,并存储在变量 resetPwd中。 现在,我们知道用户的密码,可以通过调用 ChangePassword来更改密码。

问题是,仅当成员身份系统配置设置为 False 时, RequiresQuestionAndAnswer 此代码才有效。 如果 RequiresQuestionAndAnswer 为 True,则与应用程序一样,需要 ResetPassword 向方法传递安全答案,否则将引发异常。

如果将成员资格框架配置为需要安全问题和答案,但客户端坚持管理员能够更改用户密码,则有三个选项:

  • 把手扔在空中,告诉你的客户,这只是一件不能做的事情。
  • 设置为 RequiresQuestionAndAnswer False。 这会导致应用程序安全性降低。 假设一个恶意用户获得了另一个用户的电子邮件收件箱的访问权限。 也许被入侵的用户已经离开办公桌去吃午饭,但没有锁定他们的工作站,或者他们可能从公共终端访问了他们的电子邮件,但没有注销。在任一情况下,恶意用户可以访问页面 RecoverPassword.aspx 并输入用户的用户名。 然后,系统会通过电子邮件发送恢复的密码,而不提示输入安全答案。
  • 绕过成员资格框架创建的抽象层,直接使用 SQL Server 数据库。 成员资格架构包括一个名为 aspnet_Membership_SetPassword 的存储过程,该过程设置用户的密码,不需要安全答案或旧密码即可完成任务。

这些选项都不是特别吸引人,但开发人员的生活有时就是这样。

我继续实现了第三种方法,即编写绕过 MembershipMembershipUser 类并直接对 SecurityTutorials 数据库进行操作的代码。

注意

通过直接使用数据库,成员资格框架提供的封装被破坏。 此决定将我们关联到 , SqlMembershipProvider使我们的代码不那么可移植。 此外,如果成员资格架构发生更改,此代码在 ASP.NET 的未来版本中可能无法正常工作。 此方法是一种解决方法,与大多数解决方法一样,不是最佳做法的示例。

该代码具有一些无吸引力的位,并且相当长。 因此,我不想对本教程进行深入探讨。 如果有兴趣了解详细信息,请下载本教程的代码并访问页面 ~/Administration/ManageUsers.aspx 。 我们在上一教程中创建的此页面列出了每个用户。 我已更新 GridView,以包含指向 UserInformation.aspx 页面的链接,通过查询字符串传递所选用户的用户名。 该 UserInformation.aspx 页面显示有关所选用户和 TextBox 的信息,以便更改其密码 (请参阅图 9) 。

输入新密码后,在第二个 TextBox 中确认密码,然后单击“更新用户”按钮,随后将进行回发,并 aspnet_Membership_SetPassword 调用存储过程,更新用户的密码。 我鼓励对此功能感兴趣的读者更熟悉代码,并尝试扩展该功能,以包括向密码已更改的用户发送电子邮件。

管理员可更改用户密码

图 9:管理员可能更改用户的密码 (单击以查看全尺寸图像)

注意

UserInformation.aspx 当成员资格框架配置为以清除或哈希格式存储密码时,页面当前才有效。 它缺少用于加密新密码的代码,但邀请你添加此功能。 建议添加必要代码的方式是使用反编译程序(如 Reflector)检查.NET Framework中的方法的源代码;首先检查SqlMembershipProvider类的 ChangePassword 方法。 这是我用来编写代码以创建密码哈希的方法。

摘要

ASP.NET 提供了两个控件来帮助用户管理其密码。 PasswordRecovery 控件适用于忘记密码的用户。 根据成员资格框架的配置,用户通过电子邮件发送其现有密码或随机生成的新密码。 ChangePassword 控件使用户能够更新其密码。

与 Login 和 CreateUserWizard 控件一样,PasswordRecovery 和 ChangePassword 控件可呈现丰富的用户界面,而无需编写声明性标记或代码行。 如果默认用户界面不满足需求,可以通过各种样式属性对其进行自定义。 或者,控件的接口可以转换为模板,以便更精细地控制。 这些控件在后台使用成员身份 API,调用 MembershipUser 对象的 ResetPasswordChangePassword 方法。

编程快乐!

深入阅读

有关本教程中讨论的主题的详细信息,请参阅以下资源:

关于作者

Scott Mitchell 是多本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上联系 mitchell@4guysfromrolla.com 斯科特,也可以通过他的博客在 联系 http://ScottOnWriting.NET

特别感谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者包括 Michael Emmings 和 Suchi Banerjee。 有兴趣查看我即将发布的 MSDN 文章? 如果是这样,请在 mitchell@4GuysFromRolla.com