CSRF在ASP.NET Core中的處理方法詳解
前言
前幾天,有個(gè)朋友問我關(guān)于AntiForgeryToken問題,由于對(duì)這一塊的理解也并不深入,所以就去研究了一番,梳理了一下。
在梳理之前,還需要簡(jiǎn)單了解一下背景知識(shí)。
AntiForgeryToken 可以說是處理/預(yù)防CSRF的一種處理方案。
那么什么是CSRF呢?
CSRF(Cross-site request forgery)是跨站請(qǐng)求偽造,也被稱為One Click Attack或者Session Riding,通??s寫為CSRF或者XSRF,是一種對(duì)網(wǎng)站的惡意利用。
簡(jiǎn)單理解的話就是:有人盜用了你的身份,并且用你的名義發(fā)送惡意請(qǐng)求。
最近幾年,CSRF處于不溫不火的地位,但是還是要對(duì)這個(gè)小心防范!
更加詳細(xì)的內(nèi)容可以參考維基百科:Cross-site request forgery
下面從使用的角度來分析一下CSRF在 ASP.NET Core中的處理,個(gè)人認(rèn)為主要有下面兩大塊
- 視圖層面
- 控制器
層面視圖層面
用法
@Html.AntiForgeryToken()
在視圖層面的用法相對(duì)比較簡(jiǎn)單,用的還是HtmlHelper的那一套東西。在Form表單中加上這一句就可以了。
原理淺析
當(dāng)在表單中添加了上面的代碼后,頁面會(huì)生成一個(gè)隱藏域,隱藏域的值是一個(gè)生成的token(防偽標(biāo)識(shí)),類似下面的例子
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8FBn4LzSYglJpE6Q0fWvZ8WDMTgwK49lDU1XGuP5-5j4JlSCML_IDOO3XDL5EOyI_mS2Ux7lLSfI7ASQnIIxo2ScEJvnABf9v51TUZl_iM2S63zuiPK4lcXRPa_KUUDbK-LS4HD16pJusFRppj-dEGc" />
其中的name="__RequestVerificationToken"是定義的一個(gè)const變量,value=XXXXX是根據(jù)一堆東西進(jìn)行base64編碼,并對(duì)base64編碼后的內(nèi)容進(jìn)行簡(jiǎn)單處理的結(jié)果,具體的實(shí)現(xiàn)可以參見Base64UrlTextEncoder.cs
生成上面隱藏域的代碼在AntiforgeryExtensions這個(gè)文件里面,github上的源碼文件:AntiforgeryExtensions.cs
其中重點(diǎn)的方法如下:
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("\" />");
}
相當(dāng)?shù)那逦髁耍?/p>
控制器層面
用法
[ValidateAntiForgeryToken]
[AutoValidateAntiforgeryToken]
[IgnoreAntiforgeryToken]
這三個(gè)都是可以基于類或方法的,所以我們只要在某個(gè)控制器或者是在某個(gè)Action上面加上這些Attribute就可以了。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
原理淺析
本質(zhì)是Filter(過濾器),驗(yàn)證上面隱藏域的value
過濾器實(shí)現(xiàn):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();
}
}
}
}
完整實(shí)現(xiàn)可參見github源碼:ValidateAntiforgeryTokenAuthorizationFilter.cs
當(dāng)然這里的過濾器只是一個(gè)入口,相關(guān)的驗(yàn)證并不是在這里實(shí)現(xiàn)的。而是在Antiforgery這個(gè)項(xiàng)目上,其實(shí)說這個(gè)模塊可能會(huì)更貼切一些。
由于是面向接口的編程,所以要知道具體的實(shí)現(xiàn),就要找到對(duì)應(yīng)的實(shí)現(xiàn)類才可以。
在Antiforgery這個(gè)項(xiàng)目中,有這樣一個(gè)擴(kuò)展方法AntiforgeryServiceCollectionExtensions,里面告訴了我們相對(duì)應(yīng)的實(shí)現(xiàn)是DefaultAntiforgery這個(gè)類。其實(shí)Nancy的源碼看多了,看一下類的命名就應(yīng)該能知道個(gè)八九不離十。
services.TryAddSingleton<IAntiforgery, DefaultAntiforgery>();
其中還涉及到了IServiceCollection,但這不是本文的重點(diǎn),所以不會(huì)展開講這個(gè),只是提出它在 .net core中是一個(gè)重要的點(diǎn)。
好了,回歸正題!要驗(yàn)證是否是合法的請(qǐng)求,自然要先拿到要驗(yàn)證的內(nèi)容。
var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);
它是從Cookie中拿到一個(gè)指定的前綴為.AspNetCore.Antiforgery.的Cookie,并根據(jù)這個(gè)Cookie進(jìn)行后面相應(yīng)的判斷。下面是驗(yàn)證的具體實(shí)現(xiàn):
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;
}
注:驗(yàn)證前還有一個(gè)反序列化的過程,這個(gè)反序列化就是從Cookie中拿到要判斷的cookietoken和requesttoken
如何使用
前面粗略介紹了一下其內(nèi)部的實(shí)現(xiàn),下面再用個(gè)簡(jiǎn)單的例子來看看具體的使用情況:
使用一:常規(guī)的Form表單
先在視圖添加一個(gè)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>
在控制器添加一個(gè)Action
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult AntiForm(string message)
{
return Content(message);
}
來看看生成的html是不是如我們前面所說,將@Html.AntiForgeryToken()輸出為一個(gè)name為__RequestVerificationToken的隱藏域:

再來看看cookie的相關(guān)信息:

可以看到,一切都還是按照前面所說的執(zhí)行。在輸入框輸入信息并點(diǎn)擊按鈕也能正常顯示我們輸入的文字。

使用二: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();
});
})
這樣子的寫法也是和上面的結(jié)果是一樣的!
怕的是出現(xiàn)下面這樣的寫法:
$.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);
}
});
這樣,正常情況下確實(shí)是看不出任何毛病,但是實(shí)際確是下面的結(jié)果(400錯(cuò)誤):

相信大家也都發(fā)現(xiàn)了問題的所在了??!隱藏域的相關(guān)內(nèi)容并沒有一起post過去!!
處理方法有兩種:
方法一:
在data中加上隱藏域相關(guān)的內(nèi)容,大致如下:
$.ajax({
//
data: { message: $('#ajaxMsg').val(), __RequestVerificationToken: $("input[name='__RequestVerificationToken']").val()}
});
方法二:
在請(qǐng)求中添加一個(gè)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);
}
});
});
這樣就能處理上面出現(xiàn)的問題了!
使用三:自定義相關(guān)信息
可能會(huì)有不少人覺得,像那個(gè)生成的隱藏域那個(gè)name能不能換成自己的,那個(gè)cookie的名字能不能換成自己的〜〜
答案是肯定可以的,下面簡(jiǎn)單示范一下:
在Startup的ConfigureServices方法中,添加下面的內(nèi)容即可對(duì)默認(rèn)的名稱進(jìn)行相應(yīng)的修改。
services.AddAntiforgery(option =>
{
option.CookieName = "CUSTOMER-CSRF-COOKIE";
option.FormFieldName = "CustomerFieldName";
option.HeaderName = "CUSTOMER-CSRF-HEADER";
});
相應(yīng)的,ajax請(qǐng)求也要做修改:
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:

本文涉及到的相關(guān)項(xiàng)目:
關(guān)于CSRF相關(guān)的內(nèi)容
Preventing Cross-Site Request Forgery (XSRF/CSRF) Attacks in ASP.NET Core
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
gridview+objectdatasource+aspnetpager整合實(shí)例
gridview+objectdatasource+aspnetpager整合實(shí)例,需要的朋友可以參考一下2013-04-04
C#讀取中文字符及清空緩沖區(qū)的實(shí)現(xiàn)代碼
有一個(gè)txt的中英文語料庫文件,內(nèi)容是英文一句中文一句相間的,共3000行,需要把英文句和中文句分開,放在單獨(dú)的txt文件中。2010-12-12
.net平臺(tái)的rabbitmq使用封裝demo詳解
這篇文章主要針對(duì)rabbitmq學(xué)習(xí)后封裝RabbitMQ.Client的一個(gè)分享,文章最后,我會(huì)把封裝組件和demo奉上,對(duì).net平臺(tái)的rabbitmq使用封裝相關(guān)知識(shí)感興趣的朋友一起看看吧2021-09-09
.net出現(xiàn)80080005錯(cuò)誤的解決辦法分享
這篇文章介紹了.net出現(xiàn)80080005錯(cuò)誤的解決辦法,有需要的朋友可以參考一下,希望可以對(duì)你有所幫助2013-07-07
asp.net編程獲取項(xiàng)目根目錄實(shí)現(xiàn)方法集合
這篇文章主要介紹了asp.net編程獲取項(xiàng)目根目錄實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析總結(jié)了asp.net針對(duì)項(xiàng)目目錄的操作技巧與注意事項(xiàng),需要的朋友可以參考下2015-11-11
asp.net利用ashx文件實(shí)現(xiàn)文件的上傳功能
這篇文章主要介紹了asp.net利用ashx文件實(shí)現(xiàn)文件的上傳功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Asp.net 動(dòng)態(tài)加載用戶自定義控件,并轉(zhuǎn)換成HTML代碼
Ajax現(xiàn)在已經(jīng)是相當(dāng)流行的技術(shù)了,Ajax不僅是想服務(wù)器端發(fā)送消息,更重要的是無刷新的重載頁面。2010-03-03
NET Core TagHelper實(shí)現(xiàn)分頁標(biāo)簽
這篇文章主要介紹了NET Core TagHelper實(shí)現(xiàn)分頁標(biāo)簽,講述實(shí)現(xiàn)一個(gè)簡(jiǎn)單分頁和總要注意步奏,感興趣的小伙伴們可以參考一下2016-07-07

