ValidateAntiForgeryTokenAttribute的验证逻辑-ASP.NET MVC防伪标记源码学习[下]

上篇文章主要是从源码入手,解析并了解AntiForgeryToken防伪标记的生成过程。这篇文章还是会结合源码,对ValidateAntiForgeryToken属性的验证逻辑进行分析和说明,搞懂防伪标记的验证逻辑到底是怎么一回事,也能对ASP.NET MVCV的防伪标记有着更加深入的理解。

本文中很多知识点在上一篇文章AntiForgeryToken生成过程解析里都有详细的说明,所以重复的知识点会简略概括,注意先阅读上一篇文章。

先简述下整个验证逻辑的流程,首先要明白防伪标记是分别保存在表单和Cookie里,也分别称作表单令牌(FormToken)和Cookie令牌(CookieToken),当我们启用防伪标记功能后(也就是在Action操作方法上添加ValidateAntiForgeryToken特性标记),用户在对应的表单进行提交操作时就会触发验证机制,验证的第一步是先获取对应的CookieTokenFormToken,接下来才会进行校验。

获取到的两个令牌的值都是经过加密和序列化的,所以要先进行反序列化。也就是说在进行反序列化之前两个令牌的值都是一串加密的字符串,经过反序列化后会得到一个AntiForgeryToken对象(防伪令牌对象),AntiForgeryToken类包含了许多关键属性,例如安全令牌(SecurityToken),身份授权等信息(上篇文章有对此类进行说明)。

校验过程其实就是对两个AntiForgeryToken对象中的属性进行对比判断,只要相关的属性值不匹配或者属于无效数据,都会抛出HttpAntiForgeryException异常,表示这是非法的请求或者CSRF攻击!

这里罗列几种验证不通过的情况:

  1. 在开启了防伪标记功能后,FormTokenCookieToken其中一个值为空白!也就是说只要缺少表单令牌或者Cookie令牌,验证一定是失败的。
  2. 防伪令牌中安全令牌的值不相等!
  3. 防伪令牌中的相关授权信息不一致!例如出现认证授权的用户名不一致!
  4. 防伪令牌自身的标记错误,IsSessionTokentrue表示Cookie令牌,否则为表单令牌,如果这个属性设置错误,验证失败!
  5. 其他的还会判断AdditionalDataProvider这个属性值是否有效,具体判断逻辑就不清楚了。

通俗点说ValidateAntiForgery的验证其实就像对口令,口令对上了说明就是自己人,类似 天王盖地虎,宝塔镇河妖 这种接头暗号,只是防伪标记做的更加安全和隐秘,核心的安全令牌(SecurityToken)完全是随机生成,攻击者根本不知道正确的接头暗号是什么!另外在表单里(视图界面中)生成令牌是使用@HtmlHelper.AntiForgeryToken()方法,此方法会同时生成表单和cookie令牌,生成的逻辑基本上是一样,都是先生成一个AntiForgeryToken对象,然后设置对应属性,最后进行序列化。

接下来开始从源码入手,一步步深入分析。首先看ValidateAntiForgeryTokenAttribute的源码:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
    private string _salt;

    public ValidateAntiForgeryTokenAttribute() : this(AntiForgery.Validate)
    { }

    internal ValidateAntiForgeryTokenAttribute(Action validateAction)
    {
        Debug.Assert(validateAction != null);
        ValidateAction = validateAction;
    }

    internal Action ValidateAction { get; private set; }

    public void OnAuthorization(AuthorizationContext filterContext)
    {

        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }
        ValidateAction();
    }
}

可以看到验证方法其实只是调用了AntiForgery类的Validate方法,继续查看对应的源码:

/// <summary>
/// Validates an anti-forgery token that was supplied for this request.
/// The anti-forgery token may be generated by calling GetHtml().
/// </summary>
/// <remarks>
/// Throws an HttpAntiForgeryException if validation fails.
/// </remarks>
public static void Validate()
{
    if (HttpContext.Current == null)
    {
        throw new ArgumentException(WebPageResources.HttpContextUnavailable);
    }
    _worker.Validate(new HttpContextWrapper(HttpContext.Current));
}

上面的代码中依然没有什么需要注意的地方,_workerAntiForgeryWorker类的实例化对象(上篇文章也有提及),所以接下来要阅读AntiForgeryWorker类的Validate方法源码,

// [ ENTRY POINT ]
// Given an HttpContext, validates that the anti-XSRF tokens contained
// in the cookies & form are OK for this request.
public void Validate(HttpContextBase httpContext)
{
    CheckSSLConfig(httpContext);
    // Extract cookie & form tokens
    AntiForgeryToken cookieToken = _tokenStore.GetCookieToken(httpContext);
    AntiForgeryToken formToken = _tokenStore.GetFormToken(httpContext);
    // Validate
    _validator.ValidateTokens(httpContext, ExtractIdentity(httpContext), cookieToken, formToken);
}

这里可以看到较为关键的一个步骤,就是获取对应的表单令牌和Cookie令牌并将它们进行反序列化,最终获取到两个AntiForgeryToken对象,至于反序列化的相关逻辑,感兴趣的朋友可以自己研究下,主要代码在AntiForgeryTokenStore类里。

最终获取到的防伪令牌对象会传入并调用TokenValidator类的ValidateTokens方法进行验证,到了这里我们源码追踪的任务就结束了,此方法就是令牌验证最为关键的逻辑代码:

public void ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken sessionToken, AntiForgeryToken fieldToken)
{
    // Were the tokens even present at all?
    if (sessionToken == null)
    {
        throw HttpAntiForgeryException.CreateCookieMissingException(_config.CookieName);
    }

    if (fieldToken == null)
    {
        throw HttpAntiForgeryException.CreateFormFieldMissingException(_config.FormFieldName);
    }

    // Do the tokens have the correct format?
    if (!sessionToken.IsSessionToken || fieldToken.IsSessionToken)
    {
        throw HttpAntiForgeryException.CreateTokensSwappedException(_config.CookieName, _config.FormFieldName);
    }

    // Are the security tokens embedded in each incoming token identical?
    if (!Equals(sessionToken.SecurityToken, fieldToken.SecurityToken))
    {
        throw HttpAntiForgeryException.CreateSecurityTokenMismatchException();
    }

    // Is the incoming token meant for the current user?
    string currentUsername = String.Empty;
    BinaryBlob currentClaimUid = null;

    if (identity != null && identity.IsAuthenticated)
    {
        currentClaimUid = _claimUidExtractor.ExtractClaimUid(identity);
        if (currentClaimUid == null)
        {
            currentUsername = identity.Name ?? String.Empty;
        }
    }

    // OpenID and other similar authentication schemes use URIs for the username.
    // These should be treated as case-sensitive.
    bool useCaseSensitiveUsernameComparison = currentUsername.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || currentUsername.StartsWith("https://", StringComparison.OrdinalIgnoreCase);

    if (!String.Equals(fieldToken.Username, currentUsername, (useCaseSensitiveUsernameComparison) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
    {
        throw HttpAntiForgeryException.CreateUsernameMismatchException(fieldToken.Username, currentUsername);
    }

    if (!Equals(fieldToken.ClaimUid, currentClaimUid))
    {
        throw HttpAntiForgeryException.CreateClaimUidMismatchException();
    }

    // Is the AdditionalData valid?
    if (_config.AdditionalDataProvider != null && !_config.AdditionalDataProvider.ValidateAdditionalData(httpContext, fieldToken.AdditionalData))
    {
        throw HttpAntiForgeryException.CreateAdditionalDataCheckFailedException();
    }
}

这里代码的逻辑是十分清晰明了的,特别是源码中的注释要注意查看,比起我自己表述的更加容易理解。文章开头的防伪标记验证逻辑的简述其实也是根据这段代码里的逻辑来的。总之只要两个令牌的对象有某个属性值不匹配或者是无效数据,都会抛出HttpAntiForgeryException异常,最终结果自然就是验证失败。




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

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

相关推荐

发表回复

登录后才能评论