单一登录 Web 应用程序的企业级安全系统

Posted on 2006-06-30 15:34  齐国老兵  阅读(2634)  评论(2编辑  收藏  举报
 

单一登录 Web 应用程序的企业级安全系统

发布日期: 8/17/2004 | 更新日期: 8/17/2004

Paul D. Sheriff
PDSA, Inc.

适用范围:
Microsoft® ASP.NET

摘要:揭示能够以单一登录的方式登录多个 Web 应用程序的技术。本文还提供了示例代码,使您能够在完全使用单一登录的情况下创建强大的企业安全系统方面有一个良好的开端。

Download the code samples for this article(英文)。

*
本页内容
引言 引言
单一登录解决方案概述 单一登录解决方案概述
类和页面 类和页面
表
AppLauncher Web 应用程序 AppLauncher Web 应用程序
Apps 类 Apps 类
在 Web 应用程序中检索令牌 在 Web 应用程序中检索令牌
增强您的单一登录系统 增强您的单一登录系统
安装示例 安装示例
结论 结论
参考资料 参考资料

引言

如果您的公司在运转方式上与其他绝大多数公司相同,那么您可能拥有许多用于支持公司业务的 Web 应用程序。绝大多数这种类型的应用程序需要采取安全措施,因为您肯定不希望用户随心所欲地进入任何 Web 应用程序。例如,只允许某些用户进入执行摘要决策支持系统,而其他用户则应只允许进入客户信息系统。有可能还有一些使用这些系统的外部用户,他们并不是您的 Microsoft® Windows® 域用户。可以通过安全机制强制每个用户登录系统,从而对此进行控制。但这样做的问题在于,访问不同系统时,每个系统都会不断地要求用户登录。对于仅仅具有五个 Web 应用程序的公司,如果某用户被允许访问其中的每一个程序,该用户会因为频繁地登录和注销而厌烦不已。所以必须找到一个更好的办法。

在阅读本文后,您将了解一种在企业中应用单一登录解决方案的方法。公司的内部用户可以使用 Windows 身份验证访问应用程序,而外部用户将被强制进行登录(参见 1)。

本文所讨论的主题

单一登录 Web 应用程序

生成单一登录令牌

将用户映射至应用程序

基于窗体的身份验证

Windows 中集成的身份验证

单一登录解决方案概述

1 所示为内部用户和外部用户在登录到位于一个公司的 Intranet 上的网站时所执行的一系列步骤。在 50,000 英尺的级别上,这个系统仅有四个组件:通过 Windows 身份验证的网站、Active Directory、一个数据库服务器和一个或多个基于窗体进行身份验证的网站。

singlesignon_01

1:涵盖内部用户和外部用户的单一登录解决方案的示例

Intranet 解决方案

1 的左上角可以看到内部用户使用浏览器浏览到特定的网站(第 1 步)。这个网站验证(第 2 步)用户的 Windows 凭据(通过 Active Directory)。如果用户是有效的 Windows 用户,则允许其进入该站点。通过身份验证后,将检索用户的标识,并调用包含指定用户能够运行的应用程序列表的数据库表(第 3 步)。这些应用程序将显示在 DataGrid 中,以供用户选择。

用户单击希望运行的应用程序后,将生成一个唯一的、只能使用一次的令牌(第 4 步)。此令牌和用户标识将被存储在另一个数据库表中。然后此令牌被传递给用户要运行的 Web 应用程序中的一个特定页面(通过查询行)。此特殊页面从查询行读取该令牌,然后验证在数据库表中是否存在此令牌(第 5 步)。如果令牌存在,它将在数据库中检索登录 ID,然后删除存储此令牌的记录。此操作能够防止其他人再次使用此令牌并将登录 ID 发送回 Web 应用程序。

在了解了 Web 应用程序中的用户标识后,您需要生成一个 ASP.NET 窗体身份验证票据,因为在 Intranet 中,所有的 Web 应用程序都将使用基于窗体的身份验证。此票据将被正在浏览站点中所有的安全页面的用户所使用。

Extranet 解决方案

想要访问 Web 应用程序的外部用户(您所在域之外的用户)将被定向到与内部用户不同的起始页面(参见 2)。内部用户和外部用户被定向到的 Web 应用程序采取了基于窗体的身份验证的安全措施。当外部用户试图打开此 Web 应用程序中的任何页面时,他们将被自动重定向至登录页面。此登录页面与对内部用户进行身份验证的页面不同。用户必须输入其凭据,然后系统将调用同一个数据库,以确定该用户对此应用程序是否有效。如果有效,则将为此用户会话生成正常的基于窗体的身份验证 cookie。

类和页面

您需要创建若干类和 Web 页面以支持企业级安全系统。图 2 显示了需要为此系统编写的每个类和 Web 页。本文稍后将讨论此图所显示的每个类。在此图之后,列出了每个类的说明信息以及您的系统中的 Web 页面主要功能。

singlesignon_02

2:设计和开发单一登录解决方案只需要几个类和页面

Apps 类

此类用于检索指定用户的应用程序列表。它还将生成一个新令牌、根据给定的令牌检索用户的标识信息以及删除令牌。

表 1:Apps 类的方法
方法名称 说明

GetAppsByLoginID

根据给定用户的域登录 ID,查找与此用户关联的应用程序,并返回应用程序的 DataSet。

CreateLoginToken

创建并返回新登录令牌。

GenerateToken

生成新令牌的方法。在本版本中,将使用一个简单的 GUID 作为令牌。

VerifyLoginToken

根据给定令牌,此方法将验证数据库中是否存在此令牌。它将创建 AppToken 类的一个实例,在写入相应的信息后将其返回。

DeleteToken

删除表中的令牌。

AppToken 类

此类包含从 Apps 类的 VerifyLoginToken 方法返回令牌信息时所需的四个属性。表 2 说明了此类包含的四个属性。

表 2:AppToken 类的属性
属性 说明

LoginID

字符串,其值表示用户的登录 ID。

AppName

字符串,其值表示与此 AppToken 记录有关的应用程序名称。

LoginKey

整数类型,其值表示 esUsers 表中用户的主键。

AppKey

整数类型,其值表示 esApps 表中应用程序的主键。

AppUserRoles 类

此类用于检索试图登录应用程序的用户的信息。其中的一个方法根据给定的登录 ID 和应用程序键值检查登录是否有效。另一个方法返回给定用户的角色集。还有一个方法根据登录 ID 和应用程序键值返回 esUser 表的主键。

AppLauncher 应用程序中的 Default.aspx

此 Web 页类将检索通过了 IIS 身份验证的 Windows 用户,并返回允许这些用户访问的应用程序的列表。它将在 DataGrid 中显示此列表(图 3),并允许用户单击其中特定的应用程序。用户单击应用程序后,此页将生成一个新令牌,并将此令牌和用户 ID 存储到 esAppToken 表,然后调用此应用程序将令牌传递给该 Web 应用程序中的 AppLogin.aspx 页。

singlesignon_03

3:应用程序启动器显示允许登录的用户运行的应用程序的列表

每个网站中的 AppLogin.aspx

只能从应用程序启动器调用此 Web 页类。若有任何其他的应用程序试图调用此页,它会将用户重定向至网站的 Default.aspx 页。因为每个 Web 应用程序都使用基于窗体的身份验证,所以此操作将强制 ASP.NET 将用户重定向至站点中的 Login.aspx 页。

如果使用令牌从应用程序启动器站点调用此页,那么此页将调用 Apps 类中的方法,以验证此令牌是否有效。如果令牌有效,则 Apps 类将返回一个 AppToken 对象,以便此页能够使用此对象中的信息创建基于窗体的身份验证的用户。

每个网站中的 Login.aspx

这是常规的 Web 登录页面,它要求用户输入凭据,并在数据库中检查这些凭据,以确保是有效用户;此外,如果是有效用户,则还将创建身份验证票据,并在用户进入站点时,重定向至用户请求的页面。

每个网站中的 Default.aspx

这是每个网站的主登陆页面。只有通过了 AppLogin 或 Login Web 页面的身份验证的用户才能够浏览此页以及站点中所有其他页。

您还需要在数据库中创建若干表,用来支持此单一登录企业级安全系统。本文中的表所包含的字段不多,但就所述内容而言,已经足够。 4 显示了需要在数据库中创建的每个表之间的关系。在 4 之后,会看到这些表的列表,以及本解决方案中使用的每个表的说明信息。

singlesignon_04

4:实现单一登录系统的完整角色所需的若干表

esApps

此表包含企业中所有 Web 应用程序的列表。除应用程序的名称以外,表中还包含应用程序的详细说明信息以及应用程序的 URL。URL 是完整形式的 URL,并且应以 AppLogin.aspx 页结尾。应用程序启动器中的 default.aspx 页将负责在重定向至 Web 应用程序前向 URL 中添加字符串 “Token=<GeneratedToken>”。

singlesignon_05thumb

5esApps 表的示例数据

esUsers

此表列出可以使用应用程序的所有用户。您需要为所有内部用户复制您的用户域登录 ID。您可能还希望为外部用户添加密码字段。

singlesignon_06

6esUsers 表的示例数据

esAppsUsers

此表将 esUsers 表中的用户与 esApps 表中他们能够运行的应用程序关联起来。表中的数据只有 esUsers 表和 esApps 表的外键。

esAppRoles

此表包含每个应用程序的角色集。例如,一个应用程序可能包含“Admin”和“User”角色,而其他应用程序则可能包含“User”和“Supervisor”规则。

singlesignon_07thumb

7esAppRoles 表的示例数据

esAppUsersRoles

此表包含应用程序中每个用户的每一个角色的列表。在应用程序“HR”中,用户“Joe”可能是“Supervisor”,但在应用程序“Payroll”中,则可能是“Admin”和“User”。

esAppToken

esAppToken 包含由登录门户应用程序生成并传递给单个 Web 应用程序的令牌。这些记录在通常情况下应只存在两秒(或更短的时间),因为正被调用的 Web 应用程序在从此表中收集信息之后,会立即删除此令牌。这样可以防止他人再次使用此令牌。

singlesignon_08thumb

8 esAppToken 表的示例数据

AppLauncher Web 应用程序

应用程序启动器解决方案( 9)由通过 Windows 身份验证的站点和类库项目组成。此通过 Windows 身份验证的站点将直接读取线程中的用户域 ID,并利用此用户域 ID 在用户表中查找用户。我们看一下显示用户能够运行的应用程序的 Web 页。

singlesignon_09

9AppLauncherxx 解决方案包含 AppLauncherxx 项目和对 AppLauncherDataxx 项目的引用

AppLauncherxx 中的 Default.aspx

应用程序启动器网站只需要一个 Web 页:即 default.aspx。此 Web 页从页面的 User 对象中读取用户的 Windows 登录 ID,并加载数据库中为此用户定义的应用程序列表。以下是加载 default.aspx 页面时调用的 Page_Load 事件过程的代码。

// C#
            private void Page_Load(object sender, System.EventArgs e)
            {
            // 显示用户名称
            // 不带域前缀
            lblLogin.Text = "Applications Available for: " +
            Apps.LoginIDNoDomain(User.Identity.Name);
            AppLoad();
            }
            ' VB.NET
            Private Sub Page_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
            ' 显示用户名称
            ' 不带域前缀
            lblLogin.Text = "Applications Available for: " & _
            Apps.LoginIDNoDomain(User.Identity.Name)
            AppLoad()
            End Sub
            

调用 Apps 类的静态方法 LoginIDNoDomain 是为了去除域前缀。如果登录 ID 为“Ken”的用户通过了名为“PDSA”的域的身份验证,那么 User.Identity.Name 属性将返回“PDSA\Ken”。而此方法则仅返回字符串“Ken”。

加载此用户能够运行的应用程序

AppLoad 方法使用 Apps 类的一个实例检索允许此用户运行的应用程序的 DataSet。本文后面的内容将显示 Apps 类中的 GetAppsByLoginID 方法。

// C#
            private void AppLoad()
            {
            Apps app = new Apps();
            try
            {
            // 为此用户加载应用程序
            grdApps.DataSource =
            app.GetAppsByLoginID(User.Identity.Name);
            grdApps.DataBind();
            }
            catch (Exception ex)
            {
            lblMessage.Text = ex.Message;
            }
            }
            ' VB.NET
            Private Sub AppLoad()
            Dim app As New Apps
            Try
            ' 为此用户加载应用程序
            grdApps.DataSource = app.GetAppsByLoginID(User.Identity.Name)
            grdApps.DataBind()
            Catch ex As Exception
            lblMessage.Text = ex.Message
            End Try
            End Sub
            

LinkButton 控件

default.aspx 页面的 DataGrid 控件显示指定用户的应用程序列表后(参见 3),用户就可以选择其中的应用程序。DataGrid 中用于显示超级链接以供用户单击的控件即为 LinkButton。在 Web 页面中,LinkButton 定义如下:

<asp:LinkButton id=lnkApp runat="server"
            AppID='<%# DataBinder.Eval(Container.DataItem, "iAppID") %>'
            UserID='<%# DataBinder.Eval(Container.DataItem, "iUserID") %>'
            CommandArgument='<%# DataBinder.Eval(Container.DataItem, "sURL") %>'
            Text='<%# DataBinder.Eval(Container.DataItem, "sAppName") %>'>
            </asp:LinkButton>
            

从上述代码可以看出,增添了一些 esApps 表的主键 (iAppID) 和 esUsers 表的主键 (iUserID) 的附加属性。这些属性以及 CommandArgument 中的 URL 提供了可存储到数据库中的足够的用户信息。稍后您会看到,我们将在 ItemCommand 事件过程中检索这些附加属性。

提示: 可以向服务器控件添加任何想要的属性。ASP.NET 将忽略这些属性,但是您可以使用服务器控件的 Attributes 属性检索它们的值。

ItemCommand 事件

用户单击 DataGrid 中的 LinkButton 控件后,将调用 ItemCommand 事件过程。在此方法中,需要创建 Apps 类的一个新实例,并检索 LinkButton 控件,以便获取属性,然后调用 Apps 类中的 CreateLoginToken 方法,以将此数据存储到数据库中的 esAppToken 表中。最后,从此方法检索出令牌后,此令牌将与 LinkButton 的 CommandArgument 属性中的 URL 连接,然后调用 Response.Redirect,以调用令牌所传递的 Web 应用程序。

// C#
            private void grdApps_ItemCommand(object source,
            System.Web.UI.WebControls.DataGridCommandEventArgs e)
            {
            Apps app = new Apps();
            bool redirect = false;
            string token = String.Empty;
            LinkButton lb;
            try
            {
            lb = (LinkButton) e.Item.Cells[0].Controls[1];
            // 为此用户或应用程序创建令牌
            token = app.CreateLoginToken(
            lb.Text,
            User.Identity.Name,
            Convert.ToInt32(lb.Attributes["UserID"]),
            Convert.ToInt32(lb.Attributes["AppID"]));
            redirect = true;
            }
            catch (Exception ex)
            {
            redirect = false;
            lblMessage.Text = ex.Message;
            }
            if (redirect)
            {
            // 重定向至生成的令牌中
            // 传递的 Web 应用程序
            Response.Redirect(e.CommandArgument.ToString() +
            "?Token=" + token, false);
            }
            }
            ' VB.NET
            Private Sub grdApps_ItemCommand(ByVal source As Object, _
            ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _
            Handles grdApps.ItemCommand
            Dim app As New Apps
            Dim boolRedirect As Boolean
            Dim token As String
            Dim lb As LinkButton
            Try
            lb = DirectCast(e.Item.Cells(0).Controls(1), LinkButton)
            ' 为此用户或应用程序创建令牌
            token = app.CreateLoginToken(lb.Text, _
            User.Identity.Name, _
            Convert.ToInt32(lb.Attributes("UserID")), _
            Convert.ToInt32(lb.Attributes("AppID")))
            boolRedirect = True
            Catch ex As Exception
            boolRedirect = False
            lblMessage.Text = ex.Message
            End Try
            If boolRedirect Then
            ' 重定向至生成的令牌中
            ' 传递的 Web 应用程序
            Response.Redirect(e.CommandArgument.ToString() & _
            "?Token=" & token, False)
            End If
            End Sub
            

您会注意到,在上面的代码中,检索了 e.Item 参数中的 LinkButton 控件。e.Item 用于引用您在 DataGrid 中单击的行。您可以在 LinkButton 所在的列后检索所单击的 LinkButton 控件的特定实例。在本例中,它在 Cells(0) 中。在此单元格中,可以在 Controls(1) 后取得该控件。该控件位于位置一 (1) 的原因是 DataGrid 中所使用的、允许我们将 LinkButton 置于单元格中的 ItemTemplate 被看作是元素零 (0)。

检索出 LinkButton 后,就可以使用 Attributes 属性检索在设置 LinkButton 时存储的 UserID 和 AppID。Attributes 属性是您向服务器控件添加的所有附加属性的集合,这些附加属性不是初始控件定义的组成部分。

修改 Web.Config

在 Web 应用程序对用户进行身份验证之前,必须将 Web.Config 文件中的 <authentication> 元素设置为“Windows”。此外,还必须拒绝 Web.config 中的 <authorization> 元素中的匿名用户。

<authorization>
            <deny users="?" />
            </authorization>
            

设置这两个元素可从浏览器强制服务器检索用户的 Windows 凭据。当然,只有当您在用户所登录的域中使用 Internet Explorer 时,它才会起作用。

最后一项要完成的工作是存储连接字符串,以便从数据库的表中获取。在本文的示例中,我只使用了 Web.config 中的 <appSettings> 部分存储连接字符串。

<appSettings>
            <add key="eSecurityConnectString"
            value="server=(local);Database=eSecurity;uid=myUserID;pwd=myPassword" />
            </appSettings>
            

在本文中,我在 SQL Server™ 中创建了名为 eSecurity 的数据库,并创建了本文前面内容中所述的所有的表。本文的示例代码包含一个 SQL 脚本,可以运行此脚本以在 SQL Server 数据库中创建表。如果使用其他数据库系统,则需要根据您的数据库对脚本进行适当的修改。

Apps 类

现在看一下在此企业级安全系统中用于实现主要功能的 Apps 类。AppLauncherData 程序集包含的三个类中的每一个类都可负责为用户加载应用程序、加载用户角色以及操作安全令牌。较好的做法是保留与数据库进行交互的功能,以及保留在用户界面层以外操作令牌的功能。这可以使您不必更改用户界面就可以修改令牌的创建方法,以及修改与数据库交互的方式。

Apps 类负责操作令牌以及为用户加载应用程序。我们看一下这个类的定义。

// C#
            public class Apps
            {
            string mConnectString;
            public Apps()
            {
            mConnectString = ConfigurationSettings.
            AppSettings["eSecurityConnectString"];
            }
            ...
            }
            ' VB.NET
            Public Class Apps
            Private mConnectString As String
            Public Sub New()
            mConnectString = ConfigurationSettings. _
            AppSettings("eSecurityConnectString")
            End Sub
            ...
            End Class
            

可以看到这个类首先使用从 Web.config 文件中检索出的连接字符串加载成员变量。

GetAppsByLoginID 方法

要为特定的用户加载应用程序,应将用户的登录 ID 传递给 GetAppsByLoginID 方法。此方法负责执行联接,以检索所有相关的信息。使用 SQL 联接需要从 esApps 和 esAppsUsers 表中获取信息。此外,还需要联接 esUsers 表,因为只需要检索特定用户的应用程序,而所有我们可用的信息只有用户的 Login ID,所以必须在 esUsers 表中查找用户的主键,以便联接到其他表。

注意: 本文使用动态 SQL 只是为了显示概念。在实际的企业级安全系统中,需要对所有的 SQL 调用使用存储过程。

// C#
            public DataSet GetAppsByLoginID(string loginID)
            {
            DataSet ds = new DataSet();
            SqlCommand cmd;
            SqlDataAdapter da;
            string sql;
            sql = "SELECT esApps.iAppID, esAppsUsers.iUserID, ";
            sql += " esApps.sAppName, esApps.sDesc, esApps.sURL ";
            sql += " FROM esApps";
            sql += " INNER JOIN esAppsUsers ";
            sql += " ON esApps.iAppID = esAppsUsers.iAppID ";
            sql += " INNER JOIN esUsers ";
            sql += " ON esAppsUsers.iUserID = esUsers.iUserID ";
            sql += " WHERE sLoginID = @sLoginID ";
            sql = String.Format(sql, Apps.LoginIDNoDomain(loginID));
            try
            {
            cmd = new SqlCommand(sql);
            cmd.Parameters.Add(new
            SqlParameter("@sLoginID", SqlDbType.Char));
            cmd.Parameters["@sLoginID"].Value =
            Apps.LoginIDNoDomain(loginID);
            cmd.Connection = new SqlConnection(mConnectString);
            da = new SqlDataAdapter(cmd);
            da.Fill(ds);
            return ds;
            }
            catch (Exception ex)
            {
            throw ex;
            }
            }
            ' VB.NET
            Public Function GetAppsByLoginID(ByVal LoginID As String) _
            As DataSet
            Dim ds As New DataSet
            Dim cmd As SqlCommand
            Dim da As SqlDataAdapter
            Dim sql As String
            sql = "SELECT esApps.iAppID, esAppsUsers.iUserID, "
            sql &= " esApps.sAppName, esApps.sDesc, esApps.sURL "
            sql &= " FROM esApps"
            sql &= " INNER JOIN esAppsUsers "
            sql &= " ON esApps.iAppID = esAppsUsers.iAppID "
            sql &= " INNER JOIN esUsers "
            sql &= " ON esAppsUsers.iUserID = esUsers.iUserID "
            sql &= " WHERE sLoginID = @sLoginID "
            sql = String.Format(sql, Apps.LoginIDNoDomain(LoginID))
            Try
            cmd = New SqlCommand(sql)
            cmd.Parameters.Add(New _
            SqlParameter("@sLoginID", SqlDbType.Char))
            cmd.Parameters("@sLoginID").Value = _
            Apps.LoginIDNoDomain(LoginID)
            cmd.Connection = New SqlConnection(mConnectString)
            da = New SqlDataAdapter(cmd)
            da.Fill(ds)
            Return ds
            Catch ex As Exception
            Throw ex
            End Try
            End Function
            

CreateLoginToken 方法

用户单击在 DataGrid 中的应用程序后,必须生成一个新令牌。CreateLoginToken 方法负责执行此任务。

// C#
            public string CreateLoginToken(string appName,
            string loginID, int userID, int appID)
            {
            SqlCommand cmd = new SqlCommand();
            SqlParameter param;
            string token;
            string sql;
            // 生成新令牌
            token = GenerateToken();
            sql = "INSERT INTO esAppToken(sToken, sAppName, ";
            sql += " sLoginID, iUserID, iAppID, dtCreated) ";
            sql += " VALUES(@sToken, @sAppName, @sLoginID, ";
            sql += "        @iUserID, @iAppID, @dtCreated) ";
            param = new SqlParameter("@sToken", SqlDbType.Char);
            param.Value = token;
            cmd.Parameters.Add(param);
            param = new SqlParameter("@sAppName", SqlDbType.Char);
            param.Value = appName;
            cmd.Parameters.Add(param);
            param = new SqlParameter("@sLoginID", SqlDbType.Char);
            param.Value = Apps.LoginIDNoDomain(loginID);
            cmd.Parameters.Add(param);
            param = new SqlParameter("@iUserID", SqlDbType.Int);
            param.Value = userID;
            cmd.Parameters.Add(param);
            param = new SqlParameter("@iAppID", SqlDbType.Int);
            param.Value = appID;
            cmd.Parameters.Add(param);
            param = new SqlParameter("@dtCreated", SqlDbType.DateTime);
            param.Value = DateTime.Now;
            cmd.Parameters.Add(param);
            try
            {
            cmd.CommandType = CommandType.Text;
            cmd.CommandText = sql;
            cmd.Connection = new SqlConnection(mConnectString);
            cmd.Connection.Open();
            cmd.ExecuteNonQuery();
            }
            catch (Exception ex)
            {
            throw ex;
            }
            finally
            {
            if (cmd.Connection.State != ConnectionState.Closed)
            {
            cmd.Connection.Close();
            cmd.Connection.Dispose();
            }
            }
            return token;
            }
            ' VB.NET
            Public Function CreateLoginToken(ByVal AppName As String, _
            ByVal LoginID As String, ByVal UserID As Integer, _
            ByVal AppID As Integer) As String
            Dim cmd As New SqlCommand
            Dim param As SqlParameter
            Dim token As String
            Dim sql As String
            ' 生成新令牌
            token = GenerateToken()
            sql = "INSERT INTO esAppToken(sToken, sAppName, "
            sql &= " sLoginID, iUserID, iAppID, dtCreated) "
            sql &= " VALUES(@sToken, @sAppName, @sLoginID, "
            sql &= " @iUserID, @iAppID, @dtCreated)"
            sql = String.Format(sql, token, AppName, _
            Apps.LoginIDNoDomain(LoginID), UserID, AppID, _
            DateTime.Now.ToString())
            param = New SqlParameter("@sToken", SqlDbType.Char)
            param.Value = token
            cmd.Parameters.Add(param)
            param = New SqlParameter("@sAppName", SqlDbType.Char)
            param.Value = AppName
            cmd.Parameters.Add(param)
            param = New SqlParameter("@sLoginID", SqlDbType.Char)
            param.Value = Apps.LoginIDNoDomain(LoginID)
            cmd.Parameters.Add(param)
            param = New SqlParameter("@iUserID", SqlDbType.Int)
            param.Value = UserID
            cmd.Parameters.Add(param)
            param = New SqlParameter("@iAppID", SqlDbType.Int)
            param.Value = AppID
            cmd.Parameters.Add(param)
            param = New SqlParameter("@dtCreated", SqlDbType.DateTime)
            param.Value = DateTime.Now
            cmd.Parameters.Add(param)
            Try
            cmd.CommandType = CommandType.Text
            cmd.CommandText = sql
            cmd.Connection = New SqlConnection(mConnectString)
            cmd.Connection.Open()
            cmd.ExecuteNonQuery()
            Catch ex As Exception
            Throw ex
            Finally
            If cmd.Connection.State <> ConnectionState.Closed Then
            cmd.Connection.Close()
            cmd.Connection.Dispose()
            End If
            End Try
            Return token
            End Function
            

要创建令牌,应调用 GenerateToken 方法。此方法独立于 CreateLoginToken 方法的原因是,它允许您更改以后要生成的令牌的类型。在本文的末尾,提出了有关的设想。此方法使用 Guid 类生成一个新的 GUID 作为令牌。

// C#
            public string GenerateToken()
            {
            return System.Guid.NewGuid().ToString();
            }
            ' VB.NET
            Public Function GenerateToken() As String
            Return System.Guid.NewGuid().ToString()
            End Function
            

在 Web 应用程序中检索令牌

要从应用程序启动器测试启动应用程序,应创建一个可从启动器调用的用于测试的 Web 应用程序(参见 10)。这个用于测试的 Web 应用程序将使用前面所讨论的 AppLauncherDataxx 项目。

singlesignon_10

10:每个 Web 应用程序都将使用结合了 AppLoginLogin Default 页面的 AppLauncherDataxx 项目

修改 Web.Config

要创建集成了单一登录系统的 Web 应用程序,首先需要修改 Web.config 文件,并创建 <appSettings> 区段,以存储连接字符串,从而与 eSecurity 数据库进行交互。还需要存储应用程序 ID 和应用程序名称,以供外部用户进入时使用。因为外部用户进入时,尚未创建令牌,这时需要知道用户选择的应用程序,以便在建立用户角色时加载要使用的 AppToken 对象。在本文后面的内容中,您会了解如何这样做。

<appSettings>
            <add key="eSecurityConnectString"
            value="server=(local);Database=
            eSecurity;uid=mUserID;pwd=myPassword"></add>
            <add key="eSecurityAppID" value="1"></add>
            <add key="eSecurityAppName" value="Payroll"></add>
            </appSettings>
            

您还需要设置 Web 应用程序,以使用基于窗体的身份验证。要进行此操作,请修改 <authentication> 元素,如下所示。

<authentication mode="Forms">
            <forms name="AppTest" loginUrl="Login.aspx" />
            </authentication>
            

最后,为了拒绝匿名用户访问,还需要修改 <authorization> 元素。

<authorization>
            <deny users="?" />
            </authorization>
            

AppLogin.aspx 页

请记住,从应用程序启动器调用的每个应用程序都会调用 AppLogin 页,并向此页传递生成的令牌。AppLogin 页会验证此令牌是否正确,从 esAppToken 表检索相关的信息,然后删除 esAppToken 中的记录,以使该令牌不被重复使用。

// C#
            private void Page_Load(object sender, System.EventArgs e)
            {
            VerifyToken();
            }
            private void VerifyToken()
            {
            Apps app = new Apps();
            AppToken al;
            try
            {
            al = app.VerifyLoginToken(
            Request.QueryString["Token"].ToString());
            if(al.LoginID.Trim() == "")
            {
            // 非有效登录
            // 将其重定向至默认页面
            // 这将使用户转至登录页面
            Response.Redirect("default.aspx");
            }
            else
            {
            // 创建窗体身份验证 Cookie
            // 设置窗体身份验证变量
            FormsAuthentication.Initialize();
            FormsAuthentication.SetAuthCookie(
            al.LoginID.ToString(), false);
            // 设置应用程序令牌对象
            Application["AppToken"] = al;
            // 重定向至默认页面
            Response.Redirect("default.aspx");
            }
            }
            catch
            {
            // 通过默认页面将用户重定向至登录页面
            Response.Redirect("default.aspx");
            }
            }
            ' VB.NET
            Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            VerifyToken()
            End Sub
            Private Sub VerifyToken()
            Dim app As New Apps
            Dim al As AppToken
            Try
            al = app.VerifyLoginToken( _
            Request.QueryString("Token").ToString())
            If al.LoginID.Trim() = "" Then
            ' 非有效登录
            ' 将其重定向至默认页面
            ' 这将使用户转至登录页面
            Response.Redirect("default.aspx")
            Else
            ' 创建窗体身份验证 Cookie
            ' 设置窗体身份验证变量
            FormsAuthentication.Initialize()
            FormsAuthentication.SetAuthCookie( _
            al.LoginID.ToString(), False)
            ' 设置应用程序令牌对象
            Application("AppToken") = al
            ' 重定向至默认页面
            Response.Redirect("default.aspx")
            End If
            Catch
            ' 通过默认页面将用户重定向至登录页面
            Response.Redirect("default.aspx")
            End Try
            End Sub
            

在 VerifyLogin 方法中,首先检查令牌是否有效。这一操作通过调用 Apps 类中的 VerifyLoginToken 方法完成。此方法返回 AppToken 类的一个实例。如果此类的 LoginID 属性存在值,就表明此用户是有效用户。如果没有值,则令牌无效。在令牌无效的情况下,此方法将用户重定向到网站中的 default.aspx 页面。当然,如果打开了基于窗体的身份验证,用户会被强制重定向至 Login.aspx 页面,被请求重新登录。

调用 FormsAuthentication.Initialize 和 FormsAuthentication.SetAuthCookie 方法可以向外发送窗体验证 cookie。这会将内存中的 cookie 发送到浏览器。每次用户返回此站点时,ASP.NET 运行库都会检查此 cookie。

VerifyLoginToken 方法

此方法通过查询行接受生成的令牌,并对其进行检查,以确保令牌有效。此方法会转到 esAppToken 表检查此令牌。如果在表中找到此令牌,则将表中所有的值置于 AppToken 对象的各个属性中,然后从此方法返回该 AppToken 对象。

// C#
            public AppToken VerifyLoginToken(string Token)
            {
            AppToken al = new AppToken();
            DataSet ds = new DataSet();
            SqlCommand cmd;
            DataRow dr;
            SqlDataAdapter da;
            string sql;
            sql = "SELECT iAppTokenID, sAppName, sLoginID, ";
            sql += " iAppID, iUserID ";
            sql += " FROM esAppToken";
            sql += " WHERE sToken = @sToken ";
            try
            {
            cmd = new SqlCommand(sql);
            cmd.Parameters.Add(new
            SqlParameter("@sToken", SqlDbType.Char));
            cmd.Parameters["@sToken"].Value = Token;
            cmd.Connection = new SqlConnection(mConnectString);
            da = new SqlDataAdapter(cmd);
            da.Fill(ds);
            if (ds.Tables[0].Rows.Count > 0)
            {
            dr = ds.Tables[0].Rows[0];
            al.LoginID = dr["sLoginID"].ToString();
            al.AppName = dr["sAppName"].ToString();
            al.AppKey = Convert.ToInt32(dr["iAppID"]);
            al.LoginKey = Convert.ToInt32(dr["iUserID"]);
            DeleteToken(Convert.ToInt32(dr["iAppTokenID"]));
            }
            }
            catch (Exception ex)
            {
            throw ex;
            }
            return al;
            }
            ' VB.NET
            Public Function VerifyLoginToken(ByVal Token As String) As AppToken
            Dim al As New AppToken
            Dim ds As New DataSet
            Dim cmd As SqlCommand
            Dim dr As DataRow
            Dim da As SqlDataAdapter
            Dim sql As String
            sql = "SELECT iAppTokenID, sAppName, sLoginID, "
            sql &= " iAppID, iUserID "
            sql &= " FROM esAppToken"
            sql &= " WHERE sToken = @sToken "
            Try
            cmd = New SqlCommand(sql)
            cmd.Parameters.Add(New _
            SqlParameter("@sToken", SqlDbType.Char))
            cmd.Parameters("@sToken").Value = Token
            cmd.Connection = New SqlConnection(mConnectString)
            da = New SqlDataAdapter(cmd)
            da.Fill(ds)
            If ds.Tables(0).Rows.Count > 0 Then
            dr = ds.Tables(0).Rows(0)
            al.LoginID = dr("sLoginID").ToString()
            al.AppName = dr("sAppName").ToString()
            al.AppKey = Convert.ToInt32(dr("iAppID"))
            al.LoginKey = Convert.ToInt32(dr("iUserID"))
            DeleteToken(Convert.ToInt32(dr("iAppTokenID")))
            End If
            Catch ex As Exception
            Throw ex
            End Try
            Return al
            End Function
            

基于角色的安全性

用户通过 Web 应用程序的验证后,将被重定向至 default.aspx 页面。因为已经向浏览器发送了身份验证 cookie,每次都会将此 cookie 发送回来。在向用户传送请求的页面之前,将调用 global.asax 文件中的 Application_AuthenticateRequest 方法。只有在这个方法中才能够建立要与此用户关联的任何角色。我不打算在这里显示此方法的用法,因为专门有文章对其进行讨论。您可以查看示例代码中使用此方法的例子。基于角色的安全性的代码十分简单。您完全可以改进此代码,使其能够使用缓存。它的目的只是告诉您如何着手去做。

增强您的单一登录系统

本文介绍了创建企业级安全系统方面的很多内容。但若要介绍创建企业级安全系统的所有方面,则超出了本文的范围。例如,您还应当添加一系列的维护屏幕,以允许管理员添加用户,并且建立用户和应用程序之间的映射关系。也需要对这组屏幕采取安全措施,以便只有特定角色内的用户才能够打开它们。

系统另外一个需要增强的地方是自动添加不在系统中的域用户的能力。它能够帮助您向系统中添加用户,而不必手工输入用户信息,要么您也可以编写添加用户的应用程序。您一般会将这些用户指定为默认角色,以便只允许他们运行某些应用程序而不是全部应用程序。此外,如果以这种方式添加了新用户,管理员还应当收到电子邮件形式的通知信息。

因为这种安全系统依赖于生成的特定的令牌,而在创建令牌之后,会立即调用 Web 应用程序,所以如果应用程序未启动以响应请求,就可能出现问题。如果出现这种情况,那么令牌就会留在数据库中,从而导致潜在的安全性风险。所以还需要预先安排一项工作,用来删除超过指定时间长度的令牌。也可以创建自己特定的令牌,该令牌由令牌和创建此令牌的时间组成。这样就只需修改 VerifyLoginToken 方法,以检查时间,确保它小于指定的时间长度。

本文中使用的令牌为 GUID 类型,它强制您回调数据库,以检索用户的配置文件信息。本系统的一个较好的增强方案是使用基于 WS-Security 增强标准的令牌加密配置文件信息,并仅将其作为令牌由应用程序启动器传递给每个 Web 应用程序。这样能够避免每次往返数据库。

当然,你可能希望更改代码中的所有动态 SQL 调用,以使用存储过程和带参数的命令对象来避免遭受任何 SQL 流量注入攻击,彻底保证数据表的安全。对于命令对象,应使用存储过程和参数。

安装示例

本文附带两个示例应用程序。它们都是 Web 应用程序。解决方案中还包含为每个 Web 应用程序提供的类库。示例具有 Microsoft Visual Basic® .NET 和 C# 两种版本,可以选择希望使用的版本。解决方案文件中还有一个 .SQL 文件,可用来帮助您在数据库中创建所需的表。.SQL 文件用于 SQL Server,但经过简单的修改,也可用于其他数据库系统。以下是安装示例应用程序应执行的步骤。

在 DBMS 中创建名为 eSecurity 的数据库。

执行 .SQL 文件,在 eSecurity 数据库中创建表。

将提供的 .ZIP 文件解压到文件夹中。

创建虚拟目录,使其指向希望使用的每个文件夹。例如,如果使用的是 VB.NET 版本的示例,那么请创建两个名称分别为 AppLauncherVB 和 AppTestVB 的虚拟目录,并使其指向这两个文件夹。

修改 .SLN 文件,使其指向您创建的虚拟目录文件夹。

结论

Web 应用程序的单一登录系统有助于节省用户时间,并减轻在企业中因需要记忆登录 ID 和密码所带来的不便。此外,它还允许外部用户使用内部 Web 应用程序,而不必将这些用户添加到域中。实现这样一个系统只需创建几个简单的表和类。但要创建一个完全使用单一登录方式的可靠的企业级安全系统,还需要做许多工作,本文中的示例可以使您有一个良好的开端。

参考资料

ASP.NET Developer's Jumpstart(英文)

Building Secure Microsoft ASP.NET Applications(英文)

关于作者

Paul D. Sheriff 是 PDSA, Inc. (http://www.pdsa.com/products) 的总裁,该公司是 Microsoft 的咨询公司和合作伙伴,提供有关 .NET 的咨询、产品和服务,包括 SDLC 文档和体系结构框架。Paul 是 Microsoft 在南加利福尼亚的地区主管。他撰写的 .NET 方面的书籍包括《ASP.NET Developer's Jumpstart》(Addison-Wesley) 以及在 PDSA 网站上列出的一些电子图书。Paul 的联系邮件是 PSheriff@pdsa.com

© 2004 Microsoft Corporation 版权所有。保留所有权利。使用规定。



©2006 Microsoft Corporation. 版权所有.  保留所有权利 |商标 |隐私权声明
Microsoft

Copyright © 2024 齐国老兵
Powered by .NET 8.0 on Kubernetes