在鼓捣一个项目的时候引发了HttpAntiForgeryException (0x80004005)异常,并提示:提供的防伪标记适用于用户“admin”,但当前用户为“”。从异常信息可以很直观看出问题产生的原因所在,主要在于身份认证和授权的状态发生改变,导致防伪令牌没有更新正确的身份信息从而验证失败。
虽然只是个小问题,但如果仔细深入研究,会发现此异常会出现在多种情况下,所以也有多种解决办法。此外也可以研究下ASP.NET MVC 防伪令牌AntiForgeryToken
的机制,从其源码中探究根源也是收益颇多。
备注:本文不对防伪标记的原理进行详细说明,这部分可以查阅相关书籍。 建议参考mono中ASP.NET的源码。微软官方的源码也是有,只不过在我写这篇文章的时候正准备迁移到github,所以不知道什么时候会失效。
另外我写的这两篇文章也可以参考下:
- AntiForgeryToken生成过程解析-ASP.NET MVC防伪标记源码学习[上]
- ValidateAntiForgeryTokenAttribute的验证逻辑-ASP.NET MVC防伪标记源码学习[下]
本文的情况其实比较少见,因为一般情况下使用防伪标记的POST请求都是需要进行用户认证和授权的,而用户授权验证的优先级比令牌验证的优先级高,所以基本很难产生此异常,只能是在特殊情况下,这里因为刚好碰到就拿来研究下。
先分析下问题的原因,AntiForgeryToken
(防伪标记)机制的本身是为了防御CSRF攻击(跨站请求伪造)。只要是引发了HttpAntiForgeryException
异常,就说明你的防伪标记没有验证通过,表单发送的请求被判定为非法请求!本文的情况明显不是遭到了CSRF攻击(毕竟是自己开发的项目),是属于较为特殊的情况。
根据异常详细信息的描述,可以明确得知防伪标记的用户身份不匹配,这个信息很重要,因为防伪标记在生成的过程中是包含了当前的用户名(Username),如果用户没有登录则默认为String.Empty,也就是""。
总而言之,只要在表单(有使用防伪标记)进行提交的时候,也就是请求还未发送到服务器时,用户授权/身份认证发生了改变,就会导致表单的防伪令牌和cookie的防伪令牌没有办法对应上,造成验证失败,引发HttpAntiForgeryException
异常。
具体的场景有以下几种(这里的场景默认都是使用了防伪标记,并且发送的是POST请求):
1、重复登录导致令牌重复更新
在浏览器中同时打开两个登录页面,其中A页面登录后,B页面又进行登录,不要觉得这操作很傻,但确实存在!如果B页面先刷新再登录,会发现此异常不会被引发,因为防伪令牌更新了!其实Login页面如果没有特殊的业务要求,倒是没必要使用防伪标记,这里只是列举出这种情况。
解决方法可以从登录的业务逻辑入手,无论是前后端都有办法处理,主要思路是在提交登录请求后(Post请求),要判断用户是否已通过认证,已授权就先跳转回Login页面或者指定的某个页面(此处跳转是Get请求)。
补充:上面之所以要跳转回Login页面,是因为一般向Login页面发起的Get请求的处理逻辑都会先判断用户是否登陆过了,已登陆就不会让你重复操作,会进行相应的跳转,代码如下:
[AllowAnonymous]
[HttpGet]
public ActionResult Login(string ReturnUrl)
{
ViewBag.ReturnUrl = ReturnUrl;
if (User.Identity.IsAuthenticated)
{
//防止欺诈跳转(回调地址为空也会判定为false)
if (Url.IsLocalUrl(ReturnUrl))
{
return Redirect(ReturnUrl);
}
else
{
//跳转到默认地址
return Redirect("/Home/Index");
}
}
return View();
}
前端的方法比较简单,通过AJAX处理即可,在发送登录请求前先通过AJAX判断登录状态,已登录则直接跳转。
后台的话可以不使用ValidateAntiForgeryTokenAttribute,在Action操作方法的代码内使用AntiForgery.Validate()方法,如果引发了HttpAntiForgeryException异常则说明防伪标记认证失败,再进行下一步处理,大概代码如下(相当于手动验证了):
[AllowAnonymous]
[HttpPost]
public ActionResult Login(LoginViewModel entity, string ReturnUrl)
{
//由于用户已经登录,可以跳转回Login页面
if (User.Identity.IsAuthenticated)
{
return Redirect("/Admin/Login");
}
try
{
System.Web.Helpers.AntiForgery.Validate();
}
catch (HttpAntiForgeryException hafex)
{
//相关业务逻辑代码
//或者跳转到特定的令牌验证错误页面,并提示友好的操作信息。
}
catch (Exception ex)
{
//省略代码
}
//省略相关业务代码
return View();
}
另外上面的代码也可以自己写个过滤器,可以复用在很多地方,大概代码如下(OnActionExecuting里的代码参考上面的代码):
public class CustomValidateAntiForgeryTokenAttribute : FilterAttribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
//相关业务代码,可参考上面代码
}
}
2、用户账户注销后未更新身份认证或跳转
在用户已经授权登录的情况下,使用FormsAuthentication.SignOut()
方法注销账户,并且通过return View("Login");
返回到登录页面,此时又在页面上进行登录操作就会引发HttpAntiForgeryException
异常。
这种情况最好解决就是不要使用return View
返回页面,而是要做跳转操作,或者自己手动更新身份标记。具体的代码如下(注销的操作方法):
/// <summary>
/// 账号注销
/// </summary>
/// <returns></returns>
public ActionResult AccountSignOut()
{
FormsAuthentication.SignOut();
//方法一
return Redirect("/Admin/Login");
//方法二,需要在配置文件设置默认登录页面
//FormsAuthentication.RedirectToLoginPage();
//return null;
//方法三
//HttpContext.User = new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity(string.Empty), null);
//return View("Login");
}
简单说下原因,当你使用SignOut
方法注销用户账号时,就必须做跳转操作,否则身份认证的状态是不会进行更新的,当然也可以使用上面代码中的方法三强制更新身份认证(注意根据自身情况修改GenericIdentity
方法中的用户名参数)!
这个问题主要和表单验证流程还有管道事件有关系,如果感兴趣可以看这篇文章: 解析ASP.NET中获取不到用户名及注销后User.Identity.IsAuthenticated值依然为true的原因
3、并发的登录请求
在网络不稳定有延迟的情况下,快速的点击两次提交按钮,导致发送了两次登录请求。这种情况只要从前端入手即可,写Js代码实现第一次点击提交就禁用按钮,只有当请求有对应的响应返回才重新开启按钮。
上面的三种情况各有各的解决方法,这里还有两个通用的解决方法,一个就是利用JQuery的ajax进行同步请求从而更新防伪标记,具体可以看这篇文章:ASP.NET MVC 获取及手动更新AntiForgeryToken防伪标记。另外一个则是通过捕捉全局异常,专门对HttpAntiForgeryException
进行处理,比如可以跳转到专门的令牌验证错误页面。
最后贴下异常堆栈跟踪信息,可以根据这里的信息查看对应的源码,就当一个线索吧,方便理解和学习:
源错误:
执行当前 Web 请求期间生成了未经处理的异常。可以使用下面的异常堆栈跟踪信息确定有关异常原因和发生位置的信息。
堆栈跟踪:
[HttpAntiForgeryException (0x80004005): 提供的防伪标记适用于用户“admin”,但当前用户为“”。]
System.Web.Helpers.AntiXsrf.TokenValidator.ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken sessionToken, AntiForgeryToken fieldToken) +586
System.Web.Helpers.AntiXsrf.AntiForgeryWorker.Validate(HttpContextBase httpContext) +71
System.Web.Helpers.AntiForgery.Validate() +92
System.Web.Mvc.ValidateAntiForgeryTokenAttribute.OnAuthorization(AuthorizationContext filterContext) +18
System.Web.Mvc.ControllerActionInvoker.InvokeAuthorizationFilters(ControllerContext controllerContext, IList`1 filters, ActionDescriptor actionDescriptor) +97
System.Web.Mvc.Async.<>c__DisplayClass21.<BeginInvokeAction>b__19(AsyncCallback asyncCallback, Object asyncState) +743
System.Web.Mvc.Async.WrappedAsyncResult`1.CallBeginDelegate(AsyncCallback callback, Object callbackState) +14
System.Web.Mvc.Async.WrappedAsyncResultBase`1.Begin(AsyncCallback callback, Object state, Int32 timeout) +128
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/99051.html