Web 应用程序的单Sign-On企业安全性

 

保罗·警长
PDSA, Inc.

2004 年 2 月

适用于:
    Microsoft® ASP.NET

摘要:了解为多个 Web 应用程序启用单一登录的技术。 本文还提供了示例代码,可帮助你先行创建一个具有单一登录的可靠企业安全系统。 (31 打印页)

下载本文的代码示例。 (下载示例之前请参阅安装示例。)

目录

简介
单一登录解决方案概述
类和页面

AppLauncher Web 应用程序
应用类
检索 Web 应用程序中的令牌
增强单一登录系统
安装示例
结论
相关书籍

简介

如果你的组织与大多数人一样,那么你可能有许多 Web 应用程序支持你的业务。 其中大多数 Web 应用程序都需要安全,因为你不希望用户进入他们想要的任何 Web 应用程序。 例如,应仅允许某些用户进入执行摘要决策支持系统,而其他用户应仅允许进入客户信息系统。 对于这些系统,可能还有不属于你的 Microsoft® Windows® 域的外部用户。 可以通过强制每个用户登录到系统的安全机制来控制这一点。 问题是用户不断被迫登录到每个不同的系统。 在一个甚至有五个 Web 应用程序的组织中,允许访问每个应用程序的用户将非常厌倦不断登录和关闭。 一定有更好的方法!

在本文中,你将了解一种在整个企业中使用单一登录解决方案的方法。 组织的内部用户将能够对应用程序使用Windows 身份验证,而外部用户将被迫登录 (请参阅图 1) 。

本文中介绍的主题

Web 应用程序的单一登录

  • 生成单一登录令牌
  • 将用户映射到应用程序
  • 基于表单的身份验证
  • Windows 集成的身份验证

单一登录解决方案概述

查看图 1,可以看到内部和外部用户登录公司 Intranet 上托管的网站时要经历的一系列步骤。 从 50,000 英尺的级别到此系统只有四个组件:Windows 身份验证网站、Active Directory、数据库服务器以及一个或多个基于表单的身份验证网站。

ms972971.singlesignon_01 (en-us,MSDN.10) .gif

图 1. 跨内部和外部用户的单一登录解决方案示例

Intranet 解决方案

从图 1 的左上角开始,你发现内部用户打开浏览器并导航到特定网站 (步骤 1) 。 此网站通过 Active Directory) (验证用户 (步骤 2) 其 Windows 凭据。 如果用户是有效的 Windows 用户,则允许他进入此网站。 进行身份验证后,将检索用户的标识,并调用数据库表 (步骤 3) ,其中包含指定用户可以运行的应用程序列表。 这些应用程序显示在 DataGrid 中供用户选择。

用户将单击要运行的应用程序, (步骤 4) 生成唯一的一次性使用令牌。 此令牌和用户的标识将存储在另一个数据库表中。 然后,该令牌通过用户想要运行的 Web 应用程序中) 查询行 (传递到特殊页面。 此特殊页面将从查询行中读取此令牌,并验证数据库表中是否存在该令牌 (步骤 5) 。 如果该令牌存在,它将从数据库中检索登录 ID,并删除存储此令牌的记录。 这会禁止其他人重用此令牌,并将登录 ID 发送回 Web 应用程序。

现在,你已在此 Web 应用程序中拥有用户的标识,因此需要生成一个 ASP.NET 表单身份验证票证,因为 Intranet 中的所有 Web 应用程序都将使用基于表单的身份验证。 然后,正在浏览此站点上所有安全页面的用户将使用此票证。

Extranet 解决方案

外部用户 (域外) 想要访问 Web 应用程序的外部用户将定向到与内部用户不同的起始页, (请参阅图 2) 。 内部和外部用户都定向到的 Web 应用程序使用基于表单的身份验证进行保护。 当外部用户尝试导航到此 Web 应用程序中的任何页面时,他们将被自动重定向到登录页。 此登录页与内部用户进行身份验证的页面不同。 用户必须输入其凭据,并且将调用同一数据库以确定用户是否对此应用程序有效。 如果是,则会为此用户的会话生成基于表单的正常身份验证 Cookie。

类和页面

需要创建一些类和网页来支持企业安全系统。 图 2 显示了需要为此系统编写的每个类和网页。 本文稍后将讨论此图中显示的每个类。 在下图之后,列出了每个类,并描述了每个类或网页的main函数在系统中。

ms972971.singlesignon_02 (en-us,MSDN.10) .gif

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

应用类

此类负责检索指定用户的应用程序列表。 此类还将生成新令牌,检索给定指定令牌的用户标识信息,并删除令牌。

表 1. Apps 类的方法

方法名 描述
GetAppsByLoginID 给定用户的域登录 ID,它将查找与此用户关联的应用程序,并返回应用程序的 DataSet。
CreateLoginToken 创建新的登录令牌并返回此令牌。
GenerateToken 生成新令牌的方法。 在此版本中,使用简单的 GUID 作为令牌。
VerifyLoginToken 给定令牌后,此方法将验证数据库中是否存在该令牌。 它创建 AppToken 类的实例,填充相应的信息并返回此新对象。
DeleteToken 从表中删除令牌。

AppToken 类

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

表 2. AppToken 类的属性

属性 说明
LoginID 表示用户登录 ID 的字符串值。
应用名称 一个字符串值,表示此 AppToken 记录所关联的应用程序名称。
LoginKey 一个整数值,它是 esUsers 表中用户的主键。
AppKey 一个整数值,它是 esApps 表中应用程序的主键。

AppUserRoles 类

此类负责检索有关尝试登录到应用程序的用户的信息。 一种方法将检查,以查看给定登录 ID 和应用程序密钥的登录名是否有效。 一种方法将返回给定用户的角色集。 在给定登录 ID 和应用程序密钥的情况下,一种方法将返回 esUser 表的主键。

AppLauncher 应用程序中的 Default.aspx

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

ms972971.singlesignon_03 (en-us,MSDN.10) .gif

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

每个网站中的 AppLogin.aspx

此网页类仅从应用程序启动器调用。 如果任何其他应用程序尝试调用此页面,它只会将用户重定向到网站的 Default.aspx 页。 由于每个 Web 应用程序都使用基于表单的身份验证,因此此重定向将强制 ASP.NET 将用户重定向到网站中的 Login.aspx 页面。

如果使用应用程序启动器站点中的令牌调用此页面,则此页面将调用 Apps 类上的方法来验证此令牌是否有效。 如果令牌有效,将从 Apps 类返回 AppToken 对象,以便此页面可以使用此对象中的信息创建基于表单的身份验证用户。

每个网站中的 Login.aspx

这是一个正常的登录网页,它将要求用户提供凭据,针对数据库检查这些凭据,以确保他们是有效的用户,如果有效,请创建身份验证票证,并重定向到用户在进入网站时请求的页面。

每个网站中的 Default.aspx

这是每个网站中的main登陆页面。 仅当用户通过 AppLogin 或登录网页进行身份验证时,才能导航到此页面和网站中的任何其他页面。

需要在数据库中创建几个表来支持此单一登录企业安全系统。 本文中的表没有很多字段,但足以了解这一点。 图 4 显示了数据库中需要创建的每个表之间的关系。 在图 4 中,你将找到每个表的列表,以及此解决方案中每个表的用途的说明。

ms972971.singlesignon_04 (en-us,MSDN.10) .gif

图 4。 实现包含角色的单一登录系统只需几个简单表

esApps

此表包含企业中所有 Web 应用程序的列表。 除了应用程序的名称外,此表中还保存了应用程序的长说明以及此应用程序所在位置的 URL。 URL 是完整的 URL,应始终将 AppLogin.aspx 页作为终结点。 在重定向到 Web 应用程序之前,应用程序启动器中的 default.aspx 页将负责将“Token=<GeneratedToken>”添加到 URL。

单击此处查看较大图像

图 5。 esApps 表的示例数据

esUsers

下表列出了可以使用应用程序的所有用户。 对于任何内部用户,都需要复制用户的域登录 ID。 可能还需要为外部用户添加密码字段。

ms972971.singlesignon_06 (en-us,MSDN.10) .gif

图 6。 esUsers 表的示例数据

esAppsUsers

此表将 esUsers 表中的用户与他们可以在 esApps 表中运行的应用程序相关联。 此表内的唯一数据是 esUsers 表和 esApps 表的外键。

esAppRoles

此表包含每个应用程序的一组角色。 例如,一个应用程序可能包含“管理员”和“用户”角色,另一个应用程序可能具有“用户”和“监督者”角色。

单击此处查看较大图像

图 7。 esAppRoles 表的示例数据

esAppUsersRoles

这是应用程序中每个用户的每个角色的列表。 用户“Joe”可能是“HR”应用程序中的“主管”,但可能是“工资单”应用程序中的“管理员”和“用户”。

esAppToken

esAppToken 包含从登录门户应用程序生成并传递到单个 Web 应用程序的令牌。 通常,这些记录应仅保留约两秒或更短的时间,因为正在调用的 Web 应用程序一旦从此表中收集信息,就会立即删除令牌。 这可以防止任何人重新使用令牌。

单击此处查看较大图像

图 8。 esAppToken 表的示例数据

AppLauncher Web 应用程序

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

ms972971.singlesignon_09 (en-us,MSDN.10) .gif

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

AppLauncherxx 中的 Default.aspx

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

// C#
private void Page_Load(object sender, System.EventArgs e)
{
  // Display the User Name
  // Without the Domain prefix
  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
  ' Display the User Name
  ' Without the Domain prefix
  lblLogin.Text = "Applications Available for: " & _
    Apps.LoginIDNoDomain(User.Identity.Name)

  AppLoad()
End Sub

调用 Apps 类中的 LoginIDNoDomain 静态方法以去除域前缀。 如果在名为“PDSA”的域中对登录 ID 为“Ken”的用户进行身份验证,则 User.Identity.Name 属性将返回“PDSA\Ken”。此方法将仅返回字符串“Ken”。

加载此用户可以运行的应用程序

AppLoad 方法将使用 Apps 类的实例来检索允许此用户运行的应用程序的 数据集 。 本文稍后将介绍 Apps 类中的 GetAppsByLoginID 方法。

// C#
private void AppLoad()
{
  Apps app = new Apps();

  try
  {
    // Load applications for this user
    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
    ' Load applications for this user
    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。 此 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];
    
    // Create a Token for this user/app
    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)
  {
    // Redirect to Web application 
    // passing in the generated token

    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)

    ' Create a Token for this user/app
    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
    ' Redirect to Web application 
    ' passing in the generated token
    Response.Redirect(e.CommandArgument.ToString() & _
      "?Token=" & token, False)
  End If
End Sub

你会注意到,在上面的代码中,你从 e.Item 参数检索 LinkButton 控件。 e.Item 是对 DataGrid 中单击的行的引用。 可以检索单击的 LinkButton 控件的特定实例,方法是转到 LinkButton 所在的列。 在本例中,这是在单元格 (0) 。 在该单元格中,可以通过在控件 (1) 获取控件。 控件位于位置 1 (1) 的原因是 DataGrid 中用于将 LinkButton 放入单元格的 ItemTemplate 被视为元素 0 (0) 。

检索 LinkButton 后,可以使用 Attributes 属性检索设置 LinkButton 时存储的 UserID 和 AppID。 Attributes 属性是添加到服务器控件(不属于原始控件定义)的任何其他属性的集合。

修改Web.Config

必须先将 Web.Config 文件中的 <身份验证> 元素设置为“Windows”,然后才能在 Web 应用程序中对用户进行身份验证。此外,必须在Web.config的 <授权> 元素中拒绝匿名用户。

<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数据库中创建表。 如果使用的是其他数据库系统,则需要根据数据库修改这些脚本。

应用类

现在,让我们看一下 应用 类,该类为此企业安全系统执行大部分工作。 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 方法。 此方法负责执行 Join 以检索所有适当的信息。 使用的 SQL 联接需要从 esApps 和 esAppsUsers 表获取信息。 但是,你还需要加入到 esUsers 表,因为你只想检索特定用户的应用程序,而我们提供的只是他们的登录 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;

  // Generate a new Token
  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

  ' Generate a new Token
  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 项目。

ms972971.singlesignon_10 (en-us,MSDN.10) .gif

图 10. 每个 Web 应用程序将 AppLauncherDataxx 项目与 AppLogin、登录名和默认页面结合使用

修改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 mode="Forms">
    <forms name="AppTest" loginUrl="Login.aspx" />
</authentication>

最后,还需要修改 <授权> 元素以拒绝匿名用户。

<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() == "")
    {
      // Not a valid login
      // Redirect them to default page 
      // This will put them at the login page
      Response.Redirect("default.aspx");
    }
    else
    {
      // Create a Forms Authentication Cookie
      // Set Forms authentication variables
      FormsAuthentication.Initialize();
      FormsAuthentication.SetAuthCookie(
        al.LoginID.ToString(), false);

      // Set the Application Token Object
      Application["AppToken"] = al;

      // Redirect to Default page
      Response.Redirect("default.aspx");
    }
  }
  catch
  {
    // Redirect them to the login page via the Default page
    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
      ' Not a valid login
      ' Redirect them to default page 
      ' This will put them at the login page
      Response.Redirect("default.aspx")
    Else
      ' Create a Forms Authentication Cookie
      ' Set Forms authentication variables
      FormsAuthentication.Initialize()
      FormsAuthentication.SetAuthCookie( _
        al.LoginID.ToString(), False)

      ' Set the Application Token Object
      Application("AppToken") = al

      ' Redirect to Default page
      Response.Redirect("default.aspx")
    End If

  Catch
    ' Redirect them to the login page via the Default page
    Response.Redirect("default.aspx")
  End Try
End Sub

在 VerifyLogin 方法中,首先检查令牌以查看它是否有效。 这是通过调用 Apps 类中的 VerifyLoginToken 方法完成的。 此方法返回 AppToken 类的实例。 如果填充了此类的 LoginID 属性,则表示你知道你有一个有效的用户。 如果不是,则表示令牌无效。 如果令牌无效,此方法会将用户重定向到网站中的 default.aspx 页。 当然,启用基于表单的身份验证后,这会强制用户重定向到 Login.aspx 页面,并要求他们登录。

表单身份验证 Cookie 通过调用 FormsAuthentication.Initialize 和 FormsAuthentication.SetAuthCookie 方法发出。 这会导致内存 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 应用程序,因此如果 Web 应用程序不响应启动请求,则可能存在问题。 如果发生这种情况,则会将令牌保留在数据库中,这会带来潜在的安全风险。 可能需要创建计划作业来删除早于指定分钟数的令牌。 或者,可以创建自己的特定令牌,该令牌由令牌和创建令牌的时间组成。 然后,只需修改 VerifyLoginToken 方法以检查时间,以确保它不大于指定的分钟数。

本文中使用的令牌是 GUID,它强制使用对数据库的调用来检索用户的个人资料信息。 此系统的一个很好的增强功能是使用基于WS-Security增强标准的令牌来加密配置文件信息,只需将此令牌作为令牌从应用程序启动器传递到每个 Web 应用程序即可。 这将消除每次到数据库的往返。

当然,你需要更改代码中的所有动态 SQL 调用,以使用存储过程和具有参数的命令对象,以避免 SQL 注入攻击的可能性,并完全保护这些表。 应在命令对象上使用存储过程和参数。

安装示例

有两个示例应用程序是为本文构建的。 它们是每个 Web 应用程序。 每个 Web 应用程序的解决方案中还包括一个类库。 这些示例使用 Microsoft Visual Basic® .NET 和 C# 编写,因此你可以选择要使用的版本。 有 。解决方案文件中包含的 SQL 文件可帮助你在数据库中创建适当的表。 。SQL 文件以SQL Server为目标,但可以进行修改以相当轻松地与其他数据库系统一起使用。 下面是安装示例应用程序时应执行的步骤。

  1. 在 DBMS 中创建名为 eSecurity 的数据库。
  2. 执行 。用于在 eSecurity 数据库中创建表的 SQL 文件。
  3. 将提供的 .ZIP 文件中的文件解压缩到文件夹中。
  4. 创建虚拟目录以指向要使用的每个文件夹。 例如,如果使用 VB.NET 示例,请创建两个名为 AppLauncherVB 和 AppTestVB 的虚拟目录,指向这两个文件夹。
  5. 修改 。指向虚拟目录文件夹的 SLN 文件。

结论

Web 应用程序的单一登录系统可帮助用户节省时间,并缓解企业内必须记住登录 ID 和密码的烦恼。 此外,可以允许外部用户使用内部 Web 应用程序,而无需将这些用户添加到域中。 实现此类系统只需几个简单的表和类即可。 虽然创建具有单一登录的可靠企业安全系统有很多需要,但本文中介绍的示例将为你提供良好的开端。

关于作者

Paul D. Sheriff 是 PDSA, Inc. (http://www.pdsa.com/products) 的总裁,该公司是一家 Microsoft 咨询公司,提供 .NET 咨询、产品和服务,包括 SDLC 文档和体系结构框架。 Paul 是南加州的 Microsoft 区域总监。 他的 .NET 书籍包括 ASP.NET 开发人员的 Jumpstart (Addison-Wesley) ,以及 PDSA 网站上列出的几本电子书。 可以直接通过 PSheriff@pdsa.com联系 Paul。

© Microsoft Corporation. 保留所有权利。