AntiForgeryToken生成过程解析-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类的两个疑问:是什么以及怎么使用?

_workerAntiForgeryWorker类的实例化对象。需要注意的是,在上面的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类并设置其属性IsSessionTokentrue。此外还有一行注释,大概译文如下:安全令牌(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对象,区别在于多设置了几个属性(比如与身份授权有关的ClaimUidUsername等),此外要注意表单和Cookie的安全令牌值是一样的,主要体现在这段代码:SecurityToken = cookieToken.SecurityToken

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




原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/99049.html

(0)
上一篇 2021年8月21日
下一篇 2021年8月21日

相关推荐

发表回复

登录后才能评论