You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MVC项目中Ajax请求头Antiforgery令牌验证实现咨询

MVC项目Antiforgery防伪验证实现方案

现有前端代码的问题

你当前的前端传值逻辑存在遗漏:@Html.AntiForgeryToken() 生成令牌时会同时产出两个值:

  • 存储在浏览器Cookie中的cookie令牌
  • 页面隐藏input域中的表单令牌
    官方验证逻辑需要同时拿到两个值做校验,你当前只传递了input中的表单令牌,会直接导致验证失败。

服务端实现(最小重复代码方案)

相关疑问直接给出结论:

  • 不需要为每个HTTP请求单独标注验证特性
  • 特性支持控制器层级标注,也支持全局自动注册
  • 最优方案是全局注册自定义验证过滤器,一次配置全站生效,不需要在业务代码里重复加特性

第一步:编写自定义防伪验证过滤器

你找到的官方示例是WebAPI版本的,ASP.NET MVC需要适配请求上下文类型,实现代码如下:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateAjaxAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        var request = filterContext.HttpContext.Request;

        // 跳过标注了忽略防伪验证特性的接口
        if (filterContext.ActionDescriptor.IsDefined(typeof(IgnoreAntiforgeryTokenAttribute), inherit: true)
            || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(IgnoreAntiforgeryTokenAttribute), inherit: true))
        {
            return;
        }

        // GET/HEAD/OPTIONS等读请求不做验证
        if (request.HttpMethod.Equals(HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase)
            || request.HttpMethod.Equals(HttpMethod.Head.Method, StringComparison.OrdinalIgnoreCase)
            || request.HttpMethod.Equals(HttpMethod.Options.Method, StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        string cookieToken = string.Empty;
        string formToken = string.Empty;

        // 先从自定义请求头取令牌(适配AJAX JSON请求场景)
        var tokenHeader = request.Headers["x-verification-token"];
        if (!string.IsNullOrEmpty(tokenHeader))
        {
            string[] tokens = tokenHeader.Split(':');
            if (tokens.Length == 2)
            {
                cookieToken = tokens[0].Trim();
                formToken = tokens[1].Trim();
            }
        }

        // 兜底:兼容普通表单提交场景
        if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken))
        {
            cookieToken = request.Cookies["__RequestVerificationToken"]?.Value;
            formToken = request.Form["__RequestVerificationToken"];
        }

        // 执行验证,验证失败会自动抛出HttpAntiForgeryException,返回403状态码
        System.Web.Helpers.AntiForgery.Validate(cookieToken, formToken);
    }
}

// 配套的忽略特性,用于不需要验证的特殊接口(比如第三方回调接口)
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class IgnoreAntiforgeryTokenAttribute : Attribute { }

第二步:注册过滤器(三选一即可,优先选全局注册)

  • 全局注册(最省事):打开项目App_Start/FilterConfig.cs文件,在RegisterGlobalFilters方法中添加一行配置,全站所有接口自动生效,不需要加任何特性:
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        // 添加这行
        filters.Add(new ValidateAjaxAntiForgeryTokenAttribute());
    }
    
  • 控制器层级注册:不需要全局生效的话,直接在控制器类上方标注特性,该控制器下所有Action自动执行验证:
    [ValidateAjaxAntiForgeryToken]
    public class OrderController : Controller
    {
        // 控制器内所有POST/PUT接口自动验证
        public ActionResult Submit(OrderModel model) { ... }
    }
    
  • Action层级注册:只需要给单个接口加验证的话,直接在Action方法上方标注特性即可。

修正后的前端代码

把原来的取令牌逻辑改成同时读取Cookie和隐藏域的值,拼接后再传递到请求头:

// 公共取令牌方法,全站复用
function getAntiForgeryToken() {
    // 读取Cookie中的令牌
    const cookieRow = document.cookie.split('; ').find(row => row.startsWith('__RequestVerificationToken='));
    const cookieToken = cookieRow ? decodeURIComponent(cookieRow.split('=')[1]) : '';
    // 读取页面隐藏域中的表单令牌
    const formToken = document.querySelector('input[name="__RequestVerificationToken"]')?.value || '';
    // 按验证要求用冒号拼接两个令牌
    return `${cookieToken}:${formToken}`;
}

// AJAX请求示例
return $.ajax({
    url: url,
    type: 'post',
    headers: {
        'x-system-source': headerValue,
        'x-verification-token': getAntiForgeryToken()
    },
    data: ko.toJSON(entity),
    contentType: 'application/json',
    dataType: 'json'
});

注意:页面渲染时必须在表单内调用@Html.AntiForgeryToken(),否则不会生成隐藏域和对应Cookie。

单元测试方案

核心测试逻辑不需要启动真实站点,通过Mock请求上下文模拟不同场景即可,核心测试用例包括:

  1. GET/OPTIONS等读请求访问时,不执行验证逻辑,不抛出异常
  2. POST请求未携带合法令牌时,抛出HttpAntiForgeryException(对应403响应)
  3. POST请求携带格式正确的合法令牌时,验证通过,不抛出异常
  4. 标注了[IgnoreAntiforgeryToken]的接口,无论是否携带令牌都跳过验证

简单测试示例(基于MSTest+Moq):

[TestMethod]
public void PostRequestWithoutToken_ShouldThrowAntiForgeryException()
{
    // Mock请求对象
    var mockRequest = new Mock<HttpRequestBase>();
    mockRequest.Setup(r => r.HttpMethod).Returns("POST");
    mockRequest.Setup(r => r.Headers["x-verification-token"]).Returns(string.Empty);
    mockRequest.Setup(r => r.Cookies["__RequestVerificationToken"]).Returns((HttpCookie)null);
    mockRequest.Setup(r => r.Form["__RequestVerificationToken"]).Returns(string.Empty);

    // Mock Http上下文
    var mockHttpContext = new Mock<HttpContextBase>();
    mockHttpContext.Setup(c => c.Request).Returns(mockRequest.Object);

    // 构造授权上下文
    var actionDescriptor = new Mock<ActionDescriptor>();
    var controllerDescriptor = new Mock<ControllerDescriptor>();
    actionDescriptor.Setup(a => a.ControllerDescriptor).Returns(controllerDescriptor.Object);
    var filterContext = new AuthorizationContext
    {
        HttpContext = mockHttpContext.Object,
        ActionDescriptor = actionDescriptor.Object
    };

    var filter = new ValidateAjaxAntiForgeryTokenAttribute();
    // 断言验证抛出预期异常
    Assert.ThrowsException<HttpAntiForgeryException>(() => filter.OnAuthorization(filterContext));
}

补充说明:如果使用的是ASP.NET Core MVC,框架已经原生支持AJAX请求头防伪验证,不需要自行编写过滤器,直接全局注册内置的[ValidateAntiForgeryToken]特性,前端将拼接好的令牌放到RequestVerificationToken请求头即可。

内容的提问来源于stack exchange,提问作者ilito

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 09:36:24