support csrf token from http header
前言
預設ASP.net MVC 5的[ValidateAntiForgeryToken] Attribute只會驗證來自表單欄位的CSRF token,
想要支援來自 http header的CSRF token的話,要自己實作Filter類別
實作
在ASP.net MVC 5專案下建立一Filters資料夾並新加入MyValidateAntiForgeryTokenAttribute.cs檔案
using System;
using System.Linq;
using System.Web;
using System.Web.Helpers;
using System.Web.Mvc;
namespace TestProject.Filters
{
/// <summary>
/// 自訂 Anti-Forgery Token 驗證 Filter
/// 支援從 HTTP Header 或表單欄位驗證令牌
/// </summary>
public class MyValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
/// <summary>
/// HTTP Header 中的令牌名稱(預設)
/// </summary>
private const string HeaderTokenName1 = "X-CSRF-Token";
private const string HeaderTokenName2 = "__RequestVerificationToken";
/// <summary>
/// 表單欄位中的令牌名稱
/// </summary>
private const string FormTokenName = "__REQUEstVerificationToken";
/// <summary>
/// 執行授權驗證
/// </summary>
/// <param name="filterContext">授權上下文</param>
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException(nameof(filterContext));
}
var request = filterContext.HttpContext.Request;
string tokenValue = null;
string cookieToken = null;
string[] reqHeaderKeys = request.Headers.AllKeys;
// 1. 優先從 HTTP Header 取得令牌
if (reqHeaderKeys.Any(key=> key.Equals(HeaderTokenName1,StringComparison.OrdinalIgnoreCase)))//不區分大小寫
{
tokenValue = request.Headers[HeaderTokenName1];//不區分大小寫
}
else if (reqHeaderKeys.Any(key => key.Equals(HeaderTokenName2, StringComparison.OrdinalIgnoreCase)))//不區分大小寫
{
tokenValue = request.Headers[HeaderTokenName2];//不區分大小寫
}
else // 2. 如果 Header 中沒有,則從表單欄位取得
{
tokenValue = request.Form[FormTokenName];//不區分大小寫
}
// 3. 從 Cookie 取得令牌
HttpCookie cookie = request.Cookies[AntiForgeryConfig.CookieName];
if (cookie != null)
{
cookieToken = cookie.Value;
}
// 4. 驗證令牌
try
{
if (string.IsNullOrEmpty(cookieToken))
{
throw new HttpAntiForgeryException($@"缺少 Anti-Forgery cookieToken");
}
if (string.IsNullOrEmpty(tokenValue))
{
throw new HttpAntiForgeryException($@"缺少 Anti-Forgery 前端 Token");
}
//手動驗證
AntiForgery.Validate(cookieToken, tokenValue);
}
catch (HttpAntiForgeryException ex)
{
// 驗證失敗,返回 403 Forbidden
filterContext.Result = new HttpStatusCodeResult(
System.Net.HttpStatusCode.Forbidden,
$"Anti-Forgery Token 驗證失敗: {ex.Message}"
);
}
}
}
}
使用方法
Controller的動作方法
[HttpPost]
[MyValidateAntiForgeryToken] //加這一段
public ActionResult MyPost(string userName,string userSex)
{
return Json(new { isOk = true, errMsg = "" });
}
前端View與JS
<form>
<!--加這一行↓-->
@Html.AntiForgeryToken()
</form>
//準備要發送的資料
const csrfToken= document.querySelector('input[name="__RequestVerificationToken"]').value;
const ajaxData = {
userName: "測試姓名",
userSex: "M"
};
// 使用 JS 原生fetch 發送 POST 請求
const response = await fetch('@Url.Action("MyPost","Home")', {
method: 'post',
headers: {
'X-CSRF-Token': csrfToken,
"content-type": "application/json"
},
body: JSON.stringify(ajaxData)
});
// 成功送出Ajax並取得後端的json回傳結果
const result = await response.json();