StaticFileMiddleware 解析
说明:由于部分产品没有静态资源的管理,我突然想到能不能用现有的静态文件中间件的功能调整一下实现多组织件上传文件的隔离呢?那第一步先看懂 StaticFileMiddleware做了什么吧。
PS:本文不解释中间件原理。
第一步 方法源码
using System;using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.StaticFiles
{
/// <summary>
/// 这个就是静态文件中间件的核心代码
/// </summary>
public class StaticFileMiddleware
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="next">The next middleware in the pipeline.</param>
/// <param name="hostingEnv">The used by this middleware.</param>
/// <param name="options">The configuration options.</param>
/// <param name="loggerFactory">An instance used to create loggers.</param>
public StaticFileMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory)
{
if (next == null)
{
throw new ArgumentNullException("next");
}
if (hostingEnv == null)
{
throw new ArgumentNullException("hostingEnv");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
if (loggerFactory == null)
{
throw new ArgumentNullException("loggerFactory");
}
this._next = next;
this._options = options.Value;
this._contentTypeProvider = (options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider());
this._fileProvider = (this._options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv));
this._matchUrl = this._options.RequestPath;
this._logger = LoggerFactoryExtensions.CreateLogger<StaticFileMiddleware>(loggerFactory);
} /// <summary>
/// 这里才是静态文件处理的业务方法
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
StaticFileContext fileContext = new StaticFileContext(context, this._options, this._matchUrl, this._logger, this._fileProvider, this._contentTypeProvider);
//验证请求方法
if (!fileContext.ValidateMethod())
{
this._logger.LogRequestMethodNotSupported(context.Request.Method);
}
//验证路径
else if (!fileContext.ValidatePath())
{
this._logger.LogPathMismatch(fileContext.SubPath);
}
//验证文件类型系统是否识别
else if (!fileContext.LookupContentType())
{
this._logger.LogFileTypeNotSupported(fileContext.SubPath);
}
else
{
//尝试读取文件信息
if (fileContext.LookupFileInfo())
{
//
fileContext.ComprehendRequestHeaders();
switch (fileContext.GetPreconditionState())
{
//访问成功了
case StaticFileContext.PreconditionState.Unspecified:
case StaticFileContext.PreconditionState.ShouldProcess:
//如果是head请求,直接范围200,不返回请求体
if (fileContext.IsHeadMethod)
{
await fileContext.SendStatusAsync();
return;
}
try
{
//请求包含Range
if (fileContext.IsRangeRequest)
{
await fileContext.SendRangeAsync();
return;
}
//这个默认无参的时候是200
await fileContext.SendAsync();
this._logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath);
return;
}
catch (FileNotFoundException)
{
ResponseExtensions.Clear(context.Response);
goto IL_3E2;
}
break;
//请求没有变更,这之后跳出switch,反回304
case StaticFileContext.PreconditionState.NotModified:
break;
case StaticFileContext.PreconditionState.PreconditionFailed:
this._logger.LogPreconditionFailed(fileContext.SubPath);
//请求不匹配
await fileContext.SendStatusAsync();
return;
default:
throw new NotImplementedException(fileContext.GetPreconditionState().ToString());
}
this._logger.LogPathNotModified(fileContext.SubPath);
await fileContext.SendStatusAsync();
return;
}
this._logger.LogFileNotFound(fileContext.SubPath);
}
//不符合静态资源的要求,走下一个中间件(通常是MVC中间件)
IL_3E2:
await this._next.Invoke(context);
} // Token: 0x04000048 RID: 72
private readonly StaticFileOptions _options; // Token: 0x04000049 RID: 73
private readonly PathString _matchUrl; // Token: 0x0400004A RID: 74
private readonly RequestDelegate _next; // Token: 0x0400004B RID: 75
private readonly ILogger _logger; // Token: 0x0400004C RID: 76
private readonly IFileProvider _fileProvider; // Token: 0x0400004D RID: 77
private readonly IContentTypeProvider _contentTypeProvider;
}
}
第二步 方法解析
1 ValidateMethod 验证请求方式
public bool ValidateMethod()
{
this._method = this._request.Method;
this._isGet = HttpMethods.IsGet(this._method);
this._isHead = HttpMethods.IsHead(this._method);
return this._isGet || this._isHead;
}
这个函数从字面上看就是判断当前静态资源的请求方式是不是get 和head,只要是其中一种就是可以的。
get请求方式就不说吗了,head请求方式的解释如下:
HEAD方法与GET方法的行为很类似,但服务器在响应中只返回首部。不会反回实体的主体部分。这就允许客户端在未获取实际资源的情况下,对资源的首部进行检查。一般应用于在不获取资源的情况下了解资源情况。
2 ValidatePath 验证路径
public bool ValidatePath()
{
return Helpers.TryMatchPath(this._context, this._matchUrl, false, out this._subPath);
} internal static bool TryMatchPath(HttpContext context, PathString matchUrl, bool forDirectory, out PathString subpath)
{
PathString pathString = context.Request.Path;
if (forDirectory && !Helpers.PathEndsInSlash(pathString))
{
pathString += new PathString("/");
}
return pathString.StartsWithSegments(matchUrl, ref subpath);
} internal static bool PathEndsInSlash(PathString path)
{
return path.Value.EndsWith("/", StringComparison.Ordinal);
}
这个验证其实就是验证请求文件的路径前缀是否和你系统设置的是否一致。matchUrl这个参数可以在Startup.cs中在静态文件中间件中作为参数传递进去。如图所示

如上所述,现在系统只能请求/a开头的文件了(路径开头,不是文件名称开头)。
具体匹配逻辑可以看Path 的StartsWithSegments的实现。
3 LookupContentType 文件类型验证
if (this._contentTypeProvider.TryGetContentType(this._subPath.Value, out this._contentType))
{
return true;
}
if (this._options.ServeUnknownFileTypes)
{
this._contentType = this._options.DefaultContentType;
return true;
}
return false;
这个验证就是检验拓展名的。可以结合一下实例查看

首先判断请求文件的拓展名在FileExtensionContentTypeProvider中是否存在(FileExtensionContentTypeProvider原本就会有很多拓展类型,截图中我们认为的添加上了image和log的拓展格式)
系统先去判断拓展格式是否存在,如果存在则连带的把反馈类型也返回出来。
如果不存在就去判断是否设置了允许未知的拓展出。例如我有一个拓展名叫做 .alex,然后我们希望系统可以将其以txt的格式反馈,那么就需要如下操作:

4 LookupFileInfo 文件信息验证
public bool LookupFileInfo()
{
this._fileInfo = this._fileProvider.GetFileInfo(this._subPath.Value);
if (this._fileInfo.Exists)
{
this._length = this._fileInfo.Length;
DateTimeOffset lastModified = this._fileInfo.LastModified;
this._lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, lastModified.Offset).ToUniversalTime();
long value = this._lastModified.ToFileTime() ^ this._length;
this._etag = new EntityTagHeaderValue("\"" + Convert.ToString(value, ) + "\"");
}
return this._fileInfo.Exists;
}
这个验证首先是验证文件是否存在,之后会做一系列的计算操作。这一步会获取两个值
_lastModified:文件最后操作日期
_etag :等效算法,通过文件大小、最后操作日期来生成的一个数据值。上述两个值可以通过浏览器查看如下:


这两个值其实就是判断文件缓存的,具体分析可以参看以下文章:
https://www.cnblogs.com/softidea/p/5986339.html
5 ComprehendRequestHeaders 请求头验证
public void ComprehendRequestHeaders()
{
this.ComputeIfMatch();
this.ComputeIfModifiedSince();
this.ComputeRange();
this.ComputeIfRange();
}
这一段就用到了上一个验证的时候生成的_lastModified和_etag来计算缓存问题,故不再赘述了。逻辑代码如下:
private void ComputeIfMatch()
{
IList<EntityTagHeaderValue> ifMatch = this._requestHeaders.IfMatch;
if (ifMatch != null && ifMatch.Any<EntityTagHeaderValue>())
{
this._ifMatchState = StaticFileContext.PreconditionState.PreconditionFailed;
foreach (EntityTagHeaderValue entityTagHeaderValue in ifMatch)
{
if (entityTagHeaderValue.Equals(EntityTagHeaderValue.Any) || entityTagHeaderValue.Compare(this._etag, true))
{
this._ifMatchState = StaticFileContext.PreconditionState.ShouldProcess;
break;
}
}
}
IList<EntityTagHeaderValue> ifNoneMatch = this._requestHeaders.IfNoneMatch;
if (ifNoneMatch != null && ifNoneMatch.Any<EntityTagHeaderValue>())
{
this._ifNoneMatchState = StaticFileContext.PreconditionState.ShouldProcess;
foreach (EntityTagHeaderValue entityTagHeaderValue2 in ifNoneMatch)
{
if (entityTagHeaderValue2.Equals(EntityTagHeaderValue.Any) || entityTagHeaderValue2.Compare(this._etag, true))
{
this._ifNoneMatchState = StaticFileContext.PreconditionState.NotModified;
break;
}
}
}
} // Token: 0x0600006C RID: 108 RVA: 0x00004CE8 File Offset: 0x00002EE8
private void ComputeIfModifiedSince()
{
DateTimeOffset utcNow = DateTimeOffset.UtcNow;
DateTimeOffset? ifModifiedSince = this._requestHeaders.IfModifiedSince;
if (ifModifiedSince != null && ifModifiedSince <= utcNow)
{
this._ifModifiedSinceState = ((ifModifiedSince < this._lastModified) ? StaticFileContext.PreconditionState.ShouldProcess : StaticFileContext.PreconditionState.NotModified);
}
DateTimeOffset? ifUnmodifiedSince = this._requestHeaders.IfUnmodifiedSince;
if (ifUnmodifiedSince != null && ifUnmodifiedSince <= utcNow)
{
this._ifUnmodifiedSinceState = ((ifUnmodifiedSince >= this._lastModified) ? StaticFileContext.PreconditionState.ShouldProcess : StaticFileContext.PreconditionState.PreconditionFailed);
}
}
后面几个方法,this.ComputeRange();和this.ComputeIfRange();
可以参考一下文章:
https://blog.csdn.net/shuimuniao/article/details/8086438
总结
静态资源中间件的逻辑可以简单的归纳如下:
1、判断是否是静态资源请求方式(Head或者Get)
2、判断请求连接是否符合设置UrlMatch(RequestPath配置)
3、判断文件拓展名是否存在或者启动未知拓展名服务(DefaultContentType、ServeUnknownFileTypes)
4、判断文件是否存在,计算文件_lastModified和_etag
5、如果上述4点判断都失败了,则认为不是静态资源请求,走下一个中间件
6、根据请求头和请求资源的数据判断以下几种反馈请求
- 请求类型是Head ,返回 200 但是没有请求体
- 请求If-Range不匹配,反馈412
- 请求的缓存匹配,反馈 304
- 请求成功,反馈200及请求体。
StaticFileMiddleware 解析的更多相关文章
- ASP.NET Core应用针对静态文件请求的处理[3]: StaticFileMiddleware中间件如何处理针对文件请求
我们通过<以Web的形式发布静态文件>和<条件请求与区间请求>中的实例演示,以及上面针对条件请求和区间请求的介绍,从提供的功能和特性的角度对这个名为StaticFileMidd ...
- StaticFileMiddleware中间件如何处理针对文件请求
StaticFileMiddleware中间件如何处理针对文件请求 我们通过<以Web的形式发布静态文件>和<条件请求与区间请求>中的实例演示,以及上面针对条件请求和区间请求的 ...
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- .NET Core中的认证管理解析
.NET Core中的认证管理解析 0x00 问题来源 在新建.NET Core的Web项目时选择“使用个人用户账户”就可以创建一个带有用户和权限管理的项目,已经准备好了用户注册.登录等很多页面,也可 ...
- Html Agility Pack 解析Html
Hello 好久不见 哈哈,今天给大家分享一个解析Html的类库 Html Agility Pack.这个适用于想获取某网页里面的部分内容.今天就拿我的Csdn的博客列表来举例. 打开页面 用Fir ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
- 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新
上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...
- 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...
- Asp.Net WebApi核心对象解析(下篇)
在接着写Asp.Net WebApi核心对象解析(下篇)之前,还是一如既往的扯扯淡,元旦刚过,整个人还是处于晕的状态,一大早就来处理系统BUG,简直是坑爹(好在没让我元旦赶过来该BUG),队友挖的坑, ...
随机推荐
- 一文彻底搞懂 TCP三次握手、四次挥手过程及原理
原创文章出自公众号:「码农富哥」,欢迎收藏和关注,如转载请注明出处! TCP 协议简述 TCP 提供面向有连接的通信传输,面向有连接是指在传送数据之前必须先建立连接,数据传送完成后要释放连接. 无论哪 ...
- 珠峰-buffer-流事件
#### Buffer // 字符串的二进制转10进制 let r = parseInt('11111111', 2); console.log(r); // 打印 255 // Number类型转为 ...
- Java集合效率问题
凑算式 B DEFA + --- + ------- = 10 C GHI (如果显示有问题,可以参见[图1.jpg]) 这个算式中A~I代表1~9的数字,不同的字母代表不同的数字. 比如:6+8/3 ...
- if-else连用时的陷阱
近日,在实现<The C Programing Language>上的一个练习题时,写出了下面一段代码 ; i<=right; i++) { ) ) swap(v, i, ++la ...
- mybatis 通过配置父类数据源连接和关闭数据,进行junit单元测试
来源:https://blog.csdn.net/Bigbig_lyx/article/details/80646005 解决问题,单元测试没经过单独配置,每个测试方法中要添加配置数据源 一:配置父类 ...
- pikachu-服务器端请求伪造SSRF(Server-Side Request Forgery)
一.SSRF概述(部分内容来自pikachu平台) SSRF(Server-Side Request Forgery:服务器端请求伪造),其形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功 ...
- go实现java虚拟机01
前段时间看了一本书,说的是用go语言实现java虚拟机,很有意思,于是就花了一段时间学习了一下go语言,虽然对go的底层理解不是很深,但是写代码还是可以的,就当做个读书笔记吧! 链接在这里,另外还有一 ...
- java设计模式1——单例模式
java设计模式1--单例模式 1.单例模式介绍 1.1.核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点 1.2.常见场景 1.3.单例模式的优点 1.4.常见的五种单例模式实现 ...
- wifite硬核破解WiFi密码
如题 楼主在这里分享下如何使用工具破解附近的WiFi 今天使用的工具是 wifite 现在都有WiFi万能钥匙了 暴力破解还有市场吗? 首先他俩的破解思路就不一样 wifi万能钥匙是根据云端数据库内容 ...
- c# 异步编程总结
异步编程前提 1.学委托 delegate 其中委托中的beginInvoke()和endInvoke()方法必须要会. 2.学习回调函数 (也可以不用,但是一般建议用回调函数中执行endinvoke ...