ASP.NET - Forms 身份验证用户模拟






4.63/5 (18投票s)
一个 ASP.NET 类和配套控件,用于以正确的方式为应用程序支持用户提供“以用户身份登录...”功能。
引言
构建一个完整的 Web 应用程序的一部分包括构建一个不错的管理和支持界面。一个好的支持界面的一个非常重要的部分是支持用户能够以任意客户端用户的身份登录。在我工作的公司,有几个人每天坐在办公室里使用此功能。该公司提供某种软件即服务,他们的(受信任的)支持员工处理在应用程序中遇到麻烦的客户端用户的支持电话。如果问题没有直接解决,支持用户就会使用本文中描述的功能登录到有问题的用户“并能够看到实际用户所看到的内容”。然后,支持用户要么通过电话指导客户完成他们想要完成的任何操作的过程,要么直接自己完成。有一半的时间,在我工作的公司,支持用户甚至会接到电话,并被要求为客户在其账户中执行某些操作。
为了使此类操作顺利进行,支持用户需要能够直接从管理和支持界面跳转到客户端帐户。我们不能重置密码之类的操作;当他们接到客户电话时,他们应该能够尽快高效地完成他们需要做的事情。
虽然将 ASP.NET 用户登录为另一个用户非常简单;事实上,调用 FormsAuthentication.SetAuthCookie()
就可以完成;然而,我需要一种方法来真正简化这种功能。我发现,支持用户在模拟其他用户后能够轻松地返回到他们自己的支持用户帐户,而无需再次登录支持用户帐户的麻烦。由于我找不到任何现有解决方案,我开发了 UserImpersonation
类和本文中介绍的 LoginUserImpersonation
控件来解决这个问题。
在本文中,我将首先简要介绍使用 UserImpersonation
类的要求以及如何使用它。之后,我将详细介绍我是如何实现用户模拟的,以及在开发过程中我做出决定的动机。
注意:本文与通常所说的“Windows 用户模拟”无关。本文仅涉及 ASP.NET Forms 身份验证用户。
使用 UserImpersonation 类
要求
为了能够使用 UserImpersonation
类,需要满足以下条件和要求
- 使用基于 cookie 的 FormsAuthentication 才能使用
UserImpersonation
类。 - 在使用
UserImpersonation
类时,强烈建议进行 cookie 加密。 - 仅在 .NET 2.0 上测试过。因此,
UserImpersonation
类应该可以在 .NET 2.0、3.0 和 3.5 上运行。 - 目前,您的 ASP.NET 应用程序/网站不使用
FormsAuthenticationTicket.UserData
属性。
参考
使用 UserImpersonation
类非常简单。UserImpersonation
位于 System.Web.Security
命名空间中,并具有以下成员
public static void ImpersonateUser(string userName);
public static void ImpersonateUser(string userName, string returnUrl);
public static void Deimpersonate();
private static string Serialize(string userName, string returnUrl);
private static bool Deserialize(string data, out string userName, out string returnUrl);
public static string PrevUserName { get; }
public static bool IsImpersonating { get; }
快速开始
为了立即在您的 Web 应用程序中使用用户模拟功能,请按照以下步骤操作
- 将
FormsAuthenticationUserImpersonation
预编译的二进制文件下载到您的 ASP.NET 应用程序/网站的 /Bin 文件夹中。此操作等同于为您的 ASP.NET 应用程序/网站“添加对FormsAuthenticationUserImpersonation
预编译二进制文件的引用”。 - 在您希望使用
LoginUserImpersonation
控件的页面上注册FormsAuthenticationUserImpersonation
程序集。这很可能是您的母版页。程序集的注册使用<%@ Page %>
指令完成,如下所示 - 将
LoginUserImpersonation
控件添加到您 ASP.NET 页面的所需位置。通常,您会希望将其放置在登录/注销按钮旁边。LoginUserImpersonation
控件将呈现为一个链接按钮,文本为“返回到 {UserName}” - 将以下代码添加到您希望实际启动用户模拟的代码中,其中
strUserName
包含您希望登录的用户的名称。建议在启动用户模拟后立即重定向用户,以确保更改生效
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="MasterPage.aspx.cs" Inherits="_Default"
Title="Your masterpage" %>
<%@ Register Assembly="FormsAuthenticationUserImpersonation"
Namespace="System.Web.UI.WebControls" TagPrefix="asp" %>
<asp:LoginUserImpersonation ID="LoginUserImpersonation1" runat="server" />
UserImpersonation.ImpersonateUser(strUserName);
Response.Redirect("~/YourPage.aspx");
包含的示例和源代码
包含的示例网站使用了一个基本的只读 MembershipProvider,提供两个用户:Bob 和 Alice。对于这两个用户,密码都是 test。
示例网站包含一个主页,上面有一个按钮允许登录的用户模拟 Alice,一个登录页面,以及一个只能由 Alice 访问的页面。
UserImpersonation
类和 LoginUserImpersonation
控件是用 C# 编写的。源代码已包含文档,对于任何至少轻微熟悉 ASP.NET 和 C# 的程序员来说,都应该很容易理解。
开发
现在我将讨论在开发此解决方案时遇到的一些有趣点。
设计目标
- 尽量使用户模拟逻辑尽可能接近现有的用户身份验证逻辑。
- 允许用户在模拟其他用户后轻松返回到他们原来的帐户,而无需再次登录他们自己的帐户。
跟踪原始用户
问题
正如我在引言中已经提到的,让一个用户登录为另一个用户实际上非常简单。然而,仅仅将支持用户登录为有问题的客户端用户,将导致支持用户在结束模拟其他用户后必须重新登录到他们自己的帐户。虽然这对于每月管理一次小型 Web 应用程序的某个人来说不是什么大问题,但对于整天通过应用程序帮助客户的人来说,这会变得非常恼人,我目前正在开发的应用程序就是这种情况。
简而言之,我需要一种方法来登录一个用户为另一个用户,同时跟踪他或她之前完全身份验证过的用户。这一切都需要以安全的方式完成,因为我希望用户能够通过单击鼠标一次返回到他们原来的用户帐户,无需重新输入密码。
身份验证 Cookie
处理此问题的一种安全且显而易见的方法是通过将此信息存储在 Session
属性袋中来跟踪先前用户。然而,这需要每个页面都启用会话,或者一个可由经过身份验证的用户访问的 HTTP 处理程序,并且需要额外的跟踪代码,因为会话不一定在用户身份验证 cookie 过期时过期。因此,我决定将先前用户的信息存储在会话中不是最佳选择。
存储此信息的逻辑位置是身份验证 cookie 本身,不是吗?它经过安全编码,所有信息都在 cookie 过期时过期(duh)。然而,身份验证 cookie 的可访问性不如 Session
属性袋。经过在 MSDN 上一番查找,我注意到了 FormsAuthenticationTicket
类,它在某种程度上代表了身份验证 cookie 的解码版本,实际上公开了一个允许将自定义数据包含在 cookie 中的属性:FormsAuthenticationTicket.UserData
。
“搭便车”到身份验证 cookie
当进行用户模拟时,我使用以下代码创建一个新的身份验证票证给要模拟的用户,将描述先前用户的数据“搭便车”到其中,如下面的代码所示,该代码摘自 UserImpersionation.ImpersonateUser
函数
// Declare variables
HttpContext context;
FormsAuthenticationTicket authTicket;
HttpCookie authCookie;
string strSerializedData;
// Store impersonation data in authentication ticket.
context = HttpContext.Current;
strSerializedData = Serialize(context.User.Identity.Name, returnUrl);
authCookie = FormsAuthentication.GetAuthCookie(userName, false);
authTicket = FormsAuthentication.Decrypt(authCookie.Value);
authTicket = new FormsAuthenticationTicket(authTicket.Version, authTicket.Name,
authTicket.IssueDate, authTicket.Expiration,
authTicket.IsPersistent, strSerializedData, authTicket.CookiePath);
authCookie.Value = FormsAuthentication.Encrypt(authTicket);
context.Response.Cookies.Add(authCookie);
首先,当前用户的用户名和可选的返回 URL 使用一个简单的序列化函数连接起来。然后,为要模拟的用户创建一个新的身份验证 cookie,并存储在 authCookie
变量中。由于 FormsAuthentication
类直接生成加密的身份验证 cookie,因此该 cookie 被解码为存储在 authTicket
变量中的 FormsAuthenticationTicket
类。
由于 FormsAuthenticationTicket
类中的所有属性都是只读的,因此有效地创建了一个新的 FormsAuthenticationTicket
类实例,克隆了先前获得的 FormsAuthenticationTicket
类实例,但也包含了我们的序列化数据。然后,这个新的 FormsAuthenticationTicket
类实例被加密到我们的新身份验证 cookie 中,该 cookie 随后被添加到响应头中。
检索先前用户的姓名
在设置了新的启用用户模拟的身份验证 cookie 后,就可以像下面代码中所示一样轻松地获取先前的用户名,该代码摘自 UserImpersionation.PrevUserName.get
属性
// Declare variables
HttpContext context;
FormsIdentity formsIdentity;
string strUserName, strReturnUrl;
// Get data from auth ticket
context = HttpContext.Current;
formsIdentity = (FormsIdentity)context.User.Identity;
if (string.IsNullOrEmpty(formsIdentity.Ticket.UserData))
return string.Empty;
if (!Deserialize(formsIdentity.Ticket.UserData,
out strUserName, out strReturnUrl))
return string.Empty;
return strUserName;
首先,将 User.Identity
属性(默认公开 IIdentity
接口)强制转换为 FormsIdentity
类的实例。请注意,只有在使用 Forms 身份验证时,User.Identity
属性才包含 FormsIdentity
类实例。由于 FormsIdentity
类直接公开 FormsAuthenticationTicket
,因此通过反序列化 UserData
属性的内容可以轻松获得先前的用户名。
在取消模拟时重定向用户
正如您在阅读本文时可能注意到的,一个可选的重定向 URL 存储在身份验证 cookie 中。UserImpersonation.Deimpersonate()
方法在模拟恢复后会将用户重定向到该位置。
在我目前正在开发的应用程序中使用此用户模拟功能时,支持用户可以通过单击放置在描述相关客户端用户详细信息的页面上的按钮来模拟某个客户端用户。我希望整个用户模拟体验就像开始一个新的会话一样,并通过到达会话开始时的点来干净地结束会话。
性能
我怀疑性能几乎不会受到使用 UserImpersonation
类的影响。是的,先前的用户和重定向 URL 包含在身份验证 cookie 中,但我很难相信那额外的 100 字节会以任何方式影响您的性能。
在考虑性能极端情况时:我怀疑此解决方案比使用会话存储先前用户名和重定向 URL 的效率要高得多。假设您的应用程序运行在 Web 场中,获取会话很可能需要与数据库服务器进行一次额外的往返,耗时 20 到 100 毫秒。虽然我没有测试过,但我预计解密身份验证 cookie 甚至不需要一毫秒。
结论
我认为我在实现 ASP.NET 用户模拟方面做得相当好。令我惊讶的是,我无法找到任何其他描述本文所述功能的示例,因为这类功能很可能存在于大多数企业级应用程序中,即使整个用户模拟技巧并不是最难实现的事情。
通过这种方式,我想为整个开源社区贡献一些东西,开源社区基本上帮助我从学习编程开始,到现在解决我作为专业程序员遇到的许多问题。一如既往,欢迎评论,我是否遗漏了什么,东西是否不起作用,有没有更好的解决方案?请发表评论!
链接和参考
历史
- 2009/07/18 - 版本 1.0 - 初始发布。