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请求上下文模拟不同场景即可,核心测试用例包括:
- GET/OPTIONS等读请求访问时,不执行验证逻辑,不抛出异常
- POST请求未携带合法令牌时,抛出
HttpAntiForgeryException(对应403响应) - POST请求携带格式正确的合法令牌时,验证通过,不抛出异常
- 标注了
[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
相关产品推荐
相关产品推荐

