ASP.NET Core静态文件处理源码探究

前言

    静态文件(如 HTML、CSS、图像和 JavaScript)等是Web程序的重要组成部分。传统的ASP.NET项目一般都是部署在IIS上,IIS是一个功能非常强大的服务器平台,可以直接处理接收到的静态文件处理而不需要经过应用程序池处理,所以很多情况下对于静态文件的处理程序本身是无感知的。ASP.NET Core则不同,作为Server的Kestrel服务是宿主到程序上的,由宿主运行程序启动Server然后可以监听请求,所以通过程序我们直接可以处理静态文件相关。静态文件默认存储到项目的wwwroot目录中,当然我们也可以自定义任意目录去处理静态文件。总之,在ASP.NET Core我们可以处理静态文件相关的请求。

StaticFile三剑客

    通常我们在说道静态文件相关的时候会涉及到三个话题分别是启用静态文件、默认静态页面、静态文件目录浏览,在ASP.NET Core分别是通过UseStaticFiles、UseDefaultFiles、UseDirectoryBrowser三个中间件去处理。只有配置了相关中间件才能去操作对应的处理,相信大家对这种操作已经很熟了。静态文件操作相关的源码都位于GitHub aspnetcore仓库中的https://github.com/dotnet/aspnetcore/tree/v3.1.6/src/Middleware/StaticFiles/src目录。接下来我们分别探究这三个中间件的相关代码,来揭开静态文件处理的神秘面纱。

UseStaticFiles

UseStaticFiles中间件使我们处理静态文件时最常使用的中间件,因为只有开启了这个中间件我们才能使用静态文件,比如在使用MVC开发的时候需要私用js css html等文件都需要用到它,使用的方式也比较简单

//使用默认路径,即wwwroot
app.UseStaticFiles();
//或自定义读取路径
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles");
app.UseStaticFiles(new StaticFileOptions {RequestPath="/staticfiles",FileProvider = fileProvider
});

我们直接找到中间件的注册类StaticFileExtensions[点击查看StaticFileExtensions源码]

public static class StaticFileExtensions
{public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app){return app.UseMiddleware<StaticFileMiddleware>();}public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath){return app.UseStaticFiles(new StaticFileOptions{RequestPath = new PathString(requestPath)});}public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options){return app.UseMiddleware<StaticFileMiddleware>(Options.Create(options));}
}

一般我们最常用到的是无参的方式和传递自定义StaticFileOptions的方式比较多,StaticFileOptions是自定义使用静态文件时的配置信息类,接下来我们大致看一下具体包含哪些配置项[点击查看StaticFileOptions源码]

public class StaticFileOptions : SharedOptionsBase
{public StaticFileOptions() : this(new SharedOptions()){}public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions){OnPrepareResponse = _ => { };}/// <summary>/// 文件类型提供程序,也就是我们常用的文件名对应MimeType的对应关系/// </summary>public IContentTypeProvider ContentTypeProvider { get; set; }/// <summary>/// 设置该路径下默认文件输出类型/// </summary>public string DefaultContentType { get; set; }public bool ServeUnknownFileTypes { get; set; }/// <summary>/// 文件压缩方式/// </summary>public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;/// <summary>/// 准备输出之前可以做一些自定义操作/// </summary>public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
}public abstract class SharedOptionsBase
{protected SharedOptionsBase(SharedOptions sharedOptions){SharedOptions = sharedOptions;}protected SharedOptions SharedOptions { get; private set; }/// <summary>/// 请求路径/// </summary>public PathString RequestPath{get { return SharedOptions.RequestPath; }set { SharedOptions.RequestPath = value; }}/// <summary>/// 文件提供程序,在.NET Core中如果需要访问文件相关操作可使用FileProvider文件提供程序获取文件相关信息/// </summary>public IFileProvider FileProvider{get { return SharedOptions.FileProvider; }set { SharedOptions.FileProvider = value; }}
}

我们自定义静态文件访问时,最常用到的就是RequestPath和FileProvider,一个设置请求路径信息,一个设置读取文件信息。如果需要自定义MimeType映射关系可通过ContentTypeProvider自定义设置映射关系

var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".myapp"] = "application/x-msdownload";
provider.Mappings[".htm3"] = "text/html";
app.UseStaticFiles(new StaticFileOptions
{ContentTypeProvider = provider,//可以在输出之前设置输出相关OnPrepareResponse = ctx =>{ctx.Context.Response.Headers.Append("Cache-Control", $"public, max-age=3600");}
});

接下来我们步入正题直接查看StaticFileMiddleware中间件的代码[点击查看StaticFileMiddleware源码]

public class StaticFileMiddleware
{private readonly StaticFileOptions _options;private readonly PathString _matchUrl;private readonly RequestDelegate _next;private readonly ILogger _logger;private readonly IFileProvider _fileProvider;private readonly IContentTypeProvider _contentTypeProvider;public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory){_next = next;_options = options.Value;//设置文件类型提供程序_contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();//文件提供程序_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);//匹配路径_matchUrl = _options.RequestPath;_logger = loggerFactory.CreateLogger<StaticFileMiddleware>();}public Task Invoke(HttpContext context){//判断是够获取到终结点信息,这也就是为什么我们使用UseStaticFiles要在UseRouting之前if (!ValidateNoEndpoint(context)){}//判断HttpMethod,只能是Get和Head操作else if (!ValidateMethod(context)){}//判断请求路径是否存在else if (!ValidatePath(context, _matchUrl, out var subPath)){}//根据请求文件名称判断是否可以匹配到对应的MimeType,如果匹配到则返回contentTypeelse if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType)){}else{   //执行静态文件操作return TryServeStaticFile(context, contentType, subPath);}return _next(context);}private Task TryServeStaticFile(HttpContext context, string contentType, PathString subPath){var fileContext = new StaticFileContext(context, _options, _logger, _fileProvider, contentType, subPath);//判断文件是否存在if (!fileContext.LookupFileInfo()){_logger.FileNotFound(fileContext.SubPath);}else{   //静态文件处理return fileContext.ServeStaticFile(context, _next);}return _next(context);}
}

关于FileExtensionContentTypeProvider这里就不作讲解了,主要是承载文件扩展名和MimeType的映射关系代码不复杂,但是映射关系比较多,有兴趣的可以自行查看FileExtensionContentTypeProvider源码,通过上面我们可以看到,最终执行文件相关操作的是StaticFileContext类[点击查看StaticFileContext源码]

internal struct StaticFileContext
{private const int StreamCopyBufferSize = 64 * 1024;private readonly HttpContext _context;private readonly StaticFileOptions _options;private readonly HttpRequest _request;private readonly HttpResponse _response;private readonly ILogger _logger;private readonly IFileProvider _fileProvider;private readonly string _method;private readonly string _contentType;private IFileInfo _fileInfo;private EntityTagHeaderValue _etag;private RequestHeaders _requestHeaders;private ResponseHeaders _responseHeaders;private RangeItemHeaderValue _range;private long _length;private readonly PathString _subPath;private DateTimeOffset _lastModified;private PreconditionState _ifMatchState;private PreconditionState _ifNoneMatchState;private PreconditionState _ifModifiedSinceState;private PreconditionState _ifUnmodifiedSinceState;private RequestType _requestType;public StaticFileContext(HttpContext context, StaticFileOptions options, ILogger logger, IFileProvider fileProvider, string contentType, PathString subPath){_context = context;_options = options;_request = context.Request;_response = context.Response;_logger = logger;_fileProvider = fileProvider;_method = _request.Method;_contentType = contentType;_fileInfo = null;_etag = null;_requestHeaders = null;_responseHeaders = null;_range = null;_length = 0;_subPath = subPath;_lastModified = new DateTimeOffset();_ifMatchState = PreconditionState.Unspecified;_ifNoneMatchState = PreconditionState.Unspecified;_ifModifiedSinceState = PreconditionState.Unspecified;_ifUnmodifiedSinceState = PreconditionState.Unspecified;//再次判断请求HttpMethodif (HttpMethods.IsGet(_method)){_requestType = RequestType.IsGet;}else if (HttpMethods.IsHead(_method)){_requestType = RequestType.IsHead;}else{_requestType = RequestType.Unspecified;}}/// <summary>/// 判断文件是否存在/// </summary>public bool LookupFileInfo(){//判断根据请求路径是否可以获取到文件信息_fileInfo = _fileProvider.GetFileInfo(_subPath.Value);if (_fileInfo.Exists){//获取文件长度_length = _fileInfo.Length;//最后修改日期DateTimeOffset last = _fileInfo.LastModified;_lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();//ETag标识long etagHash = _lastModified.ToFileTime() ^ _length;_etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');}return _fileInfo.Exists;}/// <summary>/// 处理文件输出/// </summary>public async Task ServeStaticFile(HttpContext context, RequestDelegate next){//1.准备输出相关Header,主要是获取和输出静态文件输出缓存相关的内容//2.我们之前提到的OnPrepareResponse也是在这里执行的ComprehendRequestHeaders();//根据ComprehendRequestHeaders方法获取到的文件状态进行判断switch (GetPreconditionState()){case PreconditionState.Unspecified://处理文件输出case PreconditionState.ShouldProcess://判断是否是Head请求if (IsHeadMethod){await SendStatusAsync(Constants.Status200Ok);return;}try{//判断是否包含range请求,即文件分段下载的情况if (IsRangeRequest){await SendRangeAsync();return;}//正常文件输出处理await SendAsync();_logger.FileServed(SubPath, PhysicalPath);return;}catch (FileNotFoundException){context.Response.Clear();}await next(context);return;case PreconditionState.NotModified:await SendStatusAsync(Constants.Status304NotModified);return;case PreconditionState.PreconditionFailed:await SendStatusAsync(Constants.Status412PreconditionFailed);return;default:var exception = new NotImplementedException(GetPreconditionState().ToString());throw exception;}}/// <summary>/// 通用文件文件返回处理/// </summary>public async Task SendAsync(){SetCompressionMode();ApplyResponseHeaders(Constants.Status200Ok);string physicalPath = _fileInfo.PhysicalPath;var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();//判断是否设置过输出特征操作相关,比如是否启动输出压缩,或者自定义的输出处理比如输出加密等等if (sendFile != null && !string.IsNullOrEmpty(physicalPath)){await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);return;}try{//不存在任何特殊处理的操作作,直接读取文件返回using (var readStream = _fileInfo.CreateReadStream()){await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);}}catch (OperationCanceledException ex){_context.Abort();}}/// <summary>/// 分段请求下载操作处理/// </summary>internal async Task SendRangeAsync(){if (_range == null){ResponseHeaders.ContentRange = new ContentRangeHeaderValue(_length);ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);_logger.RangeNotSatisfiable(SubPath);return;}//计算range相关header数据ResponseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);_response.ContentLength = length;//设置输出压缩相关headerSetCompressionMode();ApplyResponseHeaders(Constants.Status206PartialContent);string physicalPath = _fileInfo.PhysicalPath;var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();//判断是否设置过输出特征操作相关,比如是否启动输出压缩,或者自定义的输出处理比如输出加密等等if (sendFile != null && !string.IsNullOrEmpty(physicalPath)){_logger.SendingFileRange(_response.Headers[HeaderNames.ContentRange], physicalPath);await sendFile.SendFileAsync(physicalPath, start, length, CancellationToken.None);return;}try{using (var readStream = _fileInfo.CreateReadStream()){readStream.Seek(start, SeekOrigin.Begin); _logger.CopyingFileRange(_response.Headers[HeaderNames.ContentRange], SubPath);//设置文件输出起始位置和读取长度await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted);}}catch (OperationCanceledException ex){_context.Abort();}}
}

  由于代码较多删除了处主流程处理以外的其他代码,从这里我们可以看出,首先是针对输出缓存相关的读取设置和处理,其此次是针对正常返回和分段返回的情况,在返回之前判断是否有对输出做特殊处理的情况,比如输出压缩或者自定义的其他输出操作的IHttpResponseBodyFeature,分段返回和正常返回相比主要是多了一部分关于Http头Content-Range相关的设置,对于读取本身其实只是读取的起始位置和读取长度的差别。

UseDirectoryBrowser

目录浏览允许在指定目录中列出目录里的文件及子目录。出于安全方面考虑默认情况下是关闭的可以通过UseDirectoryBrowser中间件开启指定目录浏览功能。通常情况下我们会这样使用

//启用默认目录浏览,即wwwroot
app.UseDirectoryBrowser();
//或自定义指定目录浏览
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/MyImages");
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{RequestPath = "/MyImages",FileProvider = fileProvider
});

开启之后当我们访问https:///MyImages地址的时候将会展示如下效果,通过一个表格展示目录里的文件信息等
找到中间件注册类[点击查看DirectoryBrowserExtensions源码]

public static class DirectoryBrowserExtensions
{public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app){return app.UseMiddleware<DirectoryBrowserMiddleware>();}public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath){return app.UseDirectoryBrowser(new DirectoryBrowserOptions{RequestPath = new PathString(requestPath)});}public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options){return app.UseMiddleware<DirectoryBrowserMiddleware>(Options.Create(options));}
}

这个中间件启用的重载方法和UseStaticFiles类似最终都是在传递DirectoryBrowserOptions,接下来我们就看DirectoryBrowserOptions传递了哪些信息[点击查看DirectoryBrowserOptions源码]

public class DirectoryBrowserOptions : SharedOptionsBase
{public DirectoryBrowserOptions(): this(new SharedOptions()){}public DirectoryBrowserOptions(SharedOptions sharedOptions): base(sharedOptions){}/// <summary>/// 目录格式化提供,默认是提供表格的形式展示,课自定义/// </summary>public IDirectoryFormatter Formatter { get; set; }
}

无独有偶这个类和StaticFileOptions一样也是集成自SharedOptionsBase类,唯一多了IDirectoryFormatter操作,通过它我们可以自定义展示到页面的输出形式,接下来我们就重点看下DirectoryBrowserMiddleware中间件的实现

public class DirectoryBrowserMiddleware
{private readonly DirectoryBrowserOptions _options;private readonly PathString _matchUrl;private readonly RequestDelegate _next;private readonly IDirectoryFormatter _formatter;private readonly IFileProvider _fileProvider;public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DirectoryBrowserOptions> options): this(next, hostingEnv, HtmlEncoder.Default, options){}public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options){_next = next;_options = options.Value;//默认是提供默认目录的访问程序_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);//默认传递的是HtmlDirectoryFormatter类型,也就是我们看到的输出表格的页面_formatter = options.Value.Formatter ?? new HtmlDirectoryFormatter(encoder);_matchUrl = _options.RequestPath;}public Task Invoke(HttpContext context){//1.IsGetOrHeadMethod判断是否为Get或Head请求//2.TryMatchPath判断请求的路径和设置的路径是否可以匹配的上//3.TryGetDirectoryInfo判断根据匹配出来的路径能否查找到真实的物理路径if (context.GetEndpoint() == null &&Helpers.IsGetOrHeadMethod(context.Request.Method)&& Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)&& TryGetDirectoryInfo(subpath, out var contents)){//判断请求路径是否是/为结尾if (!Helpers.PathEndsInSlash(context.Request.Path)){//如果不是以斜线结尾则重定向(个人感觉直接在服务端重定向就可以了,为啥还要返回浏览器在请求一次)context.Response.StatusCode = StatusCodes.Status301MovedPermanently;var request = context.Request;var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);context.Response.Headers[HeaderNames.Location] = redirect;return Task.CompletedTask;}//返回展示目录的内容return _formatter.GenerateContentAsync(context, contents);}return _next(context);}/// <summary>/// 根据请求路径匹配到物理路径信息是否存在,存在则返回路径信息/// </summary>private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents){contents = _fileProvider.GetDirectoryContents(subpath.Value);return contents.Exists;}
}

这个操作相对简单了许多,主要就是判断请求路径能否和预设置的路径匹配的到,如果匹配到则获取可以操作当前目录内容IDirectoryContents然后通过IDirectoryFormatter输出如何展示目录内容,关于IDirectoryFormatter的默认实现类HtmlDirectoryFormatter这里就不展示里面的代码了,逻辑非常的加单就是拼接成table的html代码然后输出,有兴趣的同学可自行查看源码[点击查看HtmlDirectoryFormatter源码],如果自定义的话规则也非常简单,主要看你想输出啥

public class TreeDirectoryFormatter: IDirectoryFormatter
{public Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents){//遍历contents实现你想展示的方式}
}

然后在UseDirectoryBrowser的时候给Formatter赋值即可

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{Formatter = new TreeDirectoryFormatter()
});

UseDefaultFiles

很多时候出于安全考虑或者其他原因我们想在访问某个目录的时候返回一个默认的页面或展示,这个事实我们就需要使用UseDefaultFiles中间件,当我们配置了这个中间件,如果命中了配置路径,那么会直接返回默认的页面信息,简单使用方式如下

//wwwroot目录访问展示默认文件
app.UseDefaultFiles();
//或自定义目录默认展示文件
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles");
app.UseDefaultFiles(new DefaultFilesOptions
{RequestPath = "/staticfiles",FileProvider = fileProvider
});

老规矩,我们查看下注册UseDefaultFiles的源码[点击查看DefaultFilesExtensions源码]

public static class DefaultFilesExtensions
{public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app){return app.UseMiddleware<DefaultFilesMiddleware>();}public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath){return app.UseDefaultFiles(new DefaultFilesOptions{RequestPath = new PathString(requestPath)});}public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options){return app.UseMiddleware<DefaultFilesMiddleware>(Options.Create(options));}
}

使用方式和UseStaticFiles、UseDirectoryBrowser是一样,最终都是调用传递DefaultFilesOptions的方法,我们查看一下DefaultFilesOptions的大致实现[点击查看源码]

public class DefaultFilesOptions : SharedOptionsBase
{public DefaultFilesOptions(): this(new SharedOptions()){}public DefaultFilesOptions(SharedOptions sharedOptions): base(sharedOptions){//系统提供的默认页面的名称DefaultFileNames = new List<string>{"default.htm","default.html","index.htm","index.html",};}/// <summary>/// 通过这个属性可以配置默认文件名称/// </summary>public IList<string> DefaultFileNames { get; set; }
}

和之前的方法如出一辙,都是继承自SharedOptionsBase,通过DefaultFileNames我们可以配置默认文件的名称,默认是default.html/htm和index.html/htm。我们直接查看中间件DefaultFilesMiddleware的源码[点击查看源码]

public class DefaultFilesMiddleware
{private readonly DefaultFilesOptions _options;private readonly PathString _matchUrl;private readonly RequestDelegate _next;private readonly IFileProvider _fileProvider;public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DefaultFilesOptions> options){_next = next;_options = options.Value;_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);_matchUrl = _options.RequestPath;}public Task Invoke(HttpContext context){//1.我们使用UseDefaultFiles中间件的时候要置于UseRouting之上,否则就会不生效//2.IsGetOrHeadMethod判断请求为Get或Head的情况下才生效//3.TryMatchPath判断请求的路径和设置的路径是否可以匹配的上if (context.GetEndpoint() == null &&Helpers.IsGetOrHeadMethod(context.Request.Method)&& Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)){//根据匹配路径获取物理路径对应的信息var dirContents = _fileProvider.GetDirectoryContents(subpath.Value);if (dirContents.Exists){//循环配置的默认文件名称for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++){string defaultFile = _options.DefaultFileNames[matchIndex];//匹配配置的启用默认文件的路径+遍历到的默认文件名称的路径是否存在var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile);if (file.Exists){//判断请求路径是否已"/"结尾,如果不是则从定向(这个点个人感觉可以改进)if (!Helpers.PathEndsInSlash(context.Request.Path)){context.Response.StatusCode = StatusCodes.Status301MovedPermanently;var request = context.Request;var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);context.Response.Headers[HeaderNames.Location] = redirect;return Task.CompletedTask;}//如果匹配的上,则将配置的启用默认文件的路径+遍历到的默认文件名称的路径组合成新的Path交给_next(context)//比如将组成类似这种路径/staticfiles/index.html向下传递context.Request.Path = new PathString(context.Request.Path.Value + defaultFile);break;}}}}return _next(context);}
}

这个中间件的实现思路也非常简单主要的工作就是,匹配配置的启用默认文件的路径+遍历到的默认文件名称的路径是否存在,如果匹配的上,则将配置的启用默认文件的路径+遍历到的默认文件名称的路径组合成新的Path(比如/staticfiles/index.html)交给后续的中间件去处理。这里值得注意的是UseDefaultFiles 必须要配合UseStaticFiles一起使用,而且注册位置要出现在UseStaticFiles之上。这也是为什么UseDefaultFiles只需要匹配到默认文件所在的路径并重新赋值给context.Request.Path既可的原因。
当然我们也可以自定义默认文件的名称,因为只要能匹配的到具体的文件既可

var defaultFilesOptions = new DefaultFilesOptions
{RequestPath = "/staticfiles",FileProvider = fileProvider
};
//我们可以清除掉系统默认的默认文件名称
defaultFilesOptions.DefaultFileNames.Clear();
defaultFilesOptions.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(defaultFilesOptions);

总结

    通过上面的介绍我们已经大致了解了静态文件处理的大致实现思路,相对于传统的Asp.Net程序我们可以更方便的处理静态文件信息,但是思路是一致的,IIS会优先处理静态文件,如果静态文件处理不了的情况才会交给程序去处理。ASP.NET Core也不例外,通过我们查看中间件源码里的context.GetEndpoint()==null判断可以知道,ASP.NET Core更希望我们优先去处理静态文件,而不是任意出现在其他位置去处理。关于ASP.NET Core处理静态文件的讲解就到这里,欢迎评论区探讨交流。

????欢迎扫码关注我的公众号????

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/308763.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[SpringSecurity]基本原理_过滤器链

SpringSecurity 本质是一个过滤器链&#xff1a; 从启动是可以获取到过滤器链&#xff1a; org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil ter org.springframework.security.web.context.SecurityContextPersistenceFilter org.s…

通过Windows Visual Studio远程调试WSL2中的.NET Core Linux应用程序

最近两天在Linux中调试.NET Core应用程序&#xff0c;同时我发现在Linux中调试.NET Core应用程序并不容易。一直习惯在Visual Studio中进行编码和调试。现在我想的是可以简单快速的测试.NET Core应用在Linux。所以通过本篇文章我们能了解到如何在Windows中使用Visual Studio进行…

[SpringSecurity]基本原理_过滤器加载过程

过滤器如何进行加载的&#xff1f; 1.使用SpringSecurity配置过滤器 DelegatingFilterProxy 其中上面的getTargetBeanName()得到的名字是FilterChainProxy 找到FilterChainProxy这个类中的doFilter方法 最后两张图片里面的代码表示&#xff1a; 用了一个增强for循环和getFi…

[SpringSecurity]基本原理_两个重要的接口_UserDetailsService接口和PasswordEncoder接口

UserDetailsService接口 当什么也没有配置的时候&#xff0c;账号和密码是由 Spring Security 定义生成的。而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。 如果需要自定义逻辑时&#xff0c;只需要实现 UserDetailsService 接…

.NET 开源项目 StreamJsonRpc 介绍[下篇]

阅读本文大概需要 9 分钟。大家好&#xff0c;这是 .NET 开源项目 StreamJsonRpc 介绍的最后一篇。上篇介绍了一些预备知识&#xff0c;包括 JSON-RPC 协议介绍&#xff0c;StreamJsonRpc 是一个实现了 JSON-RPC 协议的库&#xff0c;它基于 Stream、WebSocket 和自定义的全双工…

ASP.NET Core Blazor 初探之 Blazor Server

上周初步对Blazor WebAssembly进行了初步的探索(ASP.NET Core Blazor 初探之 Blazor WebAssembly)。这次来看看Blazor Server该怎么玩。Blazor ServerBlazor 技术又分两种&#xff1a;Blazor WebAssemblyBlazor ServerBlazor WebAssembly上次已经介绍过了&#xff0c;这次主要来…

[SpringSecurity]web权限方案_用户认证_设置用户名密码

设置登陆的用户名和密码 第一种方式&#xff1a;通过配置文件 spring.security.user.nameatguigu spring.security.user.passwordatguigu第二种方式&#xff1a;通过配置类 package com.atguigu.securitydemo1.config;import org.springframework.context.annotation.Bean; i…

更优雅的在 Xunit 中使用依赖注入

Xunit.DependencyInjection 7.0 发布了Intro上次我们已经介绍过一次大师的 Xunit.DependencyInjection 在 Xunit 中使用依赖注入 &#xff0c;最近大师完成了 7.0 的重构并且已经正式发布&#xff0c;已经可以直接安装使用了7.0 为我们带来了更好的编程体验&#xff0c;在 6.x…

[SpringSecurity]web权限方案_用户认证_查询数据库完成认证

#mysql 数据库连接 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver spring.datasource.urljdbc:mysql://localhost:3306/demo?serverTimezoneUTC spring.datasource.usernameroot spring.datasource.passwordrootpackage com.atguigu.securitydemo1.config;i…

.Net Core 2.2升级3.1的避坑指南

写在前面微软在更新.Net Core版本的时候&#xff0c;动作往往很大&#xff0c;使得每次更新版本的时候都得小心翼翼&#xff0c;坑实在是太多。往往是悄咪咪的移除了某项功能或者组件&#xff0c;或者不在支持XX方法&#xff0c;这就很花时间去找回需要的东西了&#xff0c;下面…

[SpringSecurity]web权限方案_用户认证_自定义用户登录页面

在配置类中实现相关的配置 Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //自定义自己编写的登陆页面.loginPage("/login.html") //登陆页面设置.loginProcessingUrl("/user/login") //登陆访问路径.defa…

Asp.Net Core Blazor之容器部署

写在前面Docker作为开源的应用容器引擎&#xff0c;可以让我们很轻松的构建一个轻量级、易移植的容器&#xff0c;通过Docker方式进行持续交付、测试和部署&#xff0c;都是极为方便的&#xff0c;并且对于我们开发来说&#xff0c;最直观的优点还是解决了日常开发中的环境配置…

[SpringSecurity]web权限方案_用户授权_基于权限访问控制_基于角色访问控制_hasAuthority和hasAnyAuthority_hasRole和hasAnyRole

基于角色或权限进行访问控制 hasAuthority 方法 如果当前的主体具有指定的权限&#xff0c;则返回 true,否则返回 false 在配置类设置当前访问地址有哪些 Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //自定义自己编写的登…

.Net Core WebAPI + Axios +Vue 实现下载与下载进度条

写在前面老板说&#xff1a;系统很慢&#xff0c;下载半个小时无法下载&#xff0c;是否考虑先压缩再给用户下载&#xff1f;本来是已经压缩过了&#xff0c;不过第一反应应该是用户下的数量多&#xff0c;导致压缩包很大&#xff0c;然后自己测试发现&#xff0c;只是等待的时…

[SpringSecurity]web权限方案_用户授权_自定义403页面

自定义403页面 unauth.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body><h1>没有访问权限</h1></body> </html>配置类…

三分钟Docker-环境搭建篇

如题目显示&#xff0c;三分钟让你学会在windows上安装docker环境&#xff0c;开启docker之旅的第一步。安装前要求Windows 10 64位&#xff1a;专业版&#xff0c;企业版或教育版&#xff08;内部版本16299或更高版本&#xff09;。必须启用Hyper-V控制面板->程序和功能-&g…

[SpringSecurity]web权限方案_用户注销

用户注销 在配置类中添加退出映射地址 //退出http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();测试 修改配置类&#xff0c;登陆成功之后跳转到成功页面 在成功页面添加超链接&#xff0c;写设置退出路径 success.htm…

骚年快答 | 为何微服务项目都使用单体代码仓库?

【答疑解惑】| 作者 / Edison Zhou这是恰童鞋骚年的第265篇原创内容之前在学习微软的示例eShopOnContainers时发现它使用的是单体代码仓库库&#xff0c;之后又发现大家在进行微服务项目开发时也都在使用单体代码仓库。问题来了&#xff0c;为啥要微服务项目都要使用单体仓库&a…

[SpringSecurity]web权限方案_自动登陆_原理分析和具体实现

自动登陆 1.cookie技术 2.安全框架机制实现自动登陆 这里我们使用安全框架机制实现自动登陆技术 实现原理 具体实现 第一步 创建数据库 CREATE TABLE persistent_logins (username varchar(64) NOT NULL,series varchar(64) NOT NULL,token varchar(64) NOT NULL,last_us…

[SpringSecurity]web权限方案_CSRF功能

CSRF CSRF功能默认是已经打开了&#xff01; 具体过程可以阅读CsrfFilter这个过滤器的源码 CSRF 理解 在登录页面添加一个隐藏域 <input type"hidden"th:if"${_csrf}!null"th:value"${_csrf.token}"name"_csrf "/>关闭安全…