問題 通過 CSRF(Cross-Site Request Forgery)防護,保護從 MVC 頁面提交到ASP.NET Web API 的數據。 解決方案 ASP.NET 已經加入了 CSRF 防護功能,只要通過 System.web.Helpers.AntiForgery 類(System.W ...
問題
通過 CSRF(Cross-Site Request Forgery)防護,保護從 MVC 頁面提交到ASP.NET Web API 的數據。
解決方案
ASP.NET 已經加入了 CSRF 防護功能,只要通過 System.web.Helpers.AntiForgery 類(System.Web.WebPages 的一部分)就可以。
他會生成兩個 Token:
-
Cookie Token
-
基於字元串的 Token
基於字元串的 Token 是可以嵌入到表單或者請求頭(使用 Ajax 的情況下)。為了防止 CSRF 攻擊,表單提交和Ajax 請求到 API 的數據必須包含這些Token,伺服器將會驗證這兩個 Token。
在 ASP.NET Web API,anti-CSRF Token 驗證是一個典型的實現了橫切關係的 MessageHandler。
工作原理
為了能在 MVC 應用程式的上下文中生成 Token,我們必須在表單中調用一個叫做 AntiForgeryToken 的HtmlHelper 的擴展方法。
1 2 3 4 |
< form id = "myForm" >
@Html.AntiForgeryToken()
@* 其他標簽 *@
</ form >
|
這個幫助方法在AntiForgery 類中。他會寫一個 Token 到響應的 Cookie 中,同時生成一個名字叫做_RequestVerificationToken 的欄位,也會隨著表單數據同時被提交。
為能在伺服器端驗證 Token,我們可以通過調用AntiForgery 類的靜態方法 Validate 來驗證。如果調用的時候沒有傳遞參數的話,就會從 HttpContext.Current 中試著獲取相關的 Cookie 和請求體中提取 Token,在這裡,我們假設確實有一個 Body 並且 Body 中也有一個 _RequestVerificationToken。
由於這個方法是 void (無返回值)的,所以,請求驗證成功後,方法什麼反饋也沒有,如果失敗,就會拋HttpAntiForgeryException 的異常。我們可以捕獲這個異常,然後返回給客戶端相應的響應(例如,一個 HTTP 403 的狀態碼)。
有一個可替代的方式就是調用 Validate 方法,我們自己來傳這兩個 Token。這時候,就要從 Request 中獲取這兩個值。例如,可能是在 Header 中。這種方式也可以擺脫對 HttpContext 的依賴。
對於 Web API,我們可以自定義消息處理器,在每個請求進入 Web API 的時候來負責 CSRF Token 的驗證,執行必要的驗證,然後繼續管道執行,或者,在請求無效的情況下,直接短路錯誤響應(也就是說,立即返回錯誤碼)。
代碼演示
我們來演示 MessageHandler 執行 CSRF 驗證的例子如清單 1-23 所示。
兩種方式:
-
用 Ajax 請求。
-
用其他的請求。
我們都簡單假設他們都是表單提交的。如果是一個 Ajax 請求,我們可以嘗試著從請求 Header 中獲取Token,同時,可以從與 Request 一同提交的 Cookie 集合中獲取Cookie Token,然後,使用無參的 Validate方法驗證,這樣,就需要我們自己來提取 Token。
如果驗證失敗,客戶端會得到一個 403 的錯誤響應。
清單 1-23. Anti_CSRF 消息處理器
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 |
public class AntiForgeryHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
string cookieToken = null ;
string formToken = null ;
if (request.IsAjaxRequest())
{
IEnumerable< string > tokenHeaders;
if (request.Headers.TryGetValues( "__RequestVerificationToken" , out tokenHeaders))
{
var cookie = request.Headers.GetCookies(AntiForgeryConfig.CookieName).
FirstOrDefault();
if (cookie != null )
{
cookieToken = cookie[AntiForgeryConfig.CookieName].Value;
}
formToken = tokenHeaders.FirstOrDefault();
}
}
try
{
if (cookieToken != null && formToken != null )
{
AntiForgery.Validate(cookieToken, formToken);
}
else
{
AntiForgery.Validate();
}
}
catch (HttpAntiForgeryException)
{
return request.CreateResponse(HttpStatusCode.Forbidden);
}
return await base .SendAsync(request, cancellationToken);
}
}
|
我們還需要在 API 的HttpConfiguration 中註冊,這樣才會在全局起作用。
1 |
config.MessageHandlers.Add( new AntiForgeryHandler());
|
構築一個 anti-CSRF 護盾作為消息處理器並不是唯一方式。我們也可以在過濾器內部使用同樣的代碼,然後將過濾器應用到相應的 Action 上(類似的,怎麼用過濾器驗證,我們將在 5-4 詳細討論)。如果消息處理器不是全局使用,也可以附加到指定路由上。我們將在 3-9 詳細討論這一塊兒。
HttpRequestMessage有一個內建的方式來檢查是否為 Ajax 請求,就是用一個簡單的擴展方法來實現,他依賴於 Header 的 X-Requested-With,大多數的 JavaScript 框架都會自動發送這個在 Header 中。這個方法如清單1-24 所示。
清單 1-24. 檢查 HttpRequestMessage 是否為一個 Ajax 請求的擴展方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public static class HttpRequestMessageExtensions
{
public static bool IsAjaxRequest( this HttpRequestMessage request)
{
IEnumerable< string > headers;
if (request.Headers.TryGetValues( "X-Requested-With" , out headers))
{
var header = headers.FirstOrDefault();
if (! string .IsNullOrEmpty(header))
{
return header.ToLowerInvariant() == "xmlhttprequest" ;
}
}
return false ;
}
}
|
清單 1-25 展示了,傳統表單提交和 Ajax 請求都利用 anti-CSRF Token 的例子。在傳統表單提交的情況下,HTML helper 會生成一個隱藏域,同時,anti-forgery token 會隨著表單一塊兒被自動提交。在 Ajax 請求的情況下,我們顯示的從隱藏域中讀取 Token,然後,將其附加到請求頭中。
清單 1-25. 傳統表單和 Ajax 請求方式下,提交數據到 ASP.NET WEB API 使用 Anti-CSRF 防護
//HTML表單
1 2 3 4 5 6 7 8 9 10 11 12 |
<form id= "form1" method= "post" action= "/api/form" enctype= "application/x-www-form-urlencoded" >
@Html.AntiForgeryToken()
<div>
<label for = "name" >Name</label>
</div>
<div>
<input type= "text" name= "name" value= "Some Name" />
</div>
<div>
<button id= "postData" name= "postData" >Post form</button>
</div>
</form>
|
// Ajax 表單
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Html.AntiForgeryToken()
< input id = "itemJS" type = "text" disabled = "disabled" name = "text" value = "some text" />
< div >
< button id = "postJS" name = "postJS" >Post JS</ button >
</ div >
< script type = "text/javascript" >
$(function () {
$("#postJS").on("click", function () {
$.ajax({
dataType: "json",
data: JSON.stringify({ name: $("#itemJS").val() }),
type: "POST",
headers: {
"__RequestVerificationToken": $("#jsData input[name='__
RequestVerificationToken']").val()
},
contentType: "application/json; charset=utf-8",
url: "/api/items"
}).done(function (res) {
alert(res.Name);
});
});
});
</ script >
|