前言
前几天,有个朋友问我关于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
2
3
4
5
6
7
8
|
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就可以了。
1
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false , Inherited = true )]
|
原理浅析
本质是Filter(过滤器),验证上面隐藏域的value
过滤器实现:ValidateAntiforgeryTokenAuthorizationFilter和AutoValidateAntiforgeryTokenAuthorizationFilter
其中 AutoValidateAntiforgeryTokenAuthorizationFilter是继承了ValidateAntiforgeryTokenAuthorizationFilter,只重写了其中的ShouldValidate方法。
下面贴出ValidateAntiforgeryTokenAuthorizationFilter的核心方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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的源码看多了,看一下类的命名就应该能知道个八九不离十。
1
|
services.TryAddSingleton<IAntiforgery, DefaultAntiforgery>();
|
其中还涉及到了IServiceCollection,但这不是本文的重点,所以不会展开讲这个,只是提出它在 .net core中是一个重要的点。
好了,回归正题!要验证是否是合法的请求,自然要先拿到要验证的内容。
1
|
var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);
|
它是从Cookie中拿到一个指定的前缀为.AspNetCore.Antiforgery.的Cookie,并根据这个Cookie进行后面相应的判断。下面是验证的具体实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
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;
{
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表单
1
2
3
4
5
|
< 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
1
2
3
4
5
6
|
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult AntiForm( string message)
{
return Content(message);
}
|
来看看生成的html是不是如我们前面所说,将@Html.AntiForgeryToken()
输出为一个name为__RequestVerificationToken
的隐藏域:
再来看看cookie的相关信息:
可以看到,一切都还是按照前面所说的执行。在输入框输入信息并点击按钮也能正常显示我们输入的文字。
使用二:Ajax提交
表单:
1
2
3
4
5
|
< 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:
1
2
3
4
5
|
$( function () {
$( "#btnAjax" ).on( "click" , function () {
$( "#form2" ).submit();
});
})
|
这样子的写法也是和上面的结果是一样的!
怕的是出现下面这样的写法:
1
2
3
4
5
6
7
8
9
10
11
12
|
$.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中加上隐藏域相关的内容,大致如下:
1
2
3
4
|
$.ajax({
//
data: { message: $( '#ajaxMsg' ).val(), __RequestVerificationToken: $( "input[name='__RequestVerificationToken']" ).val()}
});
|
方法二:
在请求中添加一个header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
$( "#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方法中,添加下面的内容即可对默认的名称进行相应的修改。
1
2
3
4
5
6
|
services.AddAntiforgery(option =>
{
option.CookieName = "CUSTOMER-CSRF-COOKIE" ;
option.FormFieldName = "CustomerFieldName" ;
option.HeaderName = "CUSTOMER-CSRF-HEADER" ;
});
|
相应的,ajax请求也要做修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。
原文链接:http://www.cnblogs.com/catcher1994/p/6720212.html