In the enterprise development, we often meet by the user upload the file scene, such as an OA system, by the user fill out a form and upload ID, by the identity of the administrator review, Super Administrator can view.
In such a scenario, the user uploads the file can only have three kinds of people to see (can access)
- The person who uploaded the file
- Identity Examiner
- Super Admin
So, in this blog we will learn how to design and implement a file authorization middleware
Problem analysis How to determine who the file belongs to
To be able to authorize the file, the name of the file will have a regular, we can from the file name to determine who the file belongs to, for example, this article can be designed to design the file name
工号-GUID-[Front/Back]
For example: 100211-4738B54D3609410CBC785BCD1963F3FA-Front
This represents the front of the ID card uploaded by 100211
Determine which function the file belongs to
The ability to upload files in an enterprise system can be a lot:
- Upload ID in a feature
- Upload a contract in a feature
- Upload an invoice for a feature
The way we differentiate is by using paths, for example in this case, using the
- /id-card
- /contract
- /invoices
Cannot access through Staticfile middleware
Files processed by the Staticfile middleware are public, and the files processed by this middleware can only be files that are publicly accessible by anyone, such as JS, CSS, image, etc.
Design and implementation why use middleware implementation
For our needs, we can also use controller/action direct implementation, it is relatively simple, but difficult to reuse, want to use in other projects can only copy code.
Use a separate file store directory
In this example, we put all the files (regardless of the upload function) in one root directory for example: C:\xxx-uploads (Windows), this directory is not controlled by Staticfile middleware
Structure design of middleware
This is a typical Service-handler mode, when the request arrives at the file authorization middleware, the middleware lets the request FileAuthorizationService
characteristic determine according to the request belongs to the handler, and carries on the authorization authorization task, obtains the authorization result, The file authorization middleware determines whether to return files to the client or to return other unauthorized results based on the authorization result.
Request feature Design
The file authorization middleware is only entered when the request is a specific format, for example, we design it as such
host/中间件标记/handler标记/文件标记
Then the corresponding request may be:
Https://localhost:8080/files/id-card/100211-4738B54D3609410CBC785BCD1963F3FA-Front.jpg
This files
is the markup that acts on the middleware, Id-card is used for confirmation by IdCardHandler
processing, and the subsequent content is used to confirm the identity of the uploader.
Ifileauthorizationservice Design
public interface IFileAuthorizationService{ string AuthorizationScheme { get; } string FileRootPath { get; } Task<FileAuthorizeResult> AuthorizeAsync(HttpContext context, string path);
Here the AuthorizationScheme
corresponding, the middleware tag above, FileRootPath
represents the absolute path of the root directory of the file, the AuthorizeAsync
method is used for the actual authentication, and returns a certified result
Fileauthorizeresult Design
public class FileAuthorizeResult{ public bool Succeeded { get; } public string RelativePath { get; } public string FileDownloadName { get; set; } public Exception Failure { get; }
- Succeeded indicates whether the authorization was successful
- The relative path of the RelativePath file, the file in the request may be mapped to a completely different file path, which is more secure, such as
/files/id-card/4738B54D3609410CBC785BCD1963F3FA.jpg
mapping URIs to /xxx-file/abc/100211-4738B54D3609410CBC785BCD1963F3FA-Front.jpg
, which can confuse the file name in the request, more secure
- The name of the Filedownloadname file download, for example, the file hit in the previous example may contain a work number, while the download can be just a GUID
- Failure authorization is an error that occurs, or the cause of the error
Ifileauthorizehandler Design
public interface IFileAuthorizeHandler{ Task<FileAuthorizeResult> AuthorizeAsync(HttpContext context,string path); 略...
Ifileauthorizehandler only requires a method, that is, the method of authorization
Ifileauthorizationhandlerprovider Design
public interface IFileAuthorizationHandlerProvider{ Type GetHandlerType (string scheme); bool Exist(string scheme); 略...
- The gethandlertype is used to get the actual type of the specified Authorizehandler, which is used in Authorizationservice
- The exist method is used to confirm that the specified processor is included
Fileauthorizationoptions Design
public class FileAuthorizationOptions{ private List<FileAuthorizationScheme> _schemes = new List<FileAuthorizationScheme>(20); public string FileRootPath { get; set; } public string AuthorizationScheme { get; set; } public IEnumerable<FileAuthorizationScheme> Schemes { get => _schemes; } public void AddHandler<THandler>(string name) where THandler : IFileAuthorizeHandler { _schemes.Add(new FileAuthorizationScheme(name, typeof(THandler))); } public Type GetHandlerType(string scheme) { return _schemes.Find(s => s.Name == scheme)?.HandlerType; 略...
Fileauthorizationoptions's primary responsibility is to confirm the relevant options, such as Filerootpath and Authorizationscheme. and a mapping that stores handler tags and handler types.
In the previous section, Ifileauthorizationhandlerprovider is used to provide handler, so why put the storage in the options?
The reasons are as follows:
- Provider is only responsible for providing, and the storage may not be responsible for it
- Future storage may be replaced, but the component or code calling provider does not care
- This is a convenient way to do what you need right now, and there's no problem.
Fileauthorizationscheme Design
public class FileAuthorizationScheme{ public FileAuthorizationScheme(string name, Type handlerType) { if (string.IsNullOrEmpty(name)) { throw new ArgumentException("name must be a valid string.", nameof(name)); } Name = name; HandlerType = handlerType ?? throw new ArgumentNullException(nameof(handlerType)); } public string Name { get; } public Type HandlerType { get; } 略...
The function of this class is to store mappings of handler tags and handler types
Fileauthorizationservice implementation
The first part is Authorizationscheme and Filerootpath.
public class FileAuthorizationService : IFileAuthorizationService{ public FileAuthorizationOptions Options { get; } public IFileAuthorizationHandlerProvider Provider { get; } public string AuthorizationScheme => Options.AuthorizationScheme; public string FileRootPath => Options.FileRootPath;
The most important part is the implementation of the authorization method:
public async Task<FileAuthorizeResult> AuthorizeAsync(HttpContext context, string path){ var handlerScheme = GetHandlerScheme(path); if (handlerScheme == null || !Provider.Exist(handlerScheme)) { return FileAuthorizeResult.Fail(); } var handlerType = Provider.GetHandlerType(handlerScheme); if (!(context.RequestServices.GetService(handlerType) is IFileAuthorizeHandler handler)) { throw new Exception($"the required file authorization handler of ‘{handlerScheme}‘ is not found "); } // start with slash var requestFilePath = GetRequestFileUri(path, handlerScheme); return await handler.AuthorizeAsync(context, requestFilePath);}
The authorization process is divided into three steps:
- Gets the handler type of the current request map
- Get an instance of handler to the DI container
- Authorized by Handler
Here are the two private methods used in the code snippet:
private string GetHandlerScheme(string path){ var arr = path.Split(‘/‘); if (arr.Length < 2) { return null; } // arr[0] is the Options.AuthorizationScheme return arr[1];}private string GetRequestFileUri(string path, string scheme){ return path.Remove(0, Options.AuthorizationScheme.Length + scheme.Length + 1);}
Design and implementation of Fileauthorization middleware
Because the authorization logic has been extracted into IFileAuthorizationService
and IFileAuthorizationHandler
, the middleware is responsible for a small number of functions, mainly to accept requests and write files to the client.
Understanding what's next requires middleware knowledge, and if you're not familiar with middleware, learn middleware first
You can learn from the ASP. NET Core Middleware Documentation
Next we put out the complete invoke method, and then gradually parse:
Public Async Task Invoke (HttpContext context) {//trim the start slash var path = context. Request.Path.Value.TrimStart ('/'); if (! Belongtome (path)) {await _next. Invoke (context); Return } var result = await _service. Authorizeasync (context, path); if (!result. Succeeded) {_logger. Loginformation ($ "Request file is forbidden. Request path is: {path}"); Forbidden (context); Return } if (string. Isnullorwhitespace (_service. Filerootpath) {throw new Exception ("File root path is not spicificated"); } string FullName; if (path.ispathrooted (result). RelativePath)) {FullName = result. RelativePath; } else {fullName = Path.Combine (_service. Filerootpath, result. RelativePath); } var fileInfo = new FileInfo (fullName); if (!fileinfo.exists) {NotFound (context); Return } _logger. Loginformation ($ "{context. User.Identity.Name} Request file: {Fileinfo.fullname} hasBeeb authorized. File sending "); Setresponseheaders (context, result, fileInfo); Await Writefileasync (context, result, fileInfo);}
The first step is to get the requested URL and determine whether the request belongs to the current file authorization middleware
var path = context.Request.Path.Value.TrimStart(‘/‘);if (!BelongToMe(path)){ await _next.Invoke(context); return;}
The way to judge is to check if the first paragraph in the URL is equal to Authorizationscheme (for example: files)
private bool BelongToMe(string path){ return path.StartsWith(_service.AuthorizationScheme, true, CultureInfo.CurrentCulture);}
The second step is IFileAuthorizationService
to invoke the authorization
var result = await _service.AuthorizeAsync(context, path);
The third step is to process the result and, if it fails, to block the download of the file:
if (!result.Succeeded){ _logger.LogInformation($"request file is forbidden. request path is: {path}"); Forbidden(context); return;}
The way to block is to return 403, unauthorized Httpcode
private void Forbidden(HttpContext context){ HttpCode(context, 403);}private void HttpCode(HttpContext context, int code){ context.Response.StatusCode = code;}
If successful, write the file to the response:
Writing to a file is slightly more complicated than the previous logic, but it's also very simple, let's take a look
The first step is to confirm the file's full path:
string fullName;if (Path.IsPathRooted(result.RelativePath)){ fullName = result.RelativePath;}else{ fullName = Path.Combine(_service.FileRootPath, result.RelativePath);}
As mentioned earlier, we designed to store all files in a single directory, but in fact we do not do so, as long as the authorized handler will map the request to a complete physical path, so that in the future there will be more extensibility, such as a function of the file is not stored in a unified directory, then also can.
This step is to determine and confirm the final file path
The second step is to check if the file exists:
var fileInfo = new FileInfo(fullName);if (!fileInfo.Exists) { NotFound(context); return;}private void NotFound(HttpContext context){ HttpCode(context, 404);}
The last step is to write the file:
await WriteFileAsync(context, result, fileInfo);
The complete method is as follows:
Private Async Task Writefileasync (HttpContext context, fileauthorizeresult result, FileInfo FileInfo) {var Response = context. Response; var sendFile = response. Httpcontext.features.get<ihttpsendfilefeature> (); if (sendFile! = null) {await sendfile.sendfileasync (fileinfo.fullname, 0L, NULL, default (Cancellationto Ken)); Return } using (var fileStream = new FileStream (Fileinfo.fullname, FileMode.Open, FileAccess.Read, Fileshare.readwrite, buffersize, Fileoptions.asynchron ous | Fileoptions.sequentialscan) {try {await Streamcopyoperation.copytoasync (fil eSTREAM, context. Response.body, Count:null, Buffersize:buffersize, Cancel:context. requestaborted); } catch (OperationCanceledException) {//Don ' t throw this exception, it ' s MOST likely caused by the client disconnecting. However, if it is cancelled for any and reason we need to prevent empty responses. Context. Abort ();
First we ask for IHttpSendFileFeature
it, and if you do, use it directly to send the file.
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();if (sendFile != null){ await sendFile.SendFileAsync(fileInfo.FullName, 0L, null, default(CancellationToken)); return;}
This is another important feature in ASP. If you don't know it you don't have to worry about it because it doesn't make much difference, but if you want to learn it, you can refer to the request functionality document in ASP.
If it is not supported IHttpSendFileFeature
then use the original method to write the file to the request body:
using (var fileStream = new FileStream( fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, BufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan)){ try { await StreamCopyOperation.CopyToAsync(fileStream, context.Response.Body, count: null, bufferSize: BufferSize, cancel: context.RequestAborted); } catch (OperationCanceledException) { // Don‘t throw this exception, it‘s most likely caused by the client disconnecting. // However, if it was cancelled for any other reason we need to prevent empty responses. context.Abort();
Here, our middleware is complete.
The extension method of middleware
While our middleware and licensing services are all written, it doesn't seem to be working directly, so let's write the relevant extension methods to make it work.
The end result looks like this:
// 在di配置中services.AddFileAuthorization(options =>{ options.AuthorizationScheme = "file"; options.FileRootPath = CreateFileRootPath();}).AddHandler<TestHandler>("id-card");// 在管道配置中app.UseFileAuthorization();
To achieve this effect, you write three classes:
- Fileauthorizationbuilder
- Fileauthorizationappbuilderextentions
- Fileauthorizationservicecollectionextensions
Two of the ground to implementapp.UseFileAuthorization();
A third is used to implementservices.AddFileAuthorization(options =>...
The first one to implement.AddHandler<TestHandler>("id-card");
Fileauthorizationbuilder
public class FileAuthorizationBuilder{ public FileAuthorizationBuilder(IServiceCollection services) { Services = services; } public IServiceCollection Services { get; } public FileAuthorizationBuilder AddHandler<THandler>(string name) where THandler : class, IFileAuthorizeHandler { Services.Configure<FileAuthorizationOptions>(options => { options.AddHandler<THandler>(name ); }); Services.AddTransient<THandler>(); return this;
This part of the main role is to implement the method of adding handler, the added handler is instantaneous
Fileauthorizationappbuilderextentions
public static class FileAuthorizationAppBuilderExtentions{ public static IApplicationBuilder UseFileAuthorization(this IApplicationBuilder app) { if (app == null) { throw new ArgumentNullException(nameof(app)); } return app.UseMiddleware<FileAuthenticationMiddleware>();
The main role is to put the middleware into the pipeline, very simple
Fileauthorizationservicecollectionextensions
public static class FileAuthorizationServiceCollectionExtensions{ public static FileAuthorizationBuilder AddFileAuthorization(this IServiceCollection services) { return AddFileAuthorization(services, null); } public static FileAuthorizationBuilder AddFileAuthorization(this IServiceCollection services, Action<FileAuthorizationOptions> setup) { services.AddSingleton<IFileAuthorizationService, FileAuthorizationService>(); services.AddSingleton<IFileAuthorizationHandlerProvider, FileAuthorizationHandlerProvider>(); if (setup != null) { services.Configure(setup); } return new FileAuthorizationBuilder(services);
This section is a registration service, IFileAuthorizationService
and will be IFileAuthorizationService
registered as a single case
Here, all the code is done.
Test
Let's write a simple test to test how the middleware works.
To write a test handler first, this handler allows any user to access the file:
public class TestHandler : IFileAuthorizeHandler{ public const string TestHandlerScheme = "id-card"; public Task<FileAuthorizeResult> AuthorizeAsync(HttpContext context, string path) { return Task.FromResult(FileAuthorizeResult.Success(GetRelativeFilePath(path), GetDownloadFileName(path))); } public string GetRelativeFilePath(string path) { path = path.TrimStart(‘/‘, ‘\\‘).Replace(‘/‘, ‘\\‘); return $"{TestHandlerScheme}\\{path}"; } public string GetDownloadFileName(string path) { return path.Substring(path.LastIndexOf(‘/‘) + 1); }}
Test method:
public async Task InvokeTest(){ var builder = new WebHostBuilder() .Configure(app => { app.UseFileAuthorization(); }) .ConfigureServices(services => { services.AddFileAuthorization(options => { options.AuthorizationScheme = "file"; options.FileRootPath = CreateFileRootPath(); }) .AddHandler<TestHandler>("id-card"); }); var server = new TestServer(builder); var response = await server.CreateClient().GetAsync("http://example.com/file/id-card/front.jpg"); Assert.Equal(200, (int)response.StatusCode); Assert.Equal("image/jpeg", response.Content.Headers.ContentType.MediaType);}
This test passed on schedule, in this case also wrote a lot of other tests, do not post it all, in addition, this project has been uploaded to my github, the need for the code of the classmate to pick up
Https://github.com/rocketRobin/FileAuthorization
You can also get this middleware directly using NuGet:
Install-package fileauthorization
Install-package fileauthorization.abstractions
If this article is useful to you, then give me a praise: D
Welcome reprint, Reprint please indicate the original author and source, thank you
Finally, in the enterprise development we also want to detect the authenticity of the user upload files, if through the file extension confirmation, obviously not reliable, so we have to use other methods, if you have related problems, you can refer to my other blog in. Use Myrmec to detect file True format in Netcore
Using middleware to protect nonpublic files in ASP.