如何使用Owin中间件拦截404

Hac*_*ese 14 c# asp.net static-files owin owin-middleware

背景

首先让我解释一下背景.我正在研究一个项目,该项目试图将使用通过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.)
Run Code Online (Sandbox Code Playgroud)

因此就前端而言,我需要做的就是让我的主机服务我的静态内容.在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);
Run Code Online (Sandbox Code Playgroud)

捕获

基本上,它只是将所有内容托管在frontend/MyProjectAppMyProject.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未找到并返回的内容(注:不是重定向)我FileServer-served index.html使用OWIN中间件?

Jav*_*roa 15

如果你正在使用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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


小智 5

Javier Figueroa提供的解决方案真正适用于我的项目.我的程序的后端是一个OWIN自托管网络服务器,我使用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
        });
Run Code Online (Sandbox Code Playgroud)


Hac*_*ese 2

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

显然它可以被清理以供重用,包括为不同根路径定义不同默认文件的方法(例如,从“/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;
    }
}
Run Code Online (Sandbox Code Playgroud)