65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.35/5 (8投票s)

2017年7月21日

CPOL

2分钟阅读

viewsIcon

39127

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;
}
© . All rights reserved.