在 ASP.NET Web API 2 中实现 OPTIONS 响应






4.35/5 (8投票s)
REST 服务中 OPTIONS 响应的自动化
HTTP OPTIONS 方法是一个让 REST 服务自我文档化的好机会:http://zacstewart.com/2012/04/14/http-options-method.html。
最基本的要求是在响应内容中包含 Allow
头,列举给定 URI 的所有可用方法,因此实现起来相对容易。
[HttpOptions]
[ResponseType(typeof(void))]
[Route("Books", Name = "Options")]
public IHttpActionResult Options()
{
HttpContext.Current.Response.AppendHeader("Allow", "GET,OPTIONS");
return Ok();
}
响应
HTTP/1.1 200 OK
Allow: GET,OPTIONS
Content-Length: 0
然而,这种方法存在几个严重的缺点。 我们必须手动维护支持的方法列表,并且必须为每个控制器和每个支持的 URI 重复此操作。 在引入新的 URI 时,很容易忘记添加适当的 OPTIONS
操作。 因此,在这里添加一些自动化将会很棒。
ASP.NET Web API 提供了一种更高级别的解决方案:HTTP 消息处理程序。
我从 DelegatingHandler
继承了一个新的处理程序,重写了 SendAsync
方法,并将我的功能作为基础任务的延续添加进去。 这很重要,因为我希望在任何处理之前运行基本的路由机制。 在这种情况下,请求将包含所有必需的属性。
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
return await base.SendAsync(request, cancellationToken).ContinueWith(
task =>
{
var response = task.Result;
if (request.Method == HttpMethod.Options)
{
var methods = new ActionSelector(request).GetSupportedMethods();
if (methods != null)
{
response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(string.Empty)
};
response.Content.Headers.Add("Allow", methods);
response.Content.Headers.Add("Allow", "OPTIONS");
}
}
return response;
}, cancellationToken);
}
类 ActionSelector
尝试在构造函数中为给定的请求找到适当的控制器。 如果未找到控制器,方法 GetSupportedMethods
将返回 null
。 函数 IsMethodSupported
采用一种技巧,创建一个包含所需方法的新的请求,并检查是否找到了操作。 Finally
块恢复上下文中的旧 routeData
,因为 _apiSelector.SelectAction
调用可能会更改它。
private class ActionSelector
{
private readonly HttpRequestMessage _request;
private readonly HttpControllerContext _context;
private readonly ApiControllerActionSelector _apiSelector;
private static readonly string[] Methods =
{ "GET", "PUT", "POST", "PATCH", "DELETE", "HEAD", "TRACE" };
public ActionSelector(HttpRequestMessage request)
{
try
{
var configuration = request.GetConfiguration();
var requestContext = request.GetRequestContext();
var controllerDescriptor = new DefaultHttpControllerSelector(configuration)
.SelectController(request);
_context = new HttpControllerContext
{
Request = request,
RequestContext = requestContext,
Configuration = configuration,
ControllerDescriptor = controllerDescriptor
};
}
catch
{
return;
}
_request = _context.Request;
_apiSelector = new ApiControllerActionSelector();
}
public IEnumerable<string> GetSupportedMethods()
{
return _request == null ? null : Methods.Where(IsMethodSupported);
}
private bool IsMethodSupported(string method)
{
_context.Request = new HttpRequestMessage(new HttpMethod(method), _request.RequestUri);
var routeData = _context.RouteData;
try
{
return _apiSelector.SelectAction(_context) != null;
}
catch
{
return false;
}
finally
{
_context.RouteData = routeData;
}
}
}
最后一步是将我们的消息处理程序添加到启动代码中的配置中
configuration.MessageHandlers.Add(new OptionsHandler());
使用这种方法,REST 服务将为每个有效的 URI 创建 OPTIONS
响应。
不要忘记指定参数类型。 如果您使用属性路由,请在 Route
属性中指定类型
[Route("Books/{id:int}", Name = "GetBook")]
如果没有隐式指定,ActionSelector
将认为路径“Books/abcd
”是有效的。
但这仍然不是一个好的解决方案。 首先,处理程序没有考虑授权。 其次,即使 URI 包含错误的资源 ID,例如 /books/0,服务也会响应请求。 这大大降低了使用 OPTIONS
方法的实际优势。
如果我们需要资源的授权,我们必须为包含资源 ID 的每个 URI 添加控制器操作 OPTIONS
,因为只有控制器才能授权资源。 此外,如果存在任何操作响应,我们的处理程序应该使用该响应。
最终实现
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
return await base.SendAsync(request, cancellationToken).ContinueWith(
task =>
{
var response = task.Result;
switch (GetResponseAction(request, response))
{
case ResponseAction.UseOriginal:
return response;
case ResponseAction.ReturnUnauthorized:
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
case ResponseAction.ReturnUnauthorized:
var methods = new ActionSelector(request).GetSupportedMethods();
if (methods == null)
return response;
response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(string.Empty)
};
response.Content.Headers.Add("Allow", methods);
response.Content.Headers.Add("Allow", "OPTIONS");
return response;
default:
throw new InvalidOperationException("Unsupported response action code");
}
}, cancellationToken);
}
private enum ResponseAction
{
UseOriginal,
RetrieveMethods,
ReturnUnauthorized
}
private static ResponseAction GetResponseAction(
HttpRequestMessage request, HttpResponseMessage response)
{
if (request.Method != HttpMethod.Options)
return ResponseAction.UseOriginal;
if (response.StatusCode != HttpStatusCode.MethodNotAllowed)
return ResponseAction.UseOriginal;
// IsAuthenticated() returns true if current user is authenticated
return IsAuthenticated() ? ResponseAction.RetrieveMethods : ResponseAction.ReturnUnauthorized;
}