简体   繁体   English

如何使用Owin中间件拦截404

[英]How to intercept 404 using Owin middleware

Background 背景

First let me explain the background. 首先让我解释一下背景。 I am working on a project that attempts to marry a backend server that uses Web API configured via OWIN- hosted on IIS now, but potentially other OWIN-supported hosts in the future- to a frontend using AngularJS. 我正在研究一个项目,该项目试图将使用通过IIS上托管的OWIN配置的Web API的后端服务器结合起来,但未来可能还有其他OWIN支持的主机 - 使用AngularJS的前端。

The AngularJS frontend is entirely static content. AngularJS前端完全是静态内容。 I completely avoid server-side technologies such as MVC/Razor, WebForms, Bundles, anything that has to do with the frontend and the assets it uses, and defer instead to the latest and greatest techniques using Node.js, Grunt/Gulp, etc. to handle CSS compilation, bundling, minification, etc. For reasons I won't go into here, I keep the frontend and server projects in separate locations within the same project (rather than stick them all in the Host project directly (see crude diagram below). 我完全避免服务器端技术,如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.)

So as far as the frontend is concerned, all I need to do is get my Host to serve my static content. 因此就前端而言,我需要做的就是让我的主机服务我的静态内容。 In Express.js, this is trivial. 在Express.js中,这是微不足道的。 With OWIN, I was able to do this easily using Microsoft.Owin.StaticFiles middleware, and it works great (it's very slick). 使用OWIN,我能够使用Microsoft.Owin.StaticFiles中间件轻松完成这项工作,并且它运行良好(非常灵活)。

Here is my OwinStartup configuration: 这是我的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);

The Catch 捕获

Basically, it just hosts everything in frontend/MyProjectApp as if it were right inside the root of MyProject.Host. 基本上,它只是在frontend/MyProjectApp托管所有内容,就好像它位于MyProject.Host的根目录中一样。 So naturally, if you request a file that doesn't exist, IIS generates a 404 error. 很自然地,如果您请求不存在的文件,IIS会生成404错误。

Now, because this is an AngularJS app, and it supports html5mode , I will have some routes that aren't physical files on the server, but are handled as routes in the AngularJS app. 现在,因为这是一个AngularJS应用程序,它支持html5mode ,我将在服务器上有一些不是物理文件的路由,但是在AngularJS应用程序中作为路由处理。 If a user were to drop onto an AngularJS (anything other than index.html or a file that physically exists, in this example), I would get a 404 even though that route might be valid in the AngularJS app. 如果用户要放入AngularJS(除了index.html之外的任何东西或者实际存在的文件,在本例中),即使该路由在AngularJS应用程序中有效,我也会得到404。 Therefore, I need my OWIN middleware to return the index.html file in the event a requested file does not exist, and let my AngularJS app figure out if it really is a 404. 因此,我需要我的OWIN中间件在所请求的文件不存在的情况下返回index.html文件,让我的AngularJS应用程序确定它是否真的是404。

If you're familiar with SPAs and AngularJS, this is a normal and straight-forward approach. 如果您熟悉SPA和AngularJS,这是一种正常而直接的方法。 If I were using MVC or ASP.NET routing, I could just set the default route to an MVC controller that returns my index.html , or something along those lines. 如果我使用的是MVC或ASP.NET路由,我可以将默认路由设置为返回我的index.html的MVC控制器,或者沿着这些行。 However, I've already stated I'm not using MVC and I'm trying to keep this as simple and lightweight as possible. 但是,我已经说过我没有使用MVC,我试图尽量保持简单和轻量级。

This user had a similar dilemma and solved it with IIS rewriting. 这个用户有类似的困境,并通过IIS重写解决了它。 In my case, it doesn't work because a) my content doesn't physically exist where the rewrite URL module can find it, so it always returns index.html and b) I want something that doesn't rely on IIS, but is handled within OWIN middleware so it can be used flexibly. 在我的情况下,它不起作用,因为a)我的内容实际上不存在重写URL模块可以找到它,因此它总是返回index.html和b)我想要的东西不依赖于IIS,但在OWIN中间件中处理,因此可以灵活使用。

TL;DNR me, for crying out loud. TL; DNR我,大声喊叫。

Simple, how can I intercept a 404 Not Found and return the content of (note: not redirect) my FileServer -served index.html using OWIN middleware? 很简单,如何拦截404 Not Found并使用OWIN中间件返回我的FileServer -served index.html的内容(注意: 重定向)?

If you're using OWIN, you should be able to use this: 如果你正在使用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);
        }
    }
}

The solution that Javier Figueroa provided really works for my project. Javier Figueroa提供的解决方案真正适用于我的项目。 The back end of my program is an OWIN self-hosted webserver, and I use AngularJS with html5Mode enabled as the front end. 我的程序的后端是一个OWIN自托管网络服务器,我使用AngularJS并启用了html5Mode作为前端。 I tried many different ways writing a IOwinContext middleware and none of them works till I found this one, it finally works! 我尝试了很多不同的方法来编写IOwinContext中间件,但在找到这个中间件之前它们都没有工作,它终于有效了! Thanks for sharing this solution. 感谢您分享此解决方案。

solution provided by Javier Figueroa 解决方案由Javier Figueroa提供

By the way, the following is how I apply that AngularServerExtension in my OWIN startup class: 顺便说一句,以下是我在我的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
        });

I wrote this little middleware component, but I don't know if it's overkill, inefficient, or if there are other pitfalls. 我写了这个小中间件组件,但我不知道它是否过度,低效,或者是否存在其他陷阱。 Basically it just takes in the same FileServerOptions the FileServerMiddleware uses, the most important part being the FileSystem we're using. 基本上它只需要FileServerMiddleware使用的相同FileServerOptions ,最重要的部分是我们正在使用的FileSystem It is placed before the aforementioned middleware and does a quick check to see if the requested path exists. 它位于上述中间件之前,并快速检查所请求的路径是否存在。 If not, the request path is rewritten as "index.html", and the normal StaticFileMiddleware will take over from there. 如果没有,请求路径将被重写为“index.html”,正常的StaticFileMiddleware将从那里接管。

Obviously it could stand to be cleaned up for reuse, including a way to define different default files for different root paths (eg anything requested from "/feature1" that is missing should use "/feature1/index.html", likewise with "/feature2" and "/feature2/default.html", etc.). 显然它可以被清理以便重复使用,包括为不同的根路径定义不同的默认文件的方法(例如,从“/ feature1”请求的任何东西都应该使用“/feature1/index.html”,同样使用“/ feature2“和”/feature2/default.html“等)。

But for now, it this works for me. 但就目前而言,这对我有用。 This has a dependency on Microsoft.Owin.StaticFiles, obviously. 显然,这依赖于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;
    }
}

Answer given by Javier Figueroa here works and really helpful! Javier Figueroa给出的答案在这里起作用并且非常有用! Thanks for that! 感谢那! However, it has one odd behavior: whenever nothing exists (including the entry file), it runs next pipeline twice. 但是,它有一个奇怪的行为:只要不存在(包括条目文件),它就会运行next管道两次。 For example, below test fails when I apply that implementation through UseHtml5Mode : 例如,当我通过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);
    }
}

A few notes about my above test if anyone is intrested: 关于我的上述测试的一些注释,如果有人有兴趣:

The implementation I went with which makes the above test pass is as below: 我使用的实现使上述测试通过如下:

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);
    }
}

The extension is pretty similar: 扩展非常相似:

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));
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何安全地拦截自定义Owin中间件中的响应流 - How can I safely intercept the Response stream in a custom Owin Middleware Owin Middleware在服务器上产生404 - Owin Middleware results in 404 on server 如何在OWIN中删除中间件? - How to remove middleware in OWIN? 如何使用OWIN安全中间件在应用程序中设置NameClaimType - How to set the NameClaimType in an application using OWIN security middleware 如何使用OWIN中间件组件检查MVC响应流? - How to inspect MVC response stream using OWIN middleware component? 如何在OWIN中间件中捕获SecurityTokenExpiredException? - How to capture a SecurityTokenExpiredException in OWIN middleware? 使用Ninject OWIN中间件在OWIN启动中注入UserStore的依赖关系 - Dependency injecting UserStore in OWIN startup using Ninject OWIN middleware 如何从OWIN中间件重定向用户? - How to redirect user from OWIN middleware? 如何使用OWIN中间件在Web Api中实现OAuth2 Authorization_Code流? - How do I implement an OAuth2 Authorization_Code Flow in Web Api using OWIN Middleware? 使用accesstoken / AuthenticationResult之类的UseCookieAuthentication()手动设置cookie中间件 - Manual set cookies using accesstoken/AuthenticationResult -like UseCookieAuthentication() owin middleware
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM