前言

前几天,有个朋友问我关于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中的处理,个人认为主要有下面两大块

  • 视图层面
  • 控制器层面

视图层面

用法

  1. @Html.AntiForgeryToken()

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

原理浅析

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

  1. <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

其中重点的方法如下:

  1. public void WriteTo(TextWriter writer, HtmlEncoder encoder)
  2. {
  3. writer.Write("<input name=\"");
  4. encoder.Encode(writer, _fieldName);
  5. writer.Write("\" type=\"hidden\" value=\"");
  6. encoder.Encode(writer, _requestToken);
  7. writer.Write("\" />");
  8. }

相当的清晰明了!

控制器层面

用法

  1. [ValidateAntiForgeryToken]
  2. [AutoValidateAntiforgeryToken]
  3. [IgnoreAntiforgeryToken]

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

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

原理浅析

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

过滤器实现:ValidateAntiforgeryTokenAuthorizationFilterAutoValidateAntiforgeryTokenAuthorizationFilter

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

下面贴出ValidateAntiforgeryTokenAuthorizationFilter的核心方法:

  1. public class ValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
  2. {
  3. public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
  4. {
  5. if (context == null)
  6. {
  7. throw new ArgumentNullException(nameof(context));
  8. }
  9. if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context))
  10. {
  11. try
  12. {
  13. await _antiforgery.ValidateRequestAsync(context.HttpContext);
  14. }
  15. catch (AntiforgeryValidationException exception)
  16. {
  17. _logger.AntiforgeryTokenInvalid(exception.Message, exception);
  18. context.Result = new BadRequestResult();
  19. }
  20. }
  21. }
  22. }

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

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

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

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

  1. services.TryAddSingleton<IAntiforgery, DefaultAntiforgery>();

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

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

  1. var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);

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

  1. public bool TryValidateTokenSet(
  2. HttpContext httpContext,
  3. AntiforgeryToken cookieToken,
  4. AntiforgeryToken requestToken,
  5. out string message)
  6. {
  7. //去掉了部分非空的判断
  8. // Do the tokens have the correct format?
  9. if (!cookieToken.IsCookieToken || requestToken.IsCookieToken)
  10. {
  11. message = Resources.AntiforgeryToken_TokensSwapped;
  12. return false;
  13. }
  14. // Are the security tokens embedded in each incoming token identical?
  15. if (!object.Equals(cookieToken.SecurityToken, requestToken.SecurityToken))
  16. {
  17. message = Resources.AntiforgeryToken_SecurityTokenMismatch;
  18. return false;
  19. }
  20. // Is the incoming token meant for the current user?
  21. var currentUsername = string.Empty;
  22. BinaryBlob currentClaimUid = null;
  23. var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User);
  24. if (authenticatedIdentity != null)
  25. {
  26. currentClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User));
  27. if (currentClaimUid == null)
  28. {
  29. currentUsername = authenticatedIdentity.Name ?? string.Empty;
  30. }
  31. }
  32. // OpenID and other similar authentication schemes use URIs for the username.
  33. // These should be treated as case-sensitive.
  34. var comparer = StringComparer.OrdinalIgnoreCase;
  35. if (currentUsername.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
  36. currentUsername.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
  37. {
  38. comparer = StringComparer.Ordinal;
  39. }
  40. if (!comparer.Equals(requestToken.Username, currentUsername))
  41. {
  42. message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername);
  43. return false;
  44. }
  45. if (!object.Equals(requestToken.ClaimUid, currentClaimUid))
  46. {
  47. message = Resources.AntiforgeryToken_ClaimUidMismatch;
  48. return false;
  49. }
  50. // Is the AdditionalData valid?
  51. if (_additionalDataProvider != null &&
  52. !_additionalDataProvider.ValidateAdditionalData(httpContext, requestToken.AdditionalData))
  53. {
  54. message = Resources.AntiforgeryToken_AdditionalDataCheckFailed;
  55. return false;
  56. }
  57. message = null;
  58. return true;
  59. }

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

如何使用

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

使用一:常规的Form表单

先在视图添加一个Form表单

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

在控制器添加一个Action

  1. [ValidateAntiForgeryToken]
  2. [HttpPost]
  3. public IActionResult AntiForm(string message)
  4. {
  5. return Content(message);
  6. }

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

再来看看cookie的相关信息:

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

使用二:Ajax提交

表单:

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

js:

  1. $(function () {
  2. $("#btnAjax").on("click", function () {
  3. $("#form2").submit();
  4. });
  5. })

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

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

  1. $.ajax({
  2. type: "post",
  3. dataType: "html",
  4. url: '@Url.Action("AntiAjax", "Home")',
  5. data: { message: $('#ajaxMsg').val() },
  6. success: function (result) {
  7. alert(result);
  8. },
  9. error: function (err, scnd) {
  10. alert(err.statusText);
  11. }
  12. });

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

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

处理方法有两种:

方法一:

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

  1. $.ajax({
  2. //
  3. data: { message: $('#ajaxMsg').val(), __RequestVerificationToken: $("input[name='__RequestVerificationToken']").val()}
  4. });

方法二:

在请求中添加一个header

  1. $("#btnAjax").on("click", function () {
  2. var token = $("input[name='__RequestVerificationToken']").val();
  3. $.ajax({
  4. type: "post",
  5. dataType: "html",
  6. url: '@Url.Action("AntiAjax", "Home")',
  7. data: { message: $('#ajaxMsg').val() },
  8. headers:
  9. {
  10. "RequestVerificationToken": token
  11. },
  12. success: function (result) {
  13. alert(result);
  14. },
  15. error: function (err, scnd) {
  16. alert(err.statusText);
  17. }
  18. });
  19. });

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

使用三:自定义相关信息

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

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

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

  1. services.AddAntiforgery(option =>
  2. {
  3. option.CookieName = "CUSTOMER-CSRF-COOKIE";
  4. option.FormFieldName = "CustomerFieldName";
  5. option.HeaderName = "CUSTOMER-CSRF-HEADER";
  6. });

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

  1. var token = $("input[name='CustomerFieldName']").val();//隐藏域的名称要改
  2. $.ajax({
  3. type: "post",
  4. dataType: "html",
  5. url: '@Url.Action("AntiAjax", "Home")',
  6. data: { message: $('#ajaxMsg').val() },
  7. headers:
  8. {
  9. "CUSTOMER-CSRF-HEADER": token //注意header要修改
  10. },
  11. success: function (result) {
  12. alert(result);
  13. },
  14. error: function (err, scnd) {
  15. alert(err.statusText);
  16. }
  17. });

下面是效果:

Form表单:

Cookie:

本文涉及到的相关项目:

关于CSRF相关的内容

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

浅谈CSRF攻击方式

初探CSRF在ASP.NET Core中的处理方式的更多相关文章

  1. 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获

    项目开发中的一些注意事项以及技巧总结   1.jquery采用ajax向后端请求时,MVC框架并不能返回View的数据,也就是一般我们使用View().PartialView()等,只能返回json以 ...

  2. ASP.NET Core 中文文档 第三章 原理(13)管理应用程序状态

    原文:Managing Application State 作者:Steve Smith 翻译:姚阿勇(Dr.Yao) 校对:高嵩 在 ASP.NET Core 中,有多种途径可以对应用程序的状态进行 ...

  3. ASP.NET Core 中的Ajax全局Antiforgery Token配置

    前言 本文基于官方文档 <在 ASP.NET Core 防止跨站点请求伪造 (XSRF/CSRF) 攻击>扩展另一种全局配置Antiforgery方法,适用于使用ASP.NET Core ...

  4. 【翻译】介绍 ASP.NET Core 中的 Razor Pages

    介绍 ASP.NET Core 中的 Razor Pages 原文地址:Introduction to Razor Pages in ASP.NET Core         译文地址:介绍 asp. ...

  5. ASP.NET Core中防跨站点请求伪造

    CSRF(Cross-site request forgery)利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的. 例子 在某个 ...

  6. ASP.NET Core中的数据保护

    在这篇文章中,我将介绍ASP.NET Core 数据保护系统:它是什么,为什么我们需要它,以及它如何工作. 为什么我们需要数据保护系统? 数据保护系统是ASP.NET Core使用的一组加密api.加 ...

  7. ASP.NET Core 中的那些认证中间件及一些重要知识点

    前言 在读这篇文章之间,建议先看一下我的 ASP.NET Core 之 Identity 入门系列(一,二,三)奠定一下基础. 有关于 Authentication 的知识太广,所以本篇介绍几个在 A ...

  8. Asp.net Core中使用Session

    前言 2017年就这么悄无声息的开始了,2017年对我来说又是特别重要的一年. 元旦放假在家写了个Asp.net Core验证码登录, 做demo的过程中遇到两个小问题,第一是在Asp.net Cor ...

  9. 在ASP.NET Core中使用百度在线编辑器UEditor

    在ASP.NET Core中使用百度在线编辑器UEditor 0x00 起因 最近需要一个在线编辑器,之前听人说过百度的UEditor不错,去官网下了一个.不过服务端只有ASP.NET版的,如果是为了 ...

随机推荐

  1. Android Http请求头与响应头的学习

    本节引言: 上节中我们对Android涉及的网络编程进行了了解,也学习了下Http的基本概念,而本节我们 要学习的是Http的请求头与响应头,当然,可以把也可以把这节看作文档,用到的时候来查查 即可! ...

  2. canvas小球动画原理

    随着html5发展,canvas标签作为h5革命性的发展标志也越来越流行.canvas标签的强大之处,不仅在于它可以作为一个独立的画布,也可以利用canvas做一些动画而不用导入flash文件.同时, ...

  3. 关于Dapper.NET的相关论述

    年少时,为何不为自己的梦想去拼搏一次呢?纵使头破血流,也不悔有那年少轻狂.感慨很多,最近事情也很多,博客也很少更新了,毕竟每个人都需要为自己的生活去努力. 最近在一个群里遇到一个人说的话,在这里不再赘 ...

  4. move.js 源码 学习笔记

    源码笔记: /* move.js * @author:flfwzgl https://github.com/flfwzgl * @copyright: MIT license * Sorrow.X - ...

  5. iframe----摘抄出处未知

    1.frame不能脱离frameSet单独使用,iframe可以: 2.frame不能放在body中:如下可以正常显示: <!--<body>--> <frameset ...

  6. vbs文件小技巧

    vbs文件介绍: VBS是基于Visual Basic的脚本语言.VBS的全称是:Microsoft Visual Basic Script Editon.(微软公司可视化BASIC脚本版). 可以新 ...

  7. 关于利用input的file属性在页面添加图片的问题

    在页面添加图片涉及到兼容的问题怎么解决兼容问题呢?请看下面分析: 在IE浏览器上面我们能直接通过获取其input的value值来获取其图片的路径. 在火狐和谷歌需要用createObjectURL(( ...

  8. koahub.js 0.09 发布,新增钩子机制

    koahubjs发布0.09 新增钩子机制添加钩子机制,控制器钩子和函数钩子修复自动加载bug,实现除自动加载导出的default外,还能自动加载其他的方法记koahubjs钩子开发过程在使用koah ...

  9. node插件http-proxy实现反向代理

    最近自己动手做了一个微信小程序,是直接买的腾讯云的小程序解决方案,怎么说那,用起来还是会遇到不少问题的,不过在交流群里还是会有很多人帮助你的. 闲话少说,因为要再做一个别的服务,就想直接用这台小程序的 ...

  10. 图解Javascript——作用域、作用域链、闭包

    什么是作用域? 作用域是一种规则,在代码编译阶段就确定了,规定了变量与函数的可被访问的范围.全局变量拥有全局作用域,局部变量则拥有局部作用域. js是一种没有块级作用域的语言(包括if.for等语句的 ...