[Asp.net MVC] 自訂filter,支援來自http header的CSRF token驗證

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();