在ASP.NET当中,如果遇到用户已经登录,但是获取不到用户名(User.Identity.Name=""
),并且User.Identity.IsAuthenticated
的值依然为false
的情况,或者调用FormsAuthentication.SignOut()
方法注销用户账户,但是获取User.Identity.IsAuthenticated
后得到的值还是为true
。只要是遇到类似这两种无法实时获取用户身份信息的情况,就要注意当前用户的身份信息是否还没有进行创建/更新,是否没有进行重定向重新触发身份验证事件?这个问题主要和ASP.NET的身份验证机制有关!
本文中所使用的身份验证系统默认为表单/Forms验证,默认支持Cookie,默认使用的框架是ASP.NET MVC,所以如果有使用Web Forms的朋友就要留意少许的差异,文章中的相关例子和内容都是以此为准。
本文中涉及到了几个比较重要的知识点,建议阅读本文前能先进行了解:
- ASP.NET中HttpApplication的管道事件
- ASP.NET的Forms验证/表单验证
文章目录
这里直接引用书籍《ASP.NET 4 高级程序设计》对表单验证的描述:
表单验证是一个基于票据(ticket-based)[也称为基于令牌(token-based)]的系统。这意味着当用户登录系统以后,他们得到一个包含基于用户信息的票据(ticket)。这些信息被存放在加密过的cookie里面,这些cookie和响应绑定在一起,因此每一次后续请求都会被自动提交到服务器。
当用户请求匿名用户无法访问的ASP.NET页面时,ASP.NET运行时验证这个表单验证票据是否有效。如果无效,ASP.NET自动将用户转到登录页面。这时就该由你来操作了。你必须创建这个登录页面并且验证由登录页面提交的凭证。如果用户验证成功,你只需要告诉ASP.NET架构验证成功(通过调用
FormsAuthentication
类的一个方法),运行库会自动设置验证cookie(实际上包含了票据)并将用户转到原先请求的页面。通过这个请求,运行库检测到验证cookie中包含一个有效票据,然后赋给用户对这个页面的访问权限。你可以在图20-1中看到这一流程。
在ASP.NET中,用户的身份验证阶段主要发生在HttpApplication.AuthenticateRequest
事件中,在此事件中ASP.NET会构建(创建/更新)安全上下文对象(继承IPrincipal
接口)和身份标识对象(继承IIdentity
接口)!。
也就是说只有在当前发送的请求经过身份验证的阶段后,或者说AuthenticateRequest
事件开始后,我们才能通过HttpContext.User.Identity
获取到具体的身份标识信息,包括用户名(Name),是否已经登录(IsAuthenticated)等属性值。PS:如果用户未登录也会生成一个默认的身份标识,通常称为匿名用户,页面如果不允许匿名用户访问就要跳转到登录页面
PS:
HttpContext.User
属性获取到的其实就是一个继承IPrincipal
接口的安全上下文对象!而IPrincipal
对象里包含了继承IIdentity
接口的身份标识对象。 简单来列个等式(包含一些名词的别称,毕竟看到了很多种翻译):
- 安全上下文=安全信息=
HttpContext.User
=IPrincipal
- 身份标识=用户标识=
HttpContext.User.Identity
=Identity
- 安全上下文=身份标识+角色信息+其他数据。
部分文档截图:
另外,由于本文中使用的是表单验证,所以用户登录后身份标识是一个
FormsIdentity
类。
两个需要注意的地方:
BeginRequest
事件里是完全获取不到用户的身份信息的,毕竟那个时候User.Identity
都还没有进行构建。AuthenticateRequest
)过后,HttpContext.User.Identity
对象的属性值基本上不会进行改变。除非通过代码重新设置HttpContext.User
这个安全上下文对象。到了这里,如果对Forms验证和管道事件这两个知识点比较熟悉,大概就能明白为什么User.Identity.IsAuthenticated
的值总是与预期的不一样了,下面会根据具体情况进行分析说明。
之所以会出现这个问题,主要原因在于SignOut()
方法只是让包含用户信息票据的Cookie过期,然后等待后续的请求将其发送,这样才能重新走验证流程。这里并没有马上改变当前用户的身份标识信息,也就是说HttpContext.User.Identity
对象是没有任何变化的,IsAuthenticated
的值自然就保持之前的状态!在一般的账户注销流程里,注销操作的后续都是紧跟着重定向操作,这样后续发起的新请求才能重新进行身份验证。
这里注意联系上一小节的内容,用户的身份验证阶段发生在
AuthenticateRequest
事件中,只有此时ASP.NET才会自动对用户的身份信息进行创建!
上面说的还是Forms验证的知识,MSDN文档里可以看这段关键的描述:"Calling the SignOut method only removes the forms authentication cookie." ,大意就是:调用 SignOut 方法只是删除表单身份验证的cookie。
这里再以一段代码作为示例,假设在ASP.NET MVC中某个Controller的Action(操作方法)如下:
[HttpPost]
public ActionResult SignOut()
{
FormsAuthentication.SignOut();
//即使上面调用了SignOut()方法,这里IsAuthenticated的值还是为true
return View();
}
此时响应的视图界面中,如果获取User.Identity.IsAuthenticated
属性会发现它的值依然为true
,毕竟调用FormsAuthentication.SignOut()
后,终究只是把包含过期的身份验证票据Cookie添加到Http响应里,并把它发送到客户端浏览器中,后续并没有任何重新更新用户身份标识的操作和事件发生。
第一种是在调用了SignOut()
方法后重定向到其他页面,这样在重定向后发起的新请求会重新触发管道事件中的身份验证事件,从而重新构建一个默认未登录的用户身份标识信息,俗称匿名用户,代码如下:
[HttpPost]
public ActionResult SignOut()
{
FormsAuthentication.SignOut();
//方法一
return Redirect("/Login");
//方法二,需要在配置文件设置默认登录页面
//FormsAuthentication.RedirectToLoginPage();
}
第二种方法就是通过代码重新设置HttpContext.User
对象,传递一个空白的用户名从而手动删除掉身份标识,代码如下:
[HttpPost]
public ActionResult SignOut()
{
FormsAuthentication.SignOut();
HttpContext.User = new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity(string.Empty), null);
return View();
}
上面代码中安全上下文对象更新后HttpContext.User.Identity.IsAuthenticated
的值和Request.IsAuthenticated
都变成了false
。
这里有一个容易产生错觉的盲区,就是为什么不直接设置Identity
属性,甚至直接设置IsAuthenticated
属性,是因为它们都是只读属性(可以在VS中按F12查看相关定义),只能通过覆盖HttpContext.User
改变它们返回的值。很遗憾的是我也不了解返回值的判断标准,毕竟我找不到相关源码,所以也不清楚它的内部实现结构,但是肯定和安全上下文类关联,这部分感兴趣的朋友可以自己研究下。总之我们重新赋值一个未登录的用户,IsAuthenticated
值就会变成false
。
需要要注意的是,如果项目是使用ASP.NET Web Forms框架,这里会和MVC略有点不同,Web Forms中要使用HttpContext.Current.User
才可以获取当前的安全上下文对象,需要再加一个"Current"。这里可以参考stackoverflow的这个问题:Page.User.Identity.IsAuthenticated still true after FormsAuthentication.SignOut()
在用户登录操作中,无论是通过FormsAuthentication.SetAuthCookie
方法或者通过代码将FormsAuthenticationTicket
对象添加到响应的Cookie里都是无法马上获取用户名的,User.Identity.Name
属性的值还是为""
。其实这情况和上面提到的注销问题是差不多的,这两个方法都只是对包含身份验证票据的Cookie进行操作,用户的身份标识都还没有更新。
解决的方法当然还是在代码中手动设置HttpContext.User
或者做个重定向跳转。而且按照一般的用户账户登录流程,在账户登录后要么跳转到原前请求的URL,要么跳转到某个默认页面,所以这问题还算是比较少见的。不过如果是用AJAX登录的话,倒是需要注意下。
此外我在网上也有看到其他的情况,例如进行切换用户,多次登录等操作,在进行第二次登录后用户名依然是第一次登录的用户名!虽然具体细节没有详细去了解,但是我想问题的根源应该是差不多的。
本文所说的问题根源,其实都是围绕着表单验证和管道事件这两个要点来的。在项目中使用Forms验证,都是通过FormsAuthentication
类来控制注销和登录操作,正常流程下要想获取最新的用户信息/身份标识(排除通过代码设置安全上下文的情况),只能重新再走一遍身份验证流程!所以一般情况下都是做重定向操作,这样就能重新触发AuthenticateRequest
事件而构建HttpContext.User
对象。
版权声明:本文由十有三创作,采用知识共享许可协议:署名-相同方式共享 4.0 国际(CC BY-SA 4.0)。欢迎转载本文,转载请务必署名-保留作者名称及出处:https://shiyousan.com/post/636409990481316726。