Using middleware to protect nonpublic files in ASP.

Source: Internet
Author: User
Tags httpcontext sendfile

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:

    1. Provider is only responsible for providing, and the storage may not be responsible for it
    2. Future storage may be replaced, but the component or code calling provider does not care
    3. 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:

    1. Gets the handler type of the current request map
    2. Get an instance of handler to the DI container
    3. 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.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.