學習ASP.NET Core Blazor程式設計系列十七——檔案上傳(上)

2022-12-18 18:00:20
學習ASP.NET Core Blazor程式設計系列一——綜述
學習ASP.NET Core Blazor程式設計系列八——資料校驗
學習ASP.NET Core Blazor程式設計系列十——路由(上)
學習ASP.NET Core Blazor程式設計系列十三——路由(完)
學習ASP.NET Core Blazor程式設計系列十五——查詢
 

      從本篇文章開始我們來講在圖書租賃系統中如何使用內建的檔案上傳元件進行檔案上傳功能的開發。本文的範例適合上傳小型檔案。本篇文章演示如何通過Blazor的內建元件InputFile將檔案上傳至伺服器。

     安全注意事項

  在向用戶提供向上傳檔案的功能時,必須格外注意安全性。攻擊者可能對系統執行拒絕服務和其他攻擊。所以在提供上傳功能時需要注意以下安全措施:

    1. 將檔案上傳到系統上的專用檔案上傳目錄,這樣可以更輕鬆地對上傳內容實施安全措施。如果允許檔案上傳,請確保在上傳目錄禁用執行許可權。

    2. 上傳檔案的檔名在伺服器端儲存時要由應用程式自動重新命名檔名稱,而不是採用使用者輸入或已上傳檔案的檔名。

    3.請不要將上傳的檔案儲存在與應用程式相同的目錄下。

    4. 僅允許使用一組特定的副檔名。

    5. 在伺服器端重新執行使用者端檢查。 不要相信使用者端檢查,因為使用者端檢查很容易規避。

    6. 檢查上傳檔案大小,防止上傳檔案的大小比預期的檔案大小大。

    7. 對上傳檔案的內容進行病毒/惡意軟體掃描程式。

    8.應用程式中的檔案不能被具有相同名稱的上傳檔案覆蓋。

警告

   將惡意程式碼上傳到系統通常是執行程式碼的第一步,這些程式碼可以實現以下功能:

   1. 完全接管系統。

   2. 過載系統,導致系統完全崩潰。

   3. 洩露使用者或系統資料。

 

一、新增一個用於上傳檔案的檔案輔助類FileHelpers

為避免處理上傳檔案檔案時出現重複程式碼,我們首先建立一個靜態類用於處理上傳功能。

   1.在Visual Studio 2022 的解決方案資源管理器中建立一個「Utils」資料夾。

   2.在Visual Studio 2022的解決方案資源管理器中,滑鼠左鍵選中「Utils」資料夾,右鍵單擊,在彈出選單中選擇「新增—>類」(如下圖)。 將類命名為「FileHelpers」。

 

   3.在Visual Studio 2022的文字編輯器中開啟我們剛才建立的「FileHelpers.cs」類檔案,並新增以下內容。其中方法 ProcessFormFile 接受 IBrowserFile 和 ModelStateDictionary等引數,儲存成功則空字串,否則返回錯誤資訊。 檢查內容型別和長度。 如果上傳檔案未通過校驗,將向 ModelState 新增一個錯誤。

using BlazorAppDemo.Models;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Reflection;
using System.Text;
 
namespace BlazorAppDemo.Utils
{

    public class FileHelpers
    {

        public static async Task<string> ProcessFormFile(IBrowserFile formFile, ModelStateDictionary modelState,IWebHostEnvironment envir,int maxFileSize)
        {
            var fieldDisplayName = string.Empty;        
 
            if (!string.IsNullOrEmpty(formFile.Name))
            {
                // 如果名稱沒有找到,將會有一個簡單的錯誤訊息,但不會顯示檔名稱
                string displayFileName = formFile.Name.Substring(formFile.Name.IndexOf(".") + 1);

                fieldDisplayName = $"{displayFileName} ";
               
            }
 
            //使用path.GetFileName獲取一個帶路徑的全檔名。
            //通過HtmlEncode進行編碼的結果必須在錯誤訊息中返回。
            var fileName = WebUtility.HtmlEncode(Path.GetFileName(formFile.Name));
 
            if (formFile.ContentType.ToLower() != "text/plain")
            {
                modelState.AddModelError(formFile.Name,
                                         $"The {fieldDisplayName}file ({fileName}) must be a text file.");
            }
 
 
            //校驗檔案長度,如果檔案不包含內容,則不必讀取檔案長度。
            //此校驗不會檢查僅具有BOM(位元組順序標記)作為內容的檔案,
            //因此在讀取檔案內容後再次檢驗檔案內容長度,以校驗僅包含BOM的檔案。
            if (formFile.Size == 0)
            {
                modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) is empty.");

            }
            else if (formFile.Size > maxFileSize)
            {

                modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) exceeds 1 MB.");
            }
            else
            {
                try
                {
                  

                    //獲取一個隨機檔名
                    var trustedFileNameForFileStorage=Path.GetRandomFileName();
                    var path = Path.Combine(envir.ContentRootPath, envir.EnvironmentName, "unsafeUploads", trustedFileNameForFileStorage);
 
                    using (
                        var reader =
                            new FileStream(
                                path,
                                FileMode.Create))
                    {
                        await formFile.OpenReadStream(maxFileSize).CopyToAsync(reader);
                    }

                }
                catch (Exception ex)
                {
                    modelState.AddModelError(formFile.Name,

                                             $"The {fieldDisplayName}file ({fileName}) upload failed. " +

                                             $"Please contact the Help Desk for support. Error: {ex.Message}");
             //return ex.Message;
               throw ex;
                }
            }
            return string.Empty;
        }

    }

}

 

 

二、新增前端程式碼

1.  在Visual Studio 2022的解決方案資源管理器中,滑鼠右鍵單擊「Pages」資料夾。在彈出選單中選擇,新增-->Razor元件。如下圖。

 

2.在彈出對話方塊,名稱中輸入FileUpload1.razor。如下圖。

3. 在Visual Studio 2022的解決方案資源管理器中,滑鼠左鍵雙擊「Pages\FileUpload1.razor」檔案,在文字編輯器中開啟,在檔案的頂部新增@page指令。並新增如下程式碼。

@page "/FileUpload1"
@using BlazorAppDemo.Utils
@using Microsoft.AspNetCore.Mvc.ModelBinding
@inject IWebHostEnvironment Environ
<h3>多檔案上傳範例</h3>
<p>
    <label>
        提示資訊:@Message
 
    </label>
</p>
<p>
    <label>
        上傳檔案最大可以為:<input type="number" @bind="maxFileSize"/>位元組
 
    </label>
</p>
<p>
    <label>
        一次可上傳:<input type="number" @bind="maxAllowedFiles" />個檔案
 
    </label>
</p>
<p>
    <label>
        選擇上傳檔案:<InputFile OnChange="@LoadFiles" multiple />
 
    </label>
</p>
@if (isLoading)
{
    <p>檔案上傳中......</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>檔名:@file.Name</li>
                    <li>最後修改時間:@file.LastModified.ToString()</li>
                    <li>檔案大小(byte):@file.Size</li>
                    <li>檔案型別:@file.ContentType</li>
                </ul>
            </li>
           
        }
 
    </ul>
}
 
@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 18;
    private int maxAllowedFiles = 2;
    private bool isLoading;
    private string Message = string.Empty;
 
    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();
        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                ModelStateDictionary modelState = new ModelStateDictionary();
                loadedFiles.Add(file);
                string result=  await FileHelpers.ProcessFormFile(file, modelState, Environ, maxFileSize);

                if (string.IsNullOrEmpty(result))
                {
                    Message = "上傳成功!";
                }else
                Message = "上傳失敗!";
            }
            catch (Exception ex)
            {
                Message = ex.Message;
               
            }
        }
        isLoading = false;
    }
}

 

 


4. 在Visual Studio 2022的解決方案資源管理器中,滑鼠左鍵雙擊「Shared\NavMenu.razor」檔案,在文字編輯器中開啟,我們在此文中新增指向上傳檔案的選單。具體程式碼如下:

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorAppDemo</a>

        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>
 
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="BookIndex">
                <span class="oi oi-list-rich" aria-hidden="true"></span> 圖書列表
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="AddBook">
                <span class="oi oi-list-rich" aria-hidden="true"></span> 新增圖書
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="FileUpload1">
                <span class="oi oi-list-rich" aria-hidden="true"></span> 上傳檔案
            </NavLink>
        </div>
    </nav>
</div>

 
@code {
   private bool collapseNavMenu = true;

     private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {

        collapseNavMenu = !collapseNavMenu;

    }

} 

     5. 在Visual Studio 2022的選單欄上,找到「偵錯à開始偵錯」或是按F5鍵,Visual Studio 2022會生成BlazorAppDemo應用程式,並在瀏覽器中開啟Home頁面,我們使用滑鼠點選左邊的選單欄上的「上傳檔案」選單項,頁面會進入「FileUpload1」頁面,我們會看到我們寫的圖書列表頁面,如下圖。

 

6. 我們在「多檔案上傳範例」中選擇一個上傳檔案,然後應用程式會自動上傳檔案,但是卻會提示錯誤,錯誤資訊如下圖中1處,指明「找不到路徑的一部分」。我們開啟資源管理器,在專案中找一下圖中2處的目錄,發現沒有這樣的目錄結構。我們手動建立一下即可。

 

7. 我們在「多檔案上傳範例」中選擇一個上傳檔案,然後應用程式會自動上傳檔案,上傳到到目錄中卻不是我們選擇的檔名,是一個隨機的檔名。如下圖。