如何使用 Owin 中间件拦截 404
本文关键字:中间件 何使用 Owin | 更新日期: 2023-09-27 18:31:04
Background
首先让我解释一下背景。我正在从事一个项目,该项目试图将使用通过 OWIN 配置的 Web API 的后端服务器(现在托管在 IIS 上,但将来可能会使用其他 OWIN 支持的主机)与使用 AngularJS 的前端相结合。
AngularJS前端完全是静态内容。我完全避免使用服务器端技术,如MVC/Razor,WebForms,Bundles,任何与前端及其使用的资产有关的东西,而是遵循使用Node.js,Grunt/Gulp等的最新和最好的技术来处理CSS编译,捆绑,缩小等。出于我不会在这里讨论的原因,我将前端和服务器项目保留在同一项目中的不同位置(而不是将它们全部直接粘贴在 Host 项目中(请参阅下面的粗略图表)。
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
或物理存在的文件以外的任何东西,在这个例子中),我会得到一个404,即使该路由在AngularJS应用程序中可能是有效的。因此,我需要我的 OWIN 中间件在请求的文件不存在的情况下返回index.html
文件,并让我的 AngularJS 应用程序确定它是否真的是 404。
如果您熟悉SPA和AngularJS,这是一种正常且直接的方法。如果我使用 MVC 或 ASP.NET 路由,我可以将默认路由设置为返回我的 index.html
的 MVC 控制器,或类似的东西。但是,我已经说过我不使用 MVC,我试图保持它尽可能简单和轻量级。
该用户也有类似的困境,并通过IIS重写解决了它。就我而言,它不起作用,因为 a) 我的内容在重写 URL 模块可以找到它的地方不存在,所以它总是返回 index.html
b) 我想要一些不依赖 IIS 的东西,而是在 OWIN 中间件中处理,以便可以灵活使用。
TL;DNR我,因为大声哭泣。
很简单,如何使用 OWIN 中间件拦截 404 未找到并返回(注意:不重定向)我的FileServer
服务index.html
的内容?
如果您使用的是 OWIN,您应该能够使用它:
using AppFunc = Func<
IDictionary<string, object>, // 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<AppFunc, AppFunc>(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<string, object> 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);
}
}
}
哈维尔·菲格罗亚提供的解决方案确实适用于我的项目。我的程序的后端是一个OWIN自托管Web服务器,我使用启用了html5Mode的AngularJS作为前端。我尝试了许多不同的方法来编写IOwinContext中间件,但没有一种工作,直到我找到这个,它终于起作用了!感谢您分享此解决方案。
哈维尔·菲格罗亚提供的解决方案
顺便说一下,以下是我如何在我的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",普通的静态文件中间件将从那里接管。
显然,它可以被清理以供重用,包括为不同的根路径定义不同的默认文件的方法(例如,从"/feature1"请求的任何缺失都应该使用"/feature1/index.html",同样使用"/feature2"和"/feature2/default.html"等)。
但就目前而言,这对我有用。显然,这依赖于Microsoft.Owin.StaticFiles。
public class DefaultFileRewriterMiddleware : OwinMiddleware
{
private readonly FileServerOptions _options;
/// <summary>
/// Instantiates the middleware with an optional pointer to the next component.
/// </summary>
/// <param name="next"/>
/// <param name="options"></param>
public DefaultFileRewriterMiddleware(OwinMiddleware next, FileServerOptions options) : base(next)
{
_options = options;
}
#region Overrides of OwinMiddleware
/// <summary>
/// Process an individual request.
/// </summary>
/// <param name="context"/>
/// <returns/>
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;
}
}
哈维尔·菲格罗亚给出的答案在这里有效,真的很有帮助!谢谢!但是,它有一个奇怪的行为:只要不存在任何内容(包括入口文件),它就会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<IDictionary<string, object>, 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<string, object> environment) =>
m_EntryPointAwareInnerMiddleware.Invoke(environment);
}
}
扩展非常相似:
namespace Owin
{
using AppFunc = Func<IDictionary<string, object>, 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<AppFunc, AppFunc>(next => new Html5ModeMiddleware(next, options).Invoke));
}
}
}