/*十有三博客*/
  • 首页
  • 关于本站
  • 网站地图
  • RSS订阅

AntiForgeryToken生成过程解析-ASP.NET MVC防伪标记源码学习[上]

2017-08-18 十有三 0 浏览:6538 .NET技术 ASP.NET MVC

之前开发某个ASP.NET MVC项目的时候遇到了一个和防伪标记有关的问题,结果不知不觉深入到了源码的研究。本篇主要从AntiForgeryToken(防伪标记/令牌)的生成过程入手,搭配mono的ASP.NET源码进行分析。

PS:之所以使用mono的源码,主要是因为微软官方的源码项目之前是放在codeplex的,而写这篇文章的时候微软正将项目迁移到GitHub上,所以为了保证文章中源码地址的时效性,就先用mono的项目源码。

而且现在mono整合了很多.NET的源码(毕竟微软开源了.NET源码并做了相关授权给mono),如果仔细查阅GitHub上mono的ASP.NET源码,就会发现代码顶部基本都会有微软的版权声明,我也有稍微对照过几个类的源码,发现都是差不多的,所以用mono的源码来研究是可以放心的。

先大概说下令牌生成的过程,首先要明白防伪令牌会保存在两个地方,一个是FormToken(表单令牌),一个是CookieToken(Cookie令牌),最后的安全校验就是对这两个令牌的值进行对比(详细的验证逻辑会在下一篇文章说明)。FormToken和CookieToken这两个令牌的值都是一串字符串,并且完全不一样,它们所保存的字符串其实都是对AntiForgeryToken类进行序列化后的产物。也就是说防伪标记的生成其实就是获取对应的AntiForgeryToken对象后,并对其进行序列化的过程!

可以先看下AntiForgeryToken类的源码,这里列出几个关键属性并加以说明:

  1. SecurityToken:安全令牌,BinaryBlob类型,从字面上就可以理解这是核心所在,保存在表单和Cookie的令牌最后在校验的时候这个值必须一致!此外SecurityToken值如果为null会自动生成。
  2. IsSessionToken:是否会话令牌,true为Cookie令牌,false为表单令牌
  3. ClaimUid:claims-based认证的用户ID,这个我不太理解,猜测应该和身份授权有关系。FormToken会设置这个属性。
  4. Username:用户名,如果有通过身份授权,令牌生成的过程中会带入当前用户名称。FormToken会设置这个属性,CookieToken默认不设置。
  5. AdditionalData:附加数据,个人认为是类似验证码噪点的东西,让表单令牌序列化后和Cookie令牌产生差异,只有表单令牌会设置这个属性!

从上面的属性差异来看,也就能理解为什么最终保存在两个地方的令牌字符串会不一样,毕竟有几个属性只针对表单令牌,特别是保存在Cookie的值最终还会被加密过一次。当然这几个属性最后在验证的时候都会进行对比,不过这里不再多加说明,下一篇文章会具体说明。下面开始对代码进行详细的追踪和分析。

一般在ASP.NET MVC项目中只要在表单里放置@Html.AntiForgeryToken()这段代码就可以生成防伪标记。那我们就从这个方法作为切入点,逐步进行分析。先看该方法的源代码:

    [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
    public MvcHtmlString AntiForgeryToken()
    {
        return new MvcHtmlString(AntiForgery.GetHtml().ToString());
    }

可以看到@HtmlHelper.AntiForgeryToken()方法内部其实只调用了System.Web.Helpers.AntiForgery类的GetHtml方法,最终返回一个MvcHtmlString对象:其实就是一段隐藏字段的HTML代码,值保存的是表单令牌(FormToken)。其他操作则在内部进行处理,所以我们也需要继续跟踪AntiForgery类的源码:

    /// <summary>
    /// Generates an anti-forgery token for this request. This token can
    /// be validated by calling the Validate() method.
    /// </summary>
    /// <returns>An HTML string corresponding to an &lt;input type="hidden"&gt;
    /// element. This element should be put inside a &lt;form&gt;.</returns>
    /// <remarks>
    /// This method has a side effect: it may set a response cookie.
    /// </remarks>
    public static HtmlString GetHtml()
    {
        if (HttpContext.Current == null)
        {
            throw new ArgumentException(WebPageResources.HttpContextUnavailable);
        }
        TagBuilder retVal = _worker.GetFormInputElement(new HttpContextWrapper(HttpContext.Current));
        return retVal.ToHtmlString(TagRenderMode.SelfClosing);
    }

AntiForgery.GetHtml()里面依旧没什么干货,关键在于调用_worker这个对象的方法获取一个TagBuilder对象,TagBuilder类的主要功能就是用于创建HTML元素和属性,也就是对HTML元素进行构建,后面通过ToHtmlString()方法转化成HTML字符串并返回一个HtmlString对象。

PS:这里额外说下,MvcHtmlString类是继承于HtmlString,关于这两个类的如果不太熟悉的朋友可以参考这篇文章:ASP.NET MVC中MvcHtmlString类的两个疑问:是什么以及怎么使用?

_worker是AntiForgeryWorker类的实例化对象。需要注意的是,在上面的AntiForgery类中有个生成此类的方法CreateSingletonAntiForgeryWorker,里面初始化了AntiForgeryWorker类需要依赖的其他模块,从它的构造函数中可以看出,感兴趣的话可以研究研究。

继续下一步追踪,查看AntiForgeryWorker.GetFormInputElement方法究竟做了什么工作。从源码中可以看出,AntiForgeryWorker提供的功能是十分关键的,除了生成令牌外,相关验证逻辑也在此类里面。这里会逐步分析GetFormInputElement方法里的逻辑(注意代码里的注释说明):

    // [ ENTRY POINT ]
    // Generates an anti-XSRF token pair for the current user. The return
    // value is the hidden input form element that should be rendered in
    // the <form>. This method has a side effect: it may set a response
    // cookie.
    public TagBuilder GetFormInputElement(HttpContextBase httpContext)
    {
        //检测相关SSL设置,如果设置防伪标记的验证操作必须使用SSL,但是网站收到的请求不是HTTPS的安全连接则抛出异常。
        CheckSSLConfig(httpContext);

        //获取当前(旧)的Cookie令牌,如果此令牌有效后面不会再生成新的Cookie令牌
        AntiForgeryToken oldCookieToken = GetCookieTokenNoThrow(httpContext);

        AntiForgeryToken newCookieToken, formToken;

        //获取表单和Cookie对应的AntiForgeryToken对象
        GetTokens(httpContext, oldCookieToken, out newCookieToken, out formToken);
        if (newCookieToken != null)
        {
            //保存新生成的Cookie令牌,里面会对其序列化
            _tokenStore.SaveCookieToken(httpContext, newCookieToken);
        }
        // TagBuilder类最终构建的html代码格式如下:
        // <input type="hidden" name="__AntiForgeryToken" value="..." />
        TagBuilder retVal = new TagBuilder("input");
        retVal.Attributes["type"] = "hidden";
        retVal.Attributes["name"] = _config.FormFieldName;
        //这里表单令牌被序列化了
        retVal.Attributes["value"] = _serializer.Serialize(formToken);        
        return retVal;
    }

看完了上面的代码,要特别注意Cookie令牌的生成,如果之前已经生成过或存在CookieToken是不会再重新生成,而是直接使用之前的,所以如果有跟踪Cookie的,会发现CookieToken不怎么变化并且总是一样。但是表单令牌每获取一次都会重新更新,每次的值都是不一样!另外最后获取的AntiForgeryToken令牌对象都会被进行序列化操作。

继续深入GetTokens方法(要留意区分另外一个同名重载方法),并没有找到防伪标记生成的主要逻辑,方法中主要是通过_validator对象来进行操作,所以只能继续跟踪源码。

找到TokenValidator类,先看生成Cookie令牌的方法:

public AntiForgeryToken GenerateCookieToken()
{
    return new AntiForgeryToken()
    {
        //SecurityToken will be populated automatically.
        IsSessionToken = true
    };
}

极为简单的几行代码,就是直接返回了一个新实例化的AntiForgeryToken类并设置其属性IsSessionToken为true。此外还有一行注释,大概译文如下:安全令牌(SecurityToken属性)会自动填充。关于SecurityToken这个属性,开头就有提到了,我自己理解成安全令牌,就是唯一的密钥之类的。

接下来看下生成表单令牌的方法:

public AntiForgeryToken GenerateFormToken(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken)
{
    Contract.Assert(IsCookieTokenValid(cookieToken));

    AntiForgeryToken formToken = new AntiForgeryToken()
    {
        SecurityToken = cookieToken.SecurityToken,
        IsSessionToken = false
    };

    bool requireAuthenticatedUserHeuristicChecks = false;
    // populate Username and ClaimUid
    if (identity != null && identity.IsAuthenticated)
    {
        if (!_config.SuppressIdentityHeuristicChecks)
        {
            // If the user is authenticated and heuristic checks are not suppressed,
            // then Username, ClaimUid, or AdditionalData must be set.
            requireAuthenticatedUserHeuristicChecks = true;
        }

        formToken.ClaimUid = _claimUidExtractor.ExtractClaimUid(identity);
        if (formToken.ClaimUid == null)
        {
            formToken.Username = identity.Name;
        }
    }

    // populate AdditionalData
    if (_config.AdditionalDataProvider != null)
    {
        formToken.AdditionalData = _config.AdditionalDataProvider.GetAdditionalData(httpContext);
    }

    if (requireAuthenticatedUserHeuristicChecks&& String.IsNullOrEmpty(formToken.Username)&& formToken.ClaimUid == null&& String.IsNullOrEmpty(formToken.AdditionalData))
    {
        // Application says user is authenticated, but we have no identifier for the user.
        throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
        WebPageResources.TokenValidator_AuthenticatedUserWithoutUsername, identity.GetType()));
    }

    return formToken;
}

可以看出表单令牌的生成过程比Cookie令牌稍微复杂了点,但是最终也是返回一个AntiForgeryToken对象,区别在于多设置了几个属性(比如与身份授权有关的ClaimUid、Username等),此外要注意表单和Cookie的安全令牌值是一样的,主要体现在这段代码:SecurityToken = cookieToken.SecurityToken。

大致的流程就是这样了,其实还有一些需要详细探究的地方我是没在继续深入了,像AdditionalData和SecurityToken这两个属性是如何生成的?总之了解了个大概的生成过程,也算是涨了点知识吧。


版权声明:本文由十有三创作,采用知识共享许可协议:署名-相同方式共享 4.0 国际(CC BY-SA 4.0)。欢迎转载本文,转载请务必署名-保留作者名称及出处:https://shiyousan.com/post/636384960515312875。


  • 上一篇: ASP.NET MVC中MvcHtmlString类的两个疑问:是什么以及怎么使用?
  • 下一篇: ValidateAntiForgeryTokenAttribute的验证逻辑-ASP.NET MVC防伪标记源码学习[下]

相关文章
  • ASP.NET MVC出现XML5632仅允许有一个根元素
  • ASP.NET MVC UrlParameter.Optional字段用法和意思
  • 托管调试助手 "DateTimeInvalidLocalFormat":“正在将 UTC DateTime 转换为某种文本
  • 个人对C#控制台应用程序在运用方面的总结
  • ASP.NET MVC 使用视图引擎实现页面静态化
  • 翻译:Changes to the language setting will not take effect until the environment is restarted.
发表评论
记住昵称

文章分类

.NET技术 123 数据库 24 Web前端 21 网站建设运维 37 操作系统与应用 66 程序猿日常 11 开发工具 12 其他随笔 13

文章标签

ASP.NET ASP.NET MVC C# CSS HTML IIS Javascript Linux MongoDB MySql SQL SQL Server Visual Studio Windows系统 版本控制系统 插件工具 服务器 搞笑娱乐 好文分享 软件应用 生活知识 手机问题 随笔 网络知识 网站设计优化 网站维护 养生保健 异常处理 硬件设备 游戏攻略

热门文章

  • IIS8如何安装和使用URL重写工具-URL Rewrite
  • 林蛋大与楚中天,朱肚皮与朱月坡
  • 解决IE11安装升级失败和在安装前需要更新的问题
  • VS重构重命名的快捷键
  • Windows Server 2012无法安装 .NET3.5-安装角色或功能失败,找不到源文件

推荐文章

  • 使用WEB 平台安装BlogEngine.NET 3.2 后出现HTTP Error 500.19错误
  • ASP.NET MVC出现XML5632仅允许有一个根元素
  • 服务维护笔记:Windows Server 2012 R2 KB3000850补丁更新失败
  • 解决服务器80端口监听异常导致无法打开和访问网站的问题
  • 解决向日葵远程控制软件Shift无法多选文本内容和代码的问题

最新评论

  • 可乐可乐,谢谢谢谢谢*10086
  • 按上边的2个步骤,没有解决问题。 最后是右键
  • 不错
  • 专门过来看的,讲的很好,学到了很多。
  • 下面的隐藏按钮勾上 就不会出现了

友情链接

  • Passingwind的博客
  • 坤哥网
  • 码友网
  • 王政乔|中国

知识共享许可协议 CC BY-SA 4.0本站作品采用知识共享许可协议:署名-相同方式共享 4.0 国际(CC BY-SA 4.0)。
闽ICP备15003702号