使用 Iron Speed Designer 重写遗留应用程序





1.00/5 (4投票s)
遗留应用程序的规模和复杂性排除了“一次性”转换的方法。我们采用“迁移”策略,而不是“转换”。
引言
将 Web 应用程序从一种技术迁移到 ASP.NET 需要仔细规划。引入 Iron Speed Designer 会增加更多需要考虑的决策。可用性和安全策略必须在进行之前制定好;否则,您可能会面临需要返工代码而非返工策略的前景。
我们组织的遗留 Web 应用程序是一个内部(内网)应用程序,前端使用 ColdFusion 编写,后端使用 Microsoft SQL Server 作为数据库。作为我们组织的应用程序架构师,我面临的首要任务之一是决定下一代技术平台。我选择了 ASP.NET/C#,这甚至让我自己感到惊讶,因为我之前在 J2EE 领域有六年的经验(但那是另一个故事)。
遗留应用程序的规模和复杂性排除了“一次性”转换的方法。我采用“迁移”策略,而不是“转换”。在这种方法中,新的用户需求在 ASP.NET 页面中实现,这些页面尽可能使用 Iron Speed Designer 生成。遗留应用程序通过链接新的 ASP.NET 页面来“扩展”。这样做更容易管理,因为我们继续使用与 ColdFusion 应用程序相同的底层数据库。在有机会时,将转换现有页面。
遗留应用程序使用“表单”身份验证/授权模型。因此,我们已经在数据库中有了用户、角色以及用户/角色表。所以,我的第一个决定是是否
- 使用 Iron Speed Designer 的安全模型,或
- 使用 ASP.NET 2.0 的安全模型,或
- 在 C# 中改编现有应用程序的安全机制。
无论采用哪种方法,我都希望利用现有的安全数据(用户、角色和分配)。最终,我选择了使用 ASP.NET 2.0 安全模型。主要原因是,我也打算使用第三方控件。通常希望控件(如菜单)能够“感知安全”,以便从不属于某些角色的用户那里隐藏选项。我计划使用的控件(包括第三方和原生 ASP.NET 控件)都支持 ASP.NET 2.0 安全性,因此我的决定已经做出了。
我必须克服的一个重大障碍是允许用户在无需登录的情况下访问新页面。本质上,ASP.NET 环境需要检测未经身份验证的请求,确定发起请求的用户身份,然后自动将其登录到表单安全机制。
另一个要求是应用程序应使用 SQL 登录进行数据库连接。此外,我希望最小化或消除管理员在将应用程序从一个环境移动到另一个环境时对 `web.config` 元素的修改(添加/更改/删除)。换句话说,当应用程序从开发环境迁移到测试环境,再到生产环境时,`web.config` 文件不应需要修改才能连接到该环境的数据库。最后,我们希望向所有人隐藏 SQL 登录帐户和密码,甚至开发人员。
Procedure
考虑到这些要求,我们开发了以下方法,并且效果相当好:
- 配置 Web 应用程序使用模拟。这是通过设置 `web.config` 文件中的 `identity` 部分来完成的,如下面的图 1 所示。模拟是 IIS 的一种运行模式,在这种模式下,Web 应用程序代码在发起请求的 Windows 用户的安全上下文中执行。
- 配置 Web 应用程序使用表单安全。这是通过 `web.config` 文件中的 `authentication` 部分来完成的,如下面的图 2 所示。
- 配置 Web 应用程序使用自定义成员资格提供程序。这是通过 `web.config` 文件中的 `membership` 部分来完成的,如下面的图 3 所示。
- 配置 Web 应用程序使用自定义角色提供程序。这是通过 `web.config` 文件中的 `roleManager` 部分来完成的,如下面的图 4 所示。
- 配置 Web 应用程序拒绝所有页面的未经身份验证的访问。这是通过 `web.config` 文件中的 `authorization` 部分来完成的,如下面的图 5 所示。
- 编写自定义登录页面,根据用户的 Windows 身份自动登录。参见下面的图 6。
- 编写自定义成员资格提供程序类。参见下面的图 7。
- 编写自定义角色提供程序类。参见下面的图 8。
- 利用 `machine.config` 和 `aspnet_regiis.exe` 加密来保护登录 ID 和密码信息,并在应用程序在开发、测试和生产环境之间移动时最小化对 `*.config` 文件的修改。参见下面的“处理 `machine.config`”。
<!-- .......... Identity of application for windows purposes.........-->
<identity impersonate="true"/>
图 1 - 模拟会导致 Web 应用程序在 Windows 域用户的安全上下文中执行。当会计部门的 John Doe 请求页面时,代码将在服务器上使用 Windows 用户 MYDOMAIN\DoeJohn 的凭据执行。
<!-- .......... Authentication mechanism is "Forms".........-->
<authentication mode="Forms">
<forms name="authCookie"
loginUrl="Common/Login.aspx" protection="All" path="/" />
</authentication>
图 2 - 其他可用选项是 Windows 和 Passport。我们选择了表单安全来利用我们遗留应用程序的安全数据库。
<membership defaultProvider="MyMembershipProvider"
userIsOnlineTimeWindow="99">
<providers>
<clear/>
<add name="MyembershipProvider"
type="Fund.FMS.MyMembershipProvider"
connectionStringName="MyConnectionString"
enablePasswordRetrieval="false"
enablePasswordReset="false"
requiresQuestionAndAnswer="false"
writeExceptionsToEventLog="true"/>
</providers>
</membership>
图 3 - `membership` 部分允许我们定义处理用户 ID 和密码身份验证的类。我们正在覆盖默认类(`SqlMembershipProvider`)并提供自己的类。
<roleManager
defaultProvider="MyRoleProvider"
enabled="true"
cacheRolesInCookie="true"
cookieName=".ASPROLES"
cookieTimeout="30"
cookiePath="/"
cookieRequireSSL="false"
cookieSlidingExpiration="true"
cookieProtection="All">
<providers>
<clear/>
<add
name="MyRoleProvider"
type="Fund.FMS.MyRoleProvider"
connectionStringName="MyConnectionString"
applicationName="FMS"
writeExceptionsToEventLog="false"/>
</providers>
</roleManager>
图 4 - `roleManager` 部分允许我们定义处理应用程序内职责授权的类,特别是提供给定用户的角色成员资格。我们正在覆盖默认类(`SqlRoleProvider`)并提供自己的类。
<!-- .......... Everything requires authorization.........-->
<authorization>
<deny users="?"/>
<allow users="*"/>
</authorization>
图 5 - `authorization` 部分允许我们指定所有页面都需要授权,无论谁发起请求。
protected void Page_Load(object sender, EventArgs e)
{
/*
The purpose of the code below is to
1) Get the Windows network login ID of the user requesting this page.
2) If not successful, this page will render in their browser,
telling them they are an unknown user.
3) If successful, we strip off the domain portion of the login ID,
leaving just lastname and initial(s).
4) We then read the user security (user) table for this user Id,
attempting to retrieve the password.
5) If we don't find a record, the page will render, same as if they
did not have a Windows login.
6) If we find a password, we call the ValidateUser method
on the static class Membership. This isreally an indirect
reference through the static class to the custom MembershipProvider class
that we wrote and "plugged in" to the security mechanism via the
web.config file (Membership section).
7) If we are not validated, we fall through and render
the same text as if no Windows login.
8) If we are successful, we store the full user name in the session
and redirect to the originally requested page which is normally Home.aspx.
*/
bool bSuccess = false;
string errMessage = "Login failed.";
WindowsIdentity ident = WindowsIdentity.GetCurrent();
if (ident == null)
return;
string userId = ident.Name.Replace("MYDOMAIN\\", ""); // remove domain name
string password = "";
/* Get the connection string info from web.config
by using the Configuration class*/
Configuration cfg = WebConfigurationManager.OpenWebConfiguration(
System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
ConnectionStringSettingsCollection connectionStrings =
cfg.ConnectionStrings.ConnectionStrings;
ConnectionStringSettings connString = (ConnectionStringSettings)
connectionStrings["MyConnectionString"];
if (connString == null)
{
WriteToEventLog(new Exception("A configuration entry for connection string " +
"'MyConnectionString' was not found."), "Exit");
throw new Exception("A failure has occurred.");
}
try
{
SqlConnection conn = new SqlConnection(connString.ConnectionString);
conn.Open();
SqlCommand command = conn.CreateCommand();
command.CommandText =
"select password, fst_nme, lst_nme from usr_tbl where username = @username";
SqlParameter parm = new SqlParameter("@username", userId);
command.Parameters.Add(parm);
SqlDataReader reader = command.ExecuteReader();
if (reader.Read())
{ password = reader.GetString(reader.GetOrdinal("password"));
reader.Close();
command.Dispose();
conn.Close();
try
{
// using current user and password retrieved from legacy security table,
// log into the ASP.NET 2.0 Forms security manager.
if (Membership.ValidateUser(userId, password))
{
FormsAuthentication.RedirectFromLoginPage(userId, false);
}
}
catch (System.Threading.ThreadAbortException e1)
{
// the RedirecFromLoginPage throws a ThreadAbortException
// by design, so we just catch it and eat it...
}
catch (System.Exception e2)
{
}
}
else
{
reader.Close();
command.Dispose();
conn.Close();
}
}
catch (Exception e2)
{
}
图 6 - 这个自定义登录页面检索发起请求的用户的 Windows 身份,这得益于启用了模拟。使用 Windows 用户 ID,我们从遗留数据库中检索密码,并尝试以编程方式登录到我们的自定义成员资格提供程序。如果登录失败或发生任何异常,我们将继续进行,从而向用户呈现标准的表单登录页面。
using System.Web.Security;
using System.Configuration.Provider;
using System.Collections.Specialized;
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Diagnostics;
using System.Web;
using Systelobalization;
using System.Security.Cryptography;
using System.Text;
using System.Web.Configuration;
namespace Fund.FMS
{
public sealed class MyMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
// Add code here to read your custom security tables.
// Return true if user is valid, false if not.
}
// Override other methods as necessary..
}
图 7 - 这是名为 `MyMembershipProvider` 的自定义成员资格提供程序的声明。它扩展了 .NET 框架本身的基类 `MembershipProvider`。您只需覆盖方法,例如 `ValidateUser`,并从自己的安全表中读取。在您的代码中,您可以通过接口引用它,无论是显式地,还是间接地,例如通过当前页面的 `Membership` 属性,该属性引用了由于 `web.config` 文件中的规范而加载的实例。
我已将此类放置在 Iron Speed 应用程序的 `AppCode` 目录中。
有关如何实现自定义成员资格提供程序的详细说明,请参见 MSDN。
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using System.Configuration.Provider;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
namespace Fund.FMS
{
public sealed class MyRoleProvider : RoleProvider
{
public public override string[] GetRolesForUser(string username)
{
// Add code here to read your custom security tables.
// Return string array of role names.
}
// Override other methods as necessary..
}
图 8 - 这是名为 `MyRoleProvider` 的自定义角色提供程序的声明。它扩展了 .NET 框架本身的基类 `RoleProvider`。您只需覆盖方法,例如 `IsUserInRole`,并从自己的安全表中读取。在您的代码中,您可以通过接口引用它,无论是显式地,还是间接地,例如通过当前页面的 `Roles` 属性,该属性引用了由于 `web.config` 文件中的规范而加载的角色提供程序。
我已将此类放置在 Iron Speed 应用程序的 `AppCode` 目录中。
有关如何实现自定义角色提供程序的详细说明,请参见 MSDN。
处理 `machine.config`
Iron Speed Designer 将数据库连接信息存储在连接字符串格式中,但将键名和值存储在文件的 `appSettings` 部分,而不是 `connectionStrings` 部分。个人来说,我希望看到这一点得到更改,但目前就是这样。
我还必须为我们的自定义角色和成员资格提供程序创建连接字符串,其中包含几乎相同的信息,因为其中一个必需的配置元素是提供程序用于其数据库连接的连接字符串的名称。
如前所述,在将应用程序从开发环境迁移到测试环境时,修改 `web.config` 中的连接字符串值是不理想的。`machine.config` 文件包含配置信息,是对 `web.config` 中配置信息的补充。换句话说,您可以在 `machine.config` 而不是 `web.config` 中定义连接字符串。如果您在两个地方都定义了相同的元素,则可能会收到运行时错误,提示配置元素已定义多次。
正如您可能从名称中推断出的那样,`machine.config` 包含特定于其所在计算机的值。因此,可以在每个服务器上的 `machine.config` 文件中创建连接字符串。例如,开发人员桌面上的 `machine.config` 可能包含指向本地数据库的连接字符串。测试计算机上的 `machine.config` 文件可能包含相同的连接字符串,但指向与测试环境关联的数据库服务器。对于生产计算机也是如此。
`machine.config` 文件位于 .NET 安装目录的 `CONFIG` 目录中,通常是 `C:\Windows\Microsoft.NET\Framework\v2.0.50727`(或您已安装的任何版本)。在运行时,来自 `machine.config` 的配置数据与来自 `web.config` 的配置数据合并,为您的 Web 应用程序提供完整的配置数据集。
当处理 Iron Speed Designer 生成的应用程序时,将 `machine.config` 整合到我们的策略中还需要一个额外的步骤。问题在于 Iron Speed Designer 不像 ASP.NET 运行时那样读取 `machine.config` 和 `web.config`。它只读取 `web.config`。因此,您必须将连接字符串保留在 `web.config` 中,而 Iron Speed Designer 实际上将其存储在文件的 `appSettings` 部分。如果您从 `appSettings` 部分删除了 Iron Speed Designer 生成的连接字符串,Iron Speed Designer 会抱怨找不到您的应用程序的连接字符串。
解决方法是,当我们部署项目到测试环境时,我们会删除、重命名或注释掉 Iron Speed Designer 生成的连接字符串。这将阻止 ASP.NET 运行时找到两个同名的连接字符串(一个来自 `web.config`,一个来自 `machine.config`)。对于我们自定义提供程序使用的连接字符串,可以将其完全从 `web.config` 文件中删除,因为 Iron Speed Designer 不知道也不关心它。因此,我们只需在 `machine.config` 中定义它。
我们现在可以部署应用程序的更新版本,并且唯一需要修改 `web.config` 文件的是,当应用程序从开发迁移到测试时,开发人员会删除、重命名或注释掉 Iron Speed Designer 生成的连接字符串。从测试迁移到生产时,根本不需要修改,因为 Iron Speed Designer 生成的连接字符串已经被重命名、删除或注释掉在 `web.config` 中,并且已在 `machine.config` 中定义。此时,我们在开发人员的桌面(开发)、测试和生产服务器上都有 `machine.config` 文件。回想一下,我们使用 SQL 登录,并希望防止开发人员知道登录密码。为了实现这一点,我们对 `machine.config` 文件进行加密。
实用程序 `aspnet_regiis.exe` 允许您加密和解密 `web.config` 和 `machine.config` 文件中的节。ASP.NET 运行时将在应用程序运行时即时解密内容。下面的图 9 中显示的两个命令会加密 `machine.config` 文件中的 `connectionStrings` 和 `appSettings` 部分。
aspnet_regiis.exe -pd "connectionStrings. -pkm -prov "DataProtectionConfigurationProvider"
aspnet_regiis.exe -pd "appSettings. -pkm -prov "DataProtectionConfigurationProvider"
图 9 - 使用 `aspnet_regiis` 加密 `machine.config`
`–pkm` 选项告诉 `aspnet_regiis.exe` 加密 `machine.config` 文件中的指定节。省略 `–pkm` 选项将加密 `web.config` 文件。
从 `C:\Windows\Microsoft.NET\Framework\v2.0.50727`(或您的 .NET 版本目录)运行此命令。
作为最后的保护措施,您可以配置 Microsoft IIS 以不允许在测试和生产服务器上进行调试。这可以防止好奇的开发人员通过调试器单步执行代码并检查解密的连接字符串。
结论
在本文中,我们实施了策略,使我们能够将 ASP.NET 页面无缝地集成到现有 Web 应用程序中。此外,我们还了解了如何用我们自己的方法覆盖默认的 ASP.NET 2.0 表单机制,利用遗留安全数据——所有这些都无需用户登录 ASP.NET 环境。最后,我们介绍了如何使用 `machine.config` 文件提供特定于环境的配置数据,以及如何将此方法整合到 Iron Speed Designer 生成的应用程序中,以及如何保护 SQL 登录信息。