如何使用Owin中间件拦截404

背景

首先让我解释一下背景。 我正在研究一个项目,该项目试图将使用通过IIS上托管的OWIN配置的Web API的后端服务器结合起来,但未来可能还有其他OWIN支持的主机 – 使用AngularJS的前端。

AngularJS前端完全是静态内容。 我完全避免服务器端技术,如MVC / Razor,WebForms,Bundles,任何与前端及其使用的资产有关的技术,而是推迟使用Node.js,Grunt / Gulp等最新最好的技术。处理CSS编译,捆绑,缩小等等。由于我不会进入这里的原因,我将前端和服务器项目保存在同一项目中的不同位置(而不是直接将它们全部放在Host项目中(参见raw下图)。

MyProject.sln server MyProject.Host MyProject.Host.csproj Startup.cs (etc.) frontend MyProjectApp app.js index.html MyProjectApp.njproj (etc.) 

所以就前端而言,我需要做的就是让我的主机服务我的静态内容。 在Express.js中,这是微不足道的。 使用OWIN,我能够使用Microsoft.Owin.StaticFiles中间件轻松地完成此操作,并且它运行良好(非常灵活)。

这是我的OwinStartup配置:

 string dir = AppDomain.CurrentDomain.RelativeSearchPath; // get executing path string contentPath = Path.GetFullPath(Path.Combine(dir, @"../../../frontend/MyProjectApp")); // resolve nearby frontend project directory app.UseFileServer(new FileServerOptions { EnableDefaultFiles = true, FileSystem = new PhysicalFileSystem(contentPath), RequestPath = new PathString(string.Empty) // starts at the root of the host }); // ensure the above occur before map handler to prevent native static content handler app.UseStageMarker(PipelineStage.MapHandler); 

捕获

基本上,它只是在frontend/MyProjectApp托管所有内容,就好像它位于MyProject.Host的根目录中一样。 很自然地,如果您请求不存在的文件,IIS会生成404错误。

现在,因为这是一个AngularJS应用程序,它支持html5mode ,我将在服务器上有一些不是物理文件的路由,但是在AngularJS应用程序中作为路由处理。 如果用户要放入AngularJS(除了index.html之外的任何内容或者实际存在的文件,在本例中),即使该路由在AngularJS应用程序中有效,我也会得到404。 因此,如果请求的文件不存在,我需要我的OWIN中间件返回index.html文件,并让我的AngularJS应用程序确定它是否真的是404。

如果您熟悉SPA和AngularJS,这是一种正常而直接的方法。 如果我使用的是MVC或ASP.NET路由,我可以设置默认路由到MVC控制器,该控制器返回我的index.html ,或者沿着这些行。 但是,我已经说过我没有使用MVC而且我试图保持这个尽可能简单轻便。

这个用户有类似的困境,并通过IIS重写解决了它。 在我的情况下,它不起作用,因为a)我的内容实际上不存在重写URL模块可以找到它,因此它总是返回index.html和b)我想要的东西不依赖于IIS,但在OWIN中间件中处理,因此可以灵活使用。

TL; DNR我,大声喊叫。

很简单,如何拦截404 Not Found并使用OWIN中间件返回我的FileServer -served index.html的内容(注意: 重定向)?

如果你正在使用OWIN,你应该能够使用它:

 using AppFunc = Func< IDictionary, // Environment Task>; // Done public static class AngularServerExtension { public static IAppBuilder UseAngularServer(this IAppBuilder builder, string rootPath, string entryPath) { var options = new AngularServerOptions() { FileServerOptions = new FileServerOptions() { EnableDirectoryBrowsing = false, FileSystem = new PhysicalFileSystem(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath)) }, EntryPath = new PathString(entryPath) }; builder.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions); return builder.Use(new Func(next => new AngularServerMiddleware(next, options).Invoke)); } } public class AngularServerOptions { public FileServerOptions FileServerOptions { get; set; } public PathString EntryPath { get; set; } public bool Html5Mode { get { return EntryPath.HasValue; } } public AngularServerOptions() { FileServerOptions = new FileServerOptions(); EntryPath = PathString.Empty; } } public class AngularServerMiddleware { private readonly AngularServerOptions _options; private readonly AppFunc _next; private readonly StaticFileMiddleware _innerMiddleware; public AngularServerMiddleware(AppFunc next, AngularServerOptions options) { _next = next; _options = options; _innerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions); } public async Task Invoke(IDictionary arg) { await _innerMiddleware.Invoke(arg); // route to root path if the status code is 404 // and need support angular html5mode if ((int)arg["owin.ResponseStatusCode"] == 404 && _options.Html5Mode) { arg["owin.RequestPath"] = _options.EntryPath.Value; await _innerMiddleware.Invoke(arg); } } } 

Javier Figueroa提供的解决方案真正适用于我的项目。 我的程序的后端是一个OWIN自托管的Web服务器,我使用AngularJS并启用了html5Mode作为前端。 我尝试了很多不同的编写IOwinContext中间件的方法,直到找到这个中间件都没有工作,它终于有效了! 感谢您分享此解决方案。

解决方案由Javier Figueroa提供

顺便说一句,以下是我在我的OWIN启动类中应用AngularServerExtension的方法:

  // declare the use of UseAngularServer extention // "/" <= the rootPath // "/index.html" <= the entryPath app.UseAngularServer("/", "/index.html"); // Setting OWIN based web root directory app.UseFileServer(new FileServerOptions() { RequestPath = PathString.Empty, FileSystem = new PhysicalFileSystem(@staticFilesDir), // point to the root directory of my web server }); 

我写了这个小中间件组件,但我不知道它是否过度,低效,或者是否存在其他陷阱。 基本上它只需要FileServerMiddleware使用的相同FileServerOptions ,最重要的部分是我们正在使用的FileSystem 。 它位于上述中间件之前,并快速检查所请求的路径是否存在。 如果没有,请求路径将被重写为“index.html”,正常的StaticFileMiddleware将从那里接管。

显然它可以被清理以便重复使用,包括为不同的根路径定义不同的默认文件的方法(例如,从“/ feature1”请求的任何东西都应该使用“/feature1/index.html”,同样使用“/ feature2“和”/feature2/default.html“等)。

但就目前而言,这对我有用。 显然,这依赖于Microsoft.Owin.StaticFiles。

 public class DefaultFileRewriterMiddleware : OwinMiddleware { private readonly FileServerOptions _options; ///  /// Instantiates the middleware with an optional pointer to the next component. ///  ///  ///  public DefaultFileRewriterMiddleware(OwinMiddleware next, FileServerOptions options) : base(next) { _options = options; } #region Overrides of OwinMiddleware ///  /// Process an individual request. ///  ///  ///  public override async Task Invoke(IOwinContext context) { IFileInfo fileInfo; PathString subpath; if (!TryMatchPath(context, _options.RequestPath, false, out subpath) || !_options.FileSystem.TryGetFileInfo(subpath.Value, out fileInfo)) { context.Request.Path = new PathString(_options.RequestPath + "/index.html"); } await Next.Invoke(context); } #endregion internal static bool PathEndsInSlash(PathString path) { return path.Value.EndsWith("/", StringComparison.Ordinal); } internal static bool TryMatchPath(IOwinContext context, PathString matchUrl, bool forDirectory, out PathString subpath) { var path = context.Request.Path; if (forDirectory && !PathEndsInSlash(path)) { path += new PathString("/"); } if (path.StartsWithSegments(matchUrl, out subpath)) { return true; } return false; } } 

Javier Figueroa给出的答案在这里有效且非常有用! 感谢那! 但是,它有一个奇怪的行为:只要不存在(包括条目文件),它就会运行next管道两次。 例如,当我通过UseHtml5Mode应用该实现时,下面的测试失败:

 [Test] public async Task ShouldRunNextMiddlewareOnceWhenNothingExists() { // ARRANGE int hitCount = 0; var server = TestServer.Create(app => { app.UseHtml5Mode("test-resources", "/does-not-exist.html"); app.UseCountingMiddleware(() => { hitCount++; }); }); using (server) { // ACT await server.HttpClient.GetAsync("/does-not-exist.html"); // ASSERT Assert.AreEqual(1, hitCount); } } 

关于我的上述测试的一些注释,如果有人有兴趣:

  • 它使用Microsoft.Owin.Testing 。
  • 测试框架是NUnit 。
  • UseCountingMiddleware实现可在此处获得 。

我使用的实现使上述测试通过如下:

 namespace Foo { using AppFunc = Func, Task>; public class Html5ModeMiddleware { private readonly Html5ModeOptions m_Options; private readonly StaticFileMiddleware m_InnerMiddleware; private readonly StaticFileMiddleware m_EntryPointAwareInnerMiddleware; public Html5ModeMiddleware(AppFunc next, Html5ModeOptions options) { if (next == null) throw new ArgumentNullException(nameof(next)); if (options == null) throw new ArgumentNullException(nameof(options)); m_Options = options; m_InnerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions); m_EntryPointAwareInnerMiddleware = new StaticFileMiddleware((environment) => { var context = new OwinContext(environment); context.Request.Path = m_Options.EntryPath; return m_InnerMiddleware.Invoke(environment); }, options.FileServerOptions.StaticFileOptions); } public Task Invoke(IDictionary environment) => m_EntryPointAwareInnerMiddleware.Invoke(environment); } } 

扩展非常相似:

 namespace Owin { using AppFunc = Func, Task>; public static class AppBuilderExtensions { public static IAppBuilder UseHtml5Mode(this IAppBuilder app, string rootPath, string entryPath) { if (app == null) throw new ArgumentNullException(nameof(app)); if (rootPath == null) throw new ArgumentNullException(nameof(rootPath)); if (entryPath == null) throw new ArgumentNullException(nameof(entryPath)); var options = new Html5ModeOptions { EntryPath = new PathString(entryPath), FileServerOptions = new FileServerOptions() { EnableDirectoryBrowsing = false, FileSystem = new PhysicalFileSystem(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath)) } }; app.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions); return app.Use(new Func(next => new Html5ModeMiddleware(next, options).Invoke)); } } }