CSRF在ASP.NET Core中的处理方法详解

前言

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

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

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

那么什么是CSRF呢?

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

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

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

更加详细的内容可以参考维基百科:Cross-site request forgery

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

  • 视图层面
  • 控制器

层面视图层面

用法

@Html.AntiForgeryToken()

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

原理浅析

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

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8FBn4LzSYglJpE6Q0fWvZ8WDMTgwK49lDU1XGuP5-5j4JlSCML_IDOO3XDL5EOyI_mS2Ux7lLSfI7ASQnIIxo2ScEJvnABf9v51TUZl_iM2S63zuiPK4lcXRPa_KUUDbK-LS4HD16pJusFRppj-dEGc" />

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

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

其中重点的方法如下:

public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
 writer.Write("<input name=\"");
 encoder.Encode(writer, _fieldName);
 writer.Write("\" type=\"hidden\" value=\"");
 encoder.Encode(writer, _requestToken);
 writer.Write("\" />");
}

相当的清晰明了!

控制器层面

用法

[ValidateAntiForgeryToken]
[AutoValidateAntiforgeryToken]
[IgnoreAntiforgeryToken]

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

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

原理浅析

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

过滤器实现:ValidateAntiforgeryTokenAuthorizationFilter和AutoValidateAntiforgeryTokenAuthorizationFilter

其中 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) && ShouldValidate(context))
 {
 try
 {
 await _antiforgery.ValidateRequestAsync(context.HttpContext);
 }
 catch (AntiforgeryValidationException exception)
 {
 _logger.AntiforgeryTokenInvalid(exception.Message, exception);
 context.Result = new BadRequestResult();
 }
 }
 }
}

完整实现可参见github源码:ValidateAntiforgeryTokenAuthorizationFilter.cs

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

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

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

 services.TryAddSingleton<IAntiforgery, DefaultAntiforgery>();

其中还涉及到了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 (!cookieToken.IsCookieToken || requestToken.IsCookieToken)
 {
 message = Resources.AntiforgeryToken_TokensSwapped;
 return false;
 }

 // Are the security tokens embedded in each incoming token identical?
 if (!object.Equals(cookieToken.SecurityToken, requestToken.SecurityToken))
 {
 message = Resources.AntiforgeryToken_SecurityTokenMismatch;
 return false;
 }

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

 var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User);
 if (authenticatedIdentity != null)
 {
 currentClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User));
 if (currentClaimUid == 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.OrdinalIgnoreCase;
 if (currentUsername.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
 currentUsername.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
 {
 comparer = StringComparer.Ordinal;
 }

 if (!comparer.Equals(requestToken.Username, currentUsername))
 {
 message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername);
 return false;
 }

 if (!object.Equals(requestToken.ClaimUid, currentClaimUid))
 {
 message = Resources.AntiforgeryToken_ClaimUidMismatch;
 return false;
 }

 // Is the AdditionalData valid?
 if (_additionalDataProvider != null &&
 !_additionalDataProvider.ValidateAdditionalData(httpContext, requestToken.AdditionalData))
 {
 message = Resources.AntiforgeryToken_AdditionalDataCheckFailed;
 return false;
 }

 message = null;
 return true;
}

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

如何使用

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

使用一:常规的Form表单

先在视图添加一个Form表单

<form id="form1" action="/home/antiform" method="post">
 @Html.AntiForgeryToken()
 <p><input type="text" name="message" /></p>
 <p><input type="submit" value="Send by Form" /></p>
</form>

在控制器添加一个Action

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

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

再来看看cookie的相关信息:

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

使用二:Ajax提交

表单:

<form id="form2" action="/home/antiajax" method="post">
 @Html.AntiForgeryToken()
 <p><input type="text" name="message" id="ajaxMsg" /></p>
 <p><input type="button" id="btnAjax" value="Send by Ajax" /></p>
</form>

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错误):

相信大家也都发现了问题的所在了!!隐藏域的相关内容并没有一起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",
 dataType: "html",
 url: '@Url.Action("AntiAjax", "Home")',
 data: { message: $('#ajaxMsg').val() },
 headers:
 {
  "RequestVerificationToken": token
 },
 success: function (result) {
  alert(result);
 },
 error: function (err, 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",
 dataType: "html",
 url: '@Url.Action("AntiAjax", "Home")',
 data: { message: $('#ajaxMsg').val() },
 headers:
 {
 "CUSTOMER-CSRF-HEADER": token //注意header要修改
 },
 success: function (result) {
 alert(result);
 },
 error: function (err, scnd) {
 alert(err.statusText);
 }
});

下面是效果:

Form表单:

Cookie:

本文涉及到的相关项目:

关于CSRF相关的内容

Preventing Cross-Site Request Forgery (XSRF/CSRF) Attacks in ASP.NET Core

浅谈CSRF攻击方式

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 浅谈ASP.NET MVC 防止跨站请求伪造(CSRF)攻击的实现方法

    在HTTP POST请求中,我们多次在View和Controller中看下如下代码: 1.View中调用了Html.AntiForgeryToken(). 2.Controller中的方法添加了[ValidateAntiForgeryToken]注解. 这样看似一对的写法其实是为了避免引入跨站请求伪造(CSRF)攻击. 这种攻击形式大概在2001年才为人们所认知,2006年美国在线影片租赁网站Netflix爆出多个CSRF漏洞,2008年流行的视频网址YouTube受到CSRF攻击,同年墨西哥一

  • CSRF在ASP.NET Core中的处理方法详解

    前言 前几天,有个朋友问我关于AntiForgeryToken问题,由于对这一块的理解也并不深入,所以就去研究了一番,梳理了一下. 在梳理之前,还需要简单了解一下背景知识. AntiForgeryToken 可以说是处理/预防CSRF的一种处理方案. 那么什么是CSRF呢? CSRF(Cross-site request forgery)是跨站请求伪造,也被称为One Click Attack或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用. 简单理解的话

  • ASP.NET Core 中的Main方法详解

    在 ASP.NET Core 项目中,我们有一个名为Program.cs的文件.在这个文件中,我们有一个public static void Main()方法 . public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[]

  • ASP.NET Core中如何实现重定向详解

    前言 ASP.NET Core 是一个跨平台,开源的,轻量级的,模块化的,用于构建高性能的 web 开发框架, ASP.NET Core MVC 内置了多种方式将一个 request 请求跳转到指定的url,这篇文章我们就来讨论如何去实现. 理解 RedirectActionResult ASP.NET Core MVC 中内置了几种 Redirect,比如说:RedirectResult, RedirectToActionResult, RedirectToRouteResult 和 Loca

  • ASP.NET Core Middleware的实现方法详解

    概念 ASP.NET Core Middleware是在应用程序处理管道pipeline中用于处理请求和操作响应的组件. 每个组件: 在pipeline中判断是否将请求传递给下一个组件 在处理管道的下个组件执行之前和之后执行一些工作, HttpContxt对象能跨域请求.响应的执行周期 特性和行为 ASP.NET Core处理管道由一系列请求委托组成,一环接一环的被调用, 下面给出自己绘制的Middleware pipeline流程图: 从上图可以看出,请求自进入处理管道,经历了四个中间件,每个

  • [译]ASP.NET Core 2.0 路由引擎详解

    本文介绍了ASP.NET Core 2.0 路由引擎详解,分享给大家,具体如下: 问题 ASP.NET Core 2.0的路由引擎是如何工作的? 答案 创建一个空项目,为Startup类添加MVC服务和请求中间件: public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvir

  • Asp.net MVC scheduler的实现方法详解

    Asp.net MVC scheduler的实现方法详解 本例使用了fullcalendar js : https://fullcalendar.io/ 1. view : @{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; } @section PageContent{ <style> .modal-backdrop { z-index: 9; } </sty

  • Angular中的$watch方法详解

    在$apply方法中提到过脏检查,首先apply方法会触发evel方法,当evel方法解析成功后,会去触发digest方法,digest方法会触发watch方法. (1)$watch简介 在digest执行时,如果watch观察的的value与上一次执行时不一样时,就会被触发. AngularJS内部的watch实现了页面随model的及时更新. $watch方法在用的时候主要是手动的监听一个对象,但对象发生变化时触发某个事件. (2)watch方法用法 $watch(watchFn,watch

  • angularjs中的$eval方法详解

    在controller中定义了一个变量 $scope.a_1 = "abc"; 想在view里面动态输出,因为这个数字是动态的,这么输出肯定是不行的{{'a_' + '1'}},因为输出来的是a_1这个字符串,而不是a_1这个变量的值 想输出a_1这个变量的值,可以使用$eval方法:{{$eval('a_' + '1')}} $eval是作为scope的方法来使用的,在controller中使用的话,是这么使用:$scope.$eval() 以上这篇angularjs中的$eval方

  • Python中格式化format()方法详解

     Python中格式化format()方法详解 Python中格式化输出字符串使用format()函数, 字符串即类, 可以使用方法; Python是完全面向对象的语言, 任何东西都是对象; 字符串的参数使用{NUM}进行表示,0, 表示第一个参数,1, 表示第二个参数, 以后顺次递加; 使用":", 指定代表元素需要的操作, 如":.3"小数点三位, ":8"占8个字符空间等; 还可以添加特定的字母, 如: 'b' - 二进制. 将数字以2为基

  • Android中SQLite 使用方法详解

    Android中SQLite 使用方法详解 现在的主流移动设备像android.iPhone等都使用SQLite作为复杂数据的存储引擎,在我们为移动设备开发应用程序时,也许就要使用到SQLite来存储我们大量的数据,所以我们就需要掌握移动设备上的SQLite开发技巧.对于Android平台来说,系统内置了丰富的API来供开发人员操作SQLite,我们可以轻松的完成对数据的存取. 下面就向大家介绍一下SQLite常用的操作方法,为了方便,我将代码写在了Activity的onCreate中: @Ov

随机推荐