客製 CSRF/XSRF Token 驗證 for NET8 React and ASP.NET Core 專案
引言
經幾天試用 .NET8 的 React and ASP.NET Core
專案,預設的 Anti-Forgery 機制並不合用(上一版的可以)。思考後決定自製。未來若又可用了就用回去預設的 Anti-Forgery 機制。
自訂 Anti-forgery 機制
用於登入。流程大蓋如下圖。
1) 當一進入 Login page 時,自動抓取 XSRF token 回來。
2) 送出登入封包時,夾帶 XSRF token 上去檢查,若登入成功送回 Auth Cookie。
其中,不管是 XSRF toekn 還是 Auth Cookie 都用 cookie 送回,並加上同源政策檢查,httpOnly=true, Secure=true, SameSite=Lax。

程式碼紀錄
這裡只看 XSRF token 的部份。
開發成 Filter 來使用更方便。
[AllowAnonymous]
[ApiController]
[Route("api/[controller]")]
public class AccountController(ILogger<AccountController> _logger, IMemoryCache _cache, AccountService _account) : ControllerBase
{
[HttpPost("[action]")]
public ActionResult<string> GetXsrfToken()
{
ValidateXsrfTokenFilter.ResponseAndStoreXsrfToken(this.HttpContext, _cache);
return NoContent();
}
[ServiceFilter<ValidateXsrfTokenFilter>]
[HttpPost("[action]")]
public async Task<ActionResult> Login(LoginArgs login)
{
[...略...]
}
}
XSRF 相關的碼集中在一起。開發成 Filter 來使用更方便。
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
using Vista.Models;
namespace YourProject.Server.Models;
/// <summary>
/// for Anit-Forgery
/// </summary>
public class ValidateXsrfTokenFilter(ILogger<ValidateXsrfTokenFilter> _logger, IMemoryCache _cache) : Attribute, IAuthorizationFilter
{
const string XSRF_TOKEN_NAME = ".Your.XSRF-TOKEN-xyz";
public void OnAuthorization(AuthorizationFilterContext context)
{
try
{
if (!context.HttpContext.Request.Cookies.TryGetValue(XSRF_TOKEN_NAME, out string? extractedToken))
{
_logger.LogError("XSRF-TOKEN is missing.");
context.Result = new UnauthorizedResult();
return;
}
Guid loginSid = Utils.AesSimpleDecrypt<Guid>(extractedToken);
if (!_cache.TryGetValue<string>($"XSRF-TOKEN:{loginSid}", out string? _token))
{
_logger.LogError("XSRF-TOKEN is not exists.");
context.Result = new UnauthorizedResult();
return;
}
// 取出 token 後就可移除。確保只能用一次。
_cache.Remove($"XSRF-TOKEN:{loginSid}");
if (_token != extractedToken)
{
_logger.LogError("XSRF-TOKEN is invalid.");
context.Result = new UnauthorizedResult();
return;
}
// 送回新的 XSRF-TOKEN
ResponseAndStoreXsrfToken(context.HttpContext, _cache);
}
catch(Exception ex)
{
_logger.LogError(ex, "XSRF-TOKEN exception.");
context.Result = new UnauthorizedResult();
}
}
/// <summary>
/// Procedure
/// </summary>
public static void ResponseAndStoreXsrfToken(HttpContext context, IMemoryCache cache)
{
Guid loginSid = Guid.NewGuid();
string token = Utils.AesSimpleEncrypt(loginSid);
/// 此 anit-forgery-token 只能做到不能重複 post 同一個封包。
/// 正式版的 anit-forgery-token 檢驗依據項目應加入 client side 一些識別資訊!
cache.Set($"XSRF-TOKEN:{loginSid}", token, TimeSpan.FromMinutes(3)); // 3分鐘內需完成登入
// 送回 cookie
context.Response.Cookies.Append(XSRF_TOKEN_NAME, token, new CookieOptions()
{
Expires = DateTimeOffset.Now.AddMinutes(3),
SameSite = SameSiteMode.Lax, // SameSiteMode.Strict,
Secure = true,
HttpOnly = true,
IsEssential = true, // for GDPR Consent. 若該 cookie 為 essential 就不需使用者同意就可寫入。.
});
}
}
別忘了註冊
var builder = WebApplication.CreateBuilder(args);
//§ for Anit-Forgery
builder.Services.AddScoped<ValidateXsrfTokenFilter>();
...略...
完整程式碼
(EOF)
Last updated