之前开发某个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类的源码,这里列出几个关键属性并加以说明:
SecurityToken
:安全令牌,BinaryBlob
类型,从字面上就可以理解这是核心所在,保存在表单和Cookie的令牌最后在校验的时候这个值必须一致!此外SecurityToken
值如果为null
会自动生成。IsSessionToken
:是否会话令牌,true为Cookie令牌,false为表单令牌ClaimUid
:claims-based认证的用户ID,这个我不太理解,猜测应该和身份授权有关系。FormToken会设置这个属性。Username
:用户名,如果有通过身份授权,令牌生成的过程中会带入当前用户名称。FormToken会设置这个属性,CookieToken默认不设置。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 <input type="hidden">
/// element. This element should be put inside a <form>.</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
这两个属性是如何生成的?总之了解了个大概的生成过程,也算是涨了点知识吧。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/99049.html