微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

初探CSRF在ASP.NET Core中的处理方式

<h2 id="前言">前言

前几天,有个朋友问我关于AntiForgeryToken问题,由于对这一块的理解也并不深入,所以就去研究了一番,梳理了一下。

在梳理之前,还需要简单了解一下背景知识。

AntiForgeryToken 可以说是处理/预防CSRF的一种处理方案。

那么什么是CSRF呢?

CSRF(Cross-site request forgery)是跨站请求伪造,也被称为One Click Attack或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。

简单理解的话就是:有人盗用了你的身份,并且用你的名义发送恶意请求

最近几年,CSRF处于不温不火的地位,但是还是要对这个小心防范!

更加详细的内容可以参考维基百科:

下面从使用的角度来分析一下CSRF在 ASP.NET Core中的处理,个人认为主要有下面两大块

  • 视图层面
  • 控制器层面

@Html.AntiForgeryToken()

在视图层面的用法相对比较简单,用的还是HtmlHelper的那一套东西。在Form表单中加上这一句就可以了。

当在表单中添加了上面的代码后,页面会生成一个隐藏域,隐藏域的值是一个生成的token(防伪标识),类似下面的例子

其中的name="__RequestVerificationToken"是定义的一个const变量,value=XXXXX是根据一堆东西进行base64编码,并对base64编码后的内容进行简单处理的结果,具体的实现可以参见

生成上面隐藏域代码在AntiforgeryExtensions这个文件里面,github上的源码文件

其中重点的方法如下:

public void WriteTo(TextWriter writer,HtmlEncoder encoder)
{
    writer.Write("");
}

相当的清晰明了!

[ValidateAntiForgeryToken]
[AutoValidateAntiforgeryToken]
[IgnoreAntiforgeryToken]

这三个都是可以基于类或方法的,所以我们只要在某个控制器或者是在某个Action上面加上这些Attribute就可以了。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,AllowMultiple = false,Inherited = true)]

本质是Filter(过滤器),验证上面隐藏域的value

过滤器实现:ValidateAntiforgeryTokenAuthorizationFilterAutoValidateAntiforgeryTokenAuthorizationFilter

其中 AutoValidateAntiforgeryTokenAuthorizationFilter是继承了ValidateAntiforgeryTokenAuthorizationFilter,只重写了其中的ShouldValidate方法。

下面贴出ValidateAntiforgeryTokenAuthorizationFilter的核心方法:

public class ValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter,IAntiforgeryPolicy
{
    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
    if (IsClosestAntiforgeryPolicy(context.Filters) &amp;&amp; ShouldValidate(context))
    {
        try
        {
            await _antiforgery.ValidateRequestAsync(context.HttpContext);
        }
        catch (AntiforgeryValidationException exception)
        {
            _logger.AntiforgeryTokenInvalid(exception.Message,exception);
            context.Result = new BadRequestResult();
        }
    }
}

}

完整实现可参见github源码:

当然这里的过滤器只是一个入口,相关的验证并不是在这里实现的。而是在Antiforgery这个项目上,其实说这个模块可能会更贴切一些。

由于是面向接口的编程,所以要知道具体的实现,就要找到对应的实现类才可以。

Antiforgery这个项目中,有这样一个扩展方法AntiforgeryServiceCollectionExtensions,里面告诉了我们相对应的实现是DefaultAntiforgery这个类。其实Nancy的源码看多了,看一下类的命名就应该能知道个八九不离十。

  services.TryAddSingleton();

其中还涉及到了IServiceCollection,但这不是本文的重点,所以不会展开讲这个,只是提出它在 .net core中是一个重要的点。

好了,回归正题!要验证是否是合法的请求,自然要先拿到要验证的内容

 var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);

它是从Cookie中拿到一个指定的前缀为.AspNetCore.Antiforgery.的Cookie,并根据这个Cookie进行后面相应的判断。下面是验证的具体实现:

public bool TryValidatetokenSet(
    HttpContext httpContext,AntiforgeryToken cookietoken,AntiforgeryToken requestToken,out string message)
{
    //去掉了部分非空的判断
// Do the tokens have the correct format?
if (!cooki<a href="https://www.jb51.cc/tag/eto/" target="_blank" class="keywords">eto</a>ken.IsCooki<a href="https://www.jb51.cc/tag/eto/" target="_blank" class="keywords">eto</a>ken || requestToken.IsCooki<a href="https://www.jb51.cc/tag/eto/" target="_blank" class="keywords">eto</a>ken)
{
    message = Resources.AntiforgeryToken_TokensSwapped;
    return false;
}

// Are the s<a href="https://www.jb51.cc/tag/ecurity/" target="_blank" class="keywords">ecurity</a> tokens em<a href="https://www.jb51.cc/tag/bed/" target="_blank" class="keywords">bed</a>ded in each incoming token identical?
if (!object.Equals(cooki<a href="https://www.jb51.cc/tag/eto/" target="_blank" class="keywords">eto</a>ken.S<a href="https://www.jb51.cc/tag/ecurity/" target="_blank" class="keywords">ecurity</a>Token,requestToken.S<a href="https://www.jb51.cc/tag/ecurity/" target="_blank" class="keywords">ecurity</a>Token))
{
    message = Resources.AntiforgeryToken_S<a href="https://www.jb51.cc/tag/ecurity/" target="_blank" class="keywords">ecurity</a>TokenMismatch;
    return false;
}

// Is the incoming token meant for the current user?
var currentUsername = string.Empty;
BinaryBlob currentCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>Uid = null;

var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User);
if (authenticatedIdentity != null)
{
    currentCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>Uid = GetCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>UidBlob(_cl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>UidExtractor.ExtractCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>Uid(httpContext.User));
    if (currentCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>Uid == null)
    {
        currentUsername = authenticatedIdentity.Name ?? string.Empty;
    }
}

// OpenID and other similar authentication schemes use URIs for the username.
// These should be treated as case-sensitive.
var comparer = StringComparer.OrdinalIg<a href="https://www.jb51.cc/tag/nor/" target="_blank" class="keywords">nor</a>eCase;
if (currentUsername.StartsWith("http://",StringComparison.OrdinalIg<a href="https://www.jb51.cc/tag/nor/" target="_blank" class="keywords">nor</a>eCase) ||
    currentUsername.StartsWith("https://",StringComparison.OrdinalIg<a href="https://www.jb51.cc/tag/nor/" target="_blank" class="keywords">nor</a>eCase))
{
    comparer = StringComparer.Ordinal;
}

if (!comparer.Equals(requestToken.Username,currentUsername))
{
    message = Resources.Form<a href="https://www.jb51.cc/tag/atan/" target="_blank" class="keywords">atan</a>tiforgeryToken_UsernameMismatch(requestToken.Username,currentUsername);
    return false;
}

if (!object.Equals(requestToken.Cl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>Uid,currentCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>Uid))
{
    message = Resources.AntiforgeryToken_Cl<a href="https://www.jb51.cc/tag/aim/" target="_blank" class="keywords">aim</a>UidMismatch;
    return false;
}

// Is the AdditionalData valid?
if (_additionalDataProvider != null &amp;&amp;
    !_additionalDataProvider.ValidateAdditionalData(httpContext,requestToken.AdditionalData))
{
    message = Resources.AntiforgeryToken_AdditionalDataCheck<a href="https://www.jb51.cc/tag/Failed/" target="_blank" class="keywords">Failed</a>;
    return false;
}

message = null;
return true;

}

注:验证前还有一个反序列化的过程,这个反序列化就是从Cookie中拿到要判断的cookietoken和requesttoken

前面粗略介绍了一下其内部的实现,下面再用个简单的例子来看看具体的使用情况:

先在视图添加一个Form表单

在控制器添加一个Action

[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult AntiForm(string message)
{
    return Content(message);
}

来看看生成的html是不是如我们前面所说,将@Html.AntiForgeryToken()输出一个name为__RequestVerificationToken隐藏域

image

再来看看cookie的相关信息:

image

可以看到,一切都还是按照前面所说的执行。在输入框输入信息并点击按钮也能正常显示我们输入的文字

image

表单:

js:

$(function () {
    $("#btnAjax").on("click",function () {
        $("#form2").submit();                
    });
})

这样子的写法也是和上面的结果是一样的!

怕的是出现下面这样的写法:

$.ajax({
    type: "post",dataType: "html",url: '@Url.Action("AntiAjax","Home")',data: { message: $('#ajaxmsg').val() },success: function (result) {
        alert(result);
    },error: function (err,scnd) {
        alert(err.statusText);
    }
});

这样,正常情况下确实是看不出任何毛病,但是实际确是下面的结果(400错误):

fdb79d1dbbcefb6eb28d2ffb5ab96.png" alt="image">

相信大家也都发现了问题的所在了!!隐藏域的相关内容并没有一起post过去!!

处理方法有两种:

方法一:

在data中加上隐藏域相关的内容,大致如下:

$.ajax({
    //        
    data: { message: $('#ajaxmsg').val(),__RequestVerificationToken: $("input[name='__RequestVerificationToken']").val()}
});

方法二:

在请求中添加一个header

$("#btnAjax").on("click",function () {
    var token = $("input[name='__RequestVerificationToken']").val();
    $.ajax({
        type: "post",headers:
        {
            "RequestVerificationToken": token
        },success: function (result) {
            alert(result);
        },scnd) {
            alert(err.statusText);
        }
    });
});

这样就能处理上面出现的问题了!

自定义相关信息">使用三:自定义相关信息

可能会有不少人觉得,像那个生成隐藏域那个name能不能换成自己的,那个cookie的名字能不能换成自己的〜〜

答案是肯定可以的,下面简单示范一下:

在Startup的ConfigureServices方法中,添加下面的内容即可对认的名称进行相应的修改

services.AddAntiforgery(option =>
{
    option.CookieName = "CUSTOMER-CSRF-COOKIE";
    option.FormFieldName = "CustomerFieldName";
    option.HeaderName = "CUSTOMER-CSRF-HEADER";
});

相应的,ajax请求也要做修改

var token = $("input[name='CustomerFieldName']").val();//隐藏域名称要改
$.ajax({
    type: "post",headers:
    {
        "CUSTOMER-CSRF-HEADER": token //注意header要修改
    },scnd) {
        alert(err.statusText);
    }
});

下面是效果

Form表单:

image

Cookie:

image

原文地址:https://www.jb51.cc/netcore/71322.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐