Web API和ValidateAntiForgeryToken
我们有一些现有的MVC Web服务,称为AJAX风格的网页。 这些服务使用ValidateAntiForgeryToken属性来帮助防止请求伪造。
我们希望将这些服务迁移到Web API,但似乎没有等效的防伪function。
我错过了什么吗? 使用Web API解决请求伪造有什么不同的方法吗?
你可以实现这样的授权属性:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter { public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation) { try { AntiForgery.Validate(); } catch { actionContext.Response = new HttpResponseMessage { StatusCode = HttpStatusCode.Forbidden, RequestMessage = actionContext.ControllerContext.Request }; return FromResult(actionContext.Response); } return continuation(); } private Task<HttpResponseMessage> FromResult(HttpResponseMessage result) { var source = new TaskCompletionSource<HttpResponseMessage>(); source.SetResult(result); return source.Task; } }
然后用它来装饰你的API动作:
[ValidateAntiForgeryToken] public HttpResponseMessage Post() { // some work return Request.CreateResponse(HttpStatusCode.Accepted); }
补充上面的代码FilterAttribute
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter { public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation) { try { string cookieToken = ""; string formToken = ""; IEnumerable<string> tokenHeaders; if (actionContext.Request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders)) { string[] tokens = tokenHeaders.First().Split(':'); if (tokens.Length == 2) { cookieToken = tokens[0].Trim(); formToken = tokens[1].Trim(); } } AntiForgery.Validate(cookieToken, formToken); } catch (System.Web.Mvc.HttpAntiForgeryException e) { actionContext.Response = new HttpResponseMessage { StatusCode = HttpStatusCode.Forbidden, RequestMessage = actionContext.ControllerContext.Request }; return FromResult(actionContext.Response); } return continuation(); } private Task<HttpResponseMessage> FromResult(HttpResponseMessage result) { var source = new TaskCompletionSource<HttpResponseMessage>(); source.SetResult(result); return source.Task; }
Htmlfunction使用剃刀
@functions{ public string TokenHeaderValue() { string cookieToken, formToken; AntiForgery.GetTokens(null, out cookieToken, out formToken); return cookieToken + ":" + formToken; } }
使用Angular
return $http({ method: 'POST', url: '@Url.Content("~/api/invite/")', data: {}, headers: { 'RequestVerificationToken': '@TokenHeaderValue()' } });
这个链接有帮助,您可以从剃刀视图中检索防伪标记,并将标记作为标题传递:
var csrfToken = $("input[name='__RequestVerificationToken']").val(); $.ajax({ headers: { __RequestVerificationToken: csrfToken }, type: "POST", dataType: "json", contentType: 'application/json; charset=utf-8', url: "/api/products", data: JSON.stringify({ name: "Milk", price: 2.33 }), statusCode: { 200: function () { alert("Success!"); } } });
Oswaldo的答案,但作为AuthorizeAttribute实施
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class ApiValidateAntiForgeryToken : AuthorizeAttribute { public static string GenerateAntiForgeryTokenForHeader() { string cookieToken, formToken; AntiForgery.GetTokens(null, out cookieToken, out formToken); return cookieToken + ":" + formToken; } protected override bool IsAuthorized(HttpActionContext actionContext) { var headers = actionContext.Request.Headers; // we pass both the cookie and the form token into a single header field string headerToken = headers.Contains("__RequestVerificationToken") ? headers.GetValues("__RequestVerificationToken").FirstOrDefault() : null; if (headerToken == null) { return false; } string[] tokens = headerToken.Split(':'); if (tokens.Length != 2) { return false; } string cookieToken = tokens[0].Trim(); string formToken = tokens[1].Trim(); try { AntiForgery.Validate(cookieToken, formToken); } catch { return false; } return base.IsAuthorized(actionContext); } }
您可以使用[ApiValidateAntiForgeryToken]来修饰您的控制器或方法,然后在您的razorJavaScript代码中将此方法的标头传递给RequestVerificationToken:“@ ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader()”。
考虑到这一点之后,混合cookie和forms令牌是一个坏主意,因为它打败了反伪造令牌的全部目的。 将cookie部分保存为cookie,同时将表单部分移至auth头部,因此这个新的答案(又是一个AuthorizeAttribute)更好。
using System; using System.Linq; using System.Net.Http; using System.Web; using System.Web.Helpers; using System.Web.Http; using System.Web.Http.Controllers; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class ApiValidateAntiForgeryToken : AuthorizeAttribute { public const string HeaderName = "X-RequestVerificationToken"; private static string CookieName => AntiForgeryConfig.CookieName; public static string GenerateAntiForgeryTokenForHeader(HttpContext httpContext) { if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } // check that if the cookie is set to require ssl then we must be using it if (AntiForgeryConfig.RequireSsl && !httpContext.Request.IsSecureConnection) { throw new InvalidOperationException("Cannot generate an Anti Forgery Token for a non secure context"); } // try to find the old cookie token string oldCookieToken = null; try { var token = httpContext.Request.Cookies[CookieName]; if (!string.IsNullOrEmpty(token?.Value)) { oldCookieToken = token.Value; } } catch { // do nothing } string cookieToken, formToken; AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken); // set the cookie on the response if we got a new one if (cookieToken != null) { var cookie = new HttpCookie(CookieName, cookieToken) { HttpOnly = true, }; // note: don't set it directly since the default value is automatically populated from the <httpCookies> config element if (AntiForgeryConfig.RequireSsl) { cookie.Secure = AntiForgeryConfig.RequireSsl; } httpContext.Response.Cookies.Set(cookie); } return formToken; } protected override bool IsAuthorized(HttpActionContext actionContext) { if (HttpContext.Current == null) { // we need a context to be able to use AntiForgery return false; } var headers = actionContext.Request.Headers; var cookies = headers.GetCookies(); // check that if the cookie is set to require ssl then we must honor it if (AntiForgeryConfig.RequireSsl && !HttpContext.Current.Request.IsSecureConnection) { return false; } try { string cookieToken = cookies.Select(c => c[CookieName]).FirstOrDefault()?.Value?.Trim(); // this throws if the cookie does not exist string formToken = headers.GetValues(HeaderName).FirstOrDefault()?.Trim(); if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken)) { return false; } AntiForgery.Validate(cookieToken, formToken); return base.IsAuthorized(actionContext); } catch { return false; } } }
然后只需用[ApiValidateAntiForgeryToken]装饰你的控制器或方法
并添加到剃刀文件这个生成你的令牌的JavaScript:
<script> var antiForgeryToken = '@ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader(HttpContext.Current)'; // your code here that uses such token, basically setting it as a 'X-RequestVerificationToken' header for any AJAX calls </script>