OAuthBearerAuthenticationMiddleware - 服务器无法在发送 HTTP header 后附加 header

OAuthBearerAuthenticationMiddleware - Server cannot append header after HTTP headers have been sent

我一直在尝试将一些 OWIN 中间件插入现有的 WebApi 项目。我的 start-up 最初只包含以下几行:

application.UseOAuthBearerAuthentication(newOAuthBearerAuthenticationOptions());
application.UseWebApi(config);

使用此配置,我间歇性地(主要是在 iisreset 之后)收到格式错误的响应(由 fiddler 识别),这些响应是由中间件尝试添加 headers 但在响应已发送之后造成的,这被报告为异常:

Server cannot append header after HTTP headers have been sent.

我重新编译 Microsoft.Owin.Security.OAuth 以添加一些额外的跟踪以显示事情发生的顺序,我得到以下输出:

Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware Information: 0 : : OAUTH: Authenticating...
System.Web.Http.Request: ;;http://localhost:555/entityinstance/member    
System.Web.Http.Controllers: WebHostHttpControllerTypeResolver;GetControllerTypes;: 
System.Web.Http.Controllers: WebHostHttpControllerTypeResolver;GetControllerTypes;: 
System.Web.Http.MessageHandlers: LogHandler;SendAsync;: 
System.Web.Http.Controllers: DefaultHttpControllerSelector;SelectController;Route='entityDefinitionName:member,controller:EntityInstance'
System.Web.Http.Controllers: DefaultHttpControllerSelector;SelectController;EntityInstance
System.Web.Http.Controllers: HttpControllerDescriptor;CreateController;: 
System.Web.Http.Controllers: WindsorCompositionRoot;Create;: 
System.Web.Http.Controllers: WindsorCompositionRoot;Create;Loyalty.Services.WebApi.Controllers.EntityInstanceController
System.Web.Http.Controllers: HttpControllerDescriptor;CreateController;Loyalty.Services.WebApi.Controllers.EntityInstanceController
System.Web.Http.Controllers: EntityInstanceController;ExecuteAsync;: 
System.Web.Http.Action: ApiControllerActionSelector;SelectAction;: 
System.Web.Http.Action: ApiControllerActionSelector;SelectAction;Selected action 'Get(String entityDefinitionName)'
System.Net.Http.Formatting: DefaultContentNegotiator;Negotiate;Type='HttpError', formatters=[JsonMediaTypeFormatterTracer, XmlMediaTypeFormatterTracer, FormUrlEncodedMediaTypeFormatterTracer, FormUrlEncodedMediaTypeFormatterTracer]
System.Net.Http.Formatting: JsonMediaTypeFormatter;GetPerRequestFormatterInstance;Obtaining formatter of type 'JsonMediaTypeFormatter' for type='HttpError', mediaType='application/json; charset=utf-8'
System.Net.Http.Formatting: JsonMediaTypeFormatter;GetPerRequestFormatterInstance;Will use same 'JsonMediaTypeFormatter' formatter
System.Net.Http.Formatting: DefaultContentNegotiator;Negotiate;Selected formatter='JsonMediaTypeFormatter', content-type='application/json; charset=utf-8'
System.Web.Http.Controllers: EntityInstanceController;ExecuteAsync;: 
System.Net.Http.Formatting: JsonMediaTypeFormatter;WriteToStreamAsync;Value='System.Web.Http.HttpError', type='HttpError', content-type='application/json; charset=utf-8'
System.Net.Http.Formatting: JsonMediaTypeFormatter;WriteToStreamAsync;: 
System.Web.Http.Request: ;;Content-type='application/json; charset=utf-8', content-length=68
System.Web.Http.Controllers: EntityInstanceController;Dispose;: 
System.Web.Http.Controllers: EntityInstanceController;Dispose;: 
Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware Information: 0 : : OAUTH: Applying Challange...
A first chance exception of type 'System.Web.HttpException' occurred in System.Web.dll

所以看起来是中间件的响应处理部分,按照俄罗斯套娃模型试图修改headers,但是响应已经在前一阶段完成了。我尝试添加不同的阶段标记来控制这种行为,但似乎没有任何帮助。

看到那条痕迹后,令人惊讶的是,它并不是一直都在发生。我用一个更小的实现编写了我自己的这个中间件版本,在注册这个之后,代替了 MS 版本,我确实开始在每个请求上看到错误,我想知道它是否总是被抛出,但被吞下了有时或者 fiddler 是否没有等待足够长的时间来看到它。

我目前的最佳猜测是,由于对 Owin 使用与 WebApi 设置不同的 HttpConfiguration,导致出现此问题。不幸的是,我无法换出整个 HTTPApplication 并转到 OWIN 锁定库存,因为在 OWIN 的不同上下文中委派处理程序 运行 的方式,您无权访问路由数据,如这会破坏我们现有的很多基础设施。

任何人都可以给我任何关于这里发生的事情的指示,这是受支持的场景吗?我是否漏掉了一些明显的东西?

好的,好的,我已经解决了!

简答

如果您使用两个 HttpConfiguration 实例,就会遇到此问题。您必须确保对 application.UseWebApi(config); 的调用和 Webapi 配置使用相同的 HttpConfiguration。

我猜这是因为除非您使用相同的配置,否则 运行time 无法知道响应何时准备好发送,因为没有它可以查看一个地方以确定您的所有处理程序是否具有 运行.

中等大小的答案

在转换现有的 Webapi 应用程序时,您通常会在 global.asax [=20= 中注册容器 bootstrap 和 Web api 配置注册] 处理程序。迁移到 OWIN 时,您要做的第一件事就是添加一个 Startup class,您可以使用它通过 appbuilder 配置您的 OWIN 应用程序。在这种情况下,很容易遵循纯 OWIN 示例并在 startup 中新建一个新的 HttpConfiguration,同时保留使用 GlobalConfiguration 的现有注册。你最终会得到类似的东西:

Global.asax:

    protected void Application_Start()
    {
        var config =  GlobalConfiguration.Configuration;
        Bootstrapper.Run(config);            
        WebApiConfig.Register(config);
     }

Startup.cs:

  public void Configuration(IAppBuilder application)
            {
                var config = new HttpConfiguration();
                application.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions());
                application.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

                config.MapHttpAttributeRoutes();
                application.UseWebApi(config);   
            }

当你真正需要的是:

public void Configuration(IAppBuilder application)
        {
            var config = new HttpConfiguration();

            Bootstrapper.Run(config);

            application.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions());
            application.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

            WebApiConfig.Register(config);
            config.MapHttpAttributeRoutes();
            application.UseWebApi(config);   
        }

更长的答案

您可能会认为这很明显,如果您密切关注这些示例,您会很快弄清楚这一点,您是对的。这是我在遇到问题后不久尝试的(我没有编写原始代码;))。不幸的是,如果您尝试使用 WebApi 项目中使用的一些标准 web.configs 进行上述操作,您将 运行 陷入许多其他问题,这些问题会出现您认为可能与您的原始问题相关的问题,但并非如此't.

问题:每次请求404。

解决方案:您需要注册以下处理程序:

 <!-- language: lang-xml -->
  <handlers accessPolicy="Read, Execute, Script">
    <remove name="WebDAV" />    
    <remove name="ExtensionlessUrlHandler-Integrated-4.0" />      
    <remove name="TRACEVerbHandler" />
    <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*" verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

问题: 发送响应后无法重定向/发出 401 后 404 重定向到 login.aspx .

解决方案:您需要注销formsAuthentication模块:

<!-- language: lang-xml -->
<modules runAllManagedModulesForAllRequests="true">
      <remove name="FormsAuthentication" />
</modules>

问题: "message":"No HTTP resource was found that matches the request URI 'http://blah'.", "messageDetail": "No type was found that matches the controller named 'blah'."

解决方法:这是一个微妙的方法。我们使用委托处理程序来扫描我们的控制器操作以查找 Authenticate 属性。我们使用以下代码执行此操作:

public virtual T ScanForAttribute<T>(HttpRequestMessage request, HttpConfiguration config) where T : Attribute
        {
            var controllerSelector = new DefaultHttpControllerSelector(config);
            var descriptor = controllerSelector.SelectController(request);

            .. some other stuff
        }

现在,我们在 OWIN 下遇到的问题是 controllerSelector.SelectController(在 System.Web.Http 中实现)内部依赖于 MS_RequestContext 请求 属性,如果它没有找到它然后它会抛出状态代码为 404 的 HttpResponseException,这会导致发送 404 响应,因此会出现上述问题。您可以通过一些技巧在 OWIN 中使用它:

public virtual T ScanForAttribute<T>(HttpRequestMessage request, HttpConfiguration config) where T : Attribute
        {
            var data = request.GetConfiguration().Routes.GetRouteData(request);
            ((HttpRequestContext) request.Properties["MS_RequestContext"]).RouteData = data;

            var controllerSelector = new DefaultHttpControllerSelector(config);
            var descriptor = controllerSelector.SelectController(request);
            .. Some other stuff
        }

问题: 您尝试使用以下方式解析呼叫者 IP:

if (request.Properties.ContainsKey("MS_HttpContext"))
                {
                    return ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
                }

解决方案:您还需要检查以下内容,以使其在 OWIN 下工作:

  if (request.Properties.ContainsKey("MS_OwinContext"))
                {
                    OwinContext owinContext = (OwinContext)request.Properties["MS_OwinContext"];
                    if (owinContext != null)
                    {
                        return owinContext.Request.RemoteIpAddress;
                    }
                } 

而且....我们现在工作得很好!如果 OWIN 有更多的跟踪输出,解决这个问题会容易得多,但不幸的是,katana 项目中的很多中间件在跟踪方面有点安静,希望随着时间的推移,这会得到解决!

希望这对您有所帮助。