przekazywanie plików ASP.NET Core Blazor
Uwaga
Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.
Ostrzeżenie
Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz .NET i .NET Core Support Policy (Zasady obsługi platformy .NET Core). Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.
Ważne
Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.
Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.
W tym artykule wyjaśniono, jak przekazywać pliki za Blazor pomocą InputFile składnika.
Operacje przekazywania plików
Ostrzeżenie
Zawsze przestrzegaj najlepszych rozwiązań w zakresie zabezpieczeń, gdy zezwalają użytkownikom na przekazywanie plików. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
InputFile Użyj składnika, aby odczytać dane pliku przeglądarki do kodu platformy .NET. Składnik InputFile renderuje element HTML <input>
typu file
dla przekazywania pojedynczych plików. multiple
Dodaj atrybut, aby zezwolić użytkownikowi na przekazywanie wielu plików jednocześnie.
Wybór pliku nie jest skumulowany w przypadku używania składnika lub jego bazowego InputFile kodu HTML <input type="file">
, więc nie można dodawać plików do istniejącego zaznaczenia pliku. Składnik zawsze zastępuje początkowy wybór pliku użytkownika, więc odwołania do plików z poprzednich wyborów nie są dostępne.
Poniższy InputFile składnik wykonuje metodę LoadFiles
, gdy OnChange wystąpi zdarzenie (change
). Element InputFileChangeEventArgs zapewnia dostęp do wybranej listy plików i szczegółów dotyczących każdego pliku:
<InputFile OnChange="LoadFiles" multiple />
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
Renderowany kod HTML:
<input multiple="" type="file" _bl_2="">
Uwaga
W poprzednim przykładzie <input>
atrybut elementu _bl_2
jest używany do Blazorwewnętrznego przetwarzania.
Aby odczytać dane z wybranego przez użytkownika pliku, wywołaj IBrowserFile.OpenReadStream plik i odczytaj z zwróconego strumienia. Aby uzyskać więcej informacji, zobacz sekcję Strumienie plików.
OpenReadStream wymusza maksymalny rozmiar w bajtach jego Streamwartości . Odczytywanie jednego pliku lub wielu plików większych niż 500 KB powoduje wyjątek. Ten limit uniemożliwia deweloperom przypadkowe odczytywanie dużych plików do pamięci. Parametr maxAllowedSize
parametru można użyć do określenia większego rozmiaru OpenReadStream , jeśli jest to wymagane.
Jeśli potrzebujesz dostępu do elementu Stream reprezentującego bajty pliku, użyj polecenia IBrowserFile.OpenReadStream. Unikaj odczytywania przychodzącego strumienia plików bezpośrednio do pamięci jednocześnie. Na przykład nie kopiuj wszystkich bajtów pliku do MemoryStream ani nie odczytaj całego strumienia do tablicy bajtów jednocześnie. Te podejścia mogą spowodować obniżenie wydajności aplikacji i potencjalne ryzyko odmowy usługi (DoS ), szczególnie w przypadku składników po stronie serwera. Zamiast tego należy rozważyć przyjęcie jednego z następujących podejść:
- Skopiuj strumień bezpośrednio do pliku na dysku bez odczytywania go do pamięci. Należy pamiętać, że Blazor aplikacje wykonujące kod na serwerze nie mogą bezpośrednio uzyskać dostępu do systemu plików klienta.
- Przekazywanie plików z klienta bezpośrednio do usługi zewnętrznej. Aby uzyskać więcej informacji, zobacz sekcję Przekazywanie plików do usługi zewnętrznej.
W poniższych przykładach browserFile
reprezentuje przekazany plik i implementuje IBrowserFileelement . Działające implementacje programu IBrowserFile są wyświetlane w składnikach przekazywania plików w dalszej części tego artykułu.
Obsługiwane: zalecane jest następujące podejście, ponieważ plik Stream jest udostępniany bezpośrednio użytkownikowi, czyli plik, który FileStream tworzy plik w podanej ścieżce:
await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);
Obsługiwane: w przypadku usługi Microsoft Azure Blob Storage zalecane jest następujące podejście, ponieważ plik Stream jest dostarczany bezpośrednio do UploadBlobAsyncusługi :
await blobContainerClient.UploadBlobAsync(
trustedFileName, browserFile.OpenReadStream());
Niezalecane: Następujące podejście nie jest zalecane , ponieważ zawartość pliku Stream jest odczytywana w String pamięci (reader
):
var reader =
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();
Niezalecane: następujące podejście nie jest zalecane w przypadku usługi Microsoft Azure Blob Storage , ponieważ zawartość pliku Stream jest kopiowana do MemoryStream pamięci (memoryStream
) przed wywołaniem metody UploadBlobAsync:
var memoryStream = new MemoryStream();
await browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFileName, memoryStream));
Składnik odbierający plik obrazu może wywołać BrowserFileExtensions.RequestImageFileAsync metodę wygody w pliku, aby zmienić rozmiar danych obrazu w środowisku uruchomieniowym JavaScript przeglądarki przed przesyłaniem strumieniowym obrazu do aplikacji. Przypadki użycia wywołania RequestImageFileAsync są najbardziej odpowiednie dla Blazor WebAssembly aplikacji.
Autofac Inversion of Control (IoC) container users (Autofac Inversion of Control) users (Autofac Inversion of Control ( IoC) container users (Autofac In
Jeśli używasz kontenera Autofac Inversion of Control (IoC) zamiast wbudowanego kontenera iniekcji zależności ASP.NET Core, ustaw wartość DisableImplicitFromServicesParameters na true
w opcjach centrum obsługi obwodu po stronie serwera. Aby uzyskać więcej informacji, zobacz FileUpload: Nie odebrano żadnych danych w czasie przydzielonym (dotnet/aspnetcore
#38842).
Limity odczytu i przekazywania rozmiaru pliku
Po stronie serwera lub po stronie klienta nie ma limitu rozmiaru InputFile odczytu ani przekazywania pliku specjalnie dla składnika. Jednak po stronie Blazor klienta odczytuje bajty pliku do pojedynczego buforu tablicy JavaScript podczas marshalingu danych z języka JavaScript do języka C#, który jest ograniczony do 2 GB lub do dostępnej pamięci urządzenia. Przekazywanie dużych plików (> 250 MB) może zakończyć się niepowodzeniem w przypadku przekazywania po stronie klienta przy użyciu InputFile składnika. Aby uzyskać więcej informacji, zobacz następujące dyskusje:
Maksymalny obsługiwany rozmiar pliku dla InputFile składnika to 2 GB. Ponadto po stronie Blazor klienta odczytuje bajty pliku do pojedynczego buforu tablicy JavaScript podczas marshalingu danych z języka JavaScript do języka C#, który jest ograniczony do 2 GB lub do dostępnej pamięci urządzenia. Przekazywanie dużych plików (> 250 MB) może zakończyć się niepowodzeniem w przypadku przekazywania po stronie klienta przy użyciu InputFile składnika. Aby uzyskać więcej informacji, zobacz następujące dyskusje:
- Składnik Blazor InputFile powinien obsługiwać fragmentowanie podczas przekazywania pliku (dotnet/runtime #84685)
- Żądanie przekazywania strumieniowego za pośrednictwem programu obsługi http (dotnet/runtime #36634)
W przypadku przekazywania dużych plików po stronie klienta, które kończą się niepowodzeniem podczas próby użycia InputFile składnika, zalecamy fragmentowanie dużych plików za pomocą składnika niestandardowego przy użyciu wielu żądań zakresu HTTP zamiast używania InputFile składnika.
Obecnie zaplanowano pracę dla platformy .NET 9 (pod koniec 2024 r.), aby rozwiązać problem z ograniczeniem przekazywania plików po stronie klienta.
Przykłady
W poniższych przykładach pokazano wiele przekazywania plików w składniku. InputFileChangeEventArgs.GetMultipleFiles umożliwia odczytywanie wielu plików. Określ maksymalną liczbę plików, aby uniemożliwić złośliwemu użytkownikowi przekazywanie większej liczby plików niż oczekiwana przez aplikację. InputFileChangeEventArgs.File umożliwia odczytywanie pierwszego pliku i tylko wtedy, gdy przekazywanie pliku nie obsługuje wielu plików.
InputFileChangeEventArgs znajduje się w Microsoft.AspNetCore.Components.Forms przestrzeni nazw, która jest zazwyczaj jedną z przestrzeni nazw w pliku aplikacji _Imports.razor
. Gdy przestrzeń nazw znajduje się w _Imports.razor
pliku, zapewnia dostęp do składowych interfejsu API do składników aplikacji.
Przestrzenie nazw w _Imports.razor
pliku nie są stosowane do plików C# (.cs
). Pliki języka C# wymagają jawnej using
dyrektywy w górnej części pliku klasy:
using Microsoft.AspNetCore.Components.Forms;
W przypadku testowania składników przekazywania plików można utworzyć pliki testowe o dowolnym rozmiarze za pomocą programu PowerShell:
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
W powyższym poleceniu:
- Symbol
{SIZE}
zastępczy to rozmiar pliku w bajtach (na przykład2097152
dla pliku o rozmiarze 2 MB). - Symbol
{PATH}
zastępczy to ścieżka i plik z rozszerzeniem pliku (na przykładD:/test_files/testfile2MB.txt
).
Przykład przekazywania pliku po stronie serwera
Aby użyć następującego kodu, utwórz Development/unsafe_uploads
folder w katalogu głównym aplikacji działającej Development
w środowisku.
Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads
folder dla Staging
środowiska. Production/unsafe_uploads
Utwórz folder dla Production
środowiska.
Ostrzeżenie
Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
FileUpload1.razor
:
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileName);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
Przykład przekazywania pliku po stronie klienta
Poniższy przykład przetwarza bajty plików i nie wysyła plików do miejsca docelowego poza aplikacją. Razor Przykładowy składnik, który wysyła plik do serwera lub usługi, zobacz następujące sekcje:
- Przekazywanie plików na serwer za pomocą renderowania po stronie klienta (CSR)
- Przekazywanie plików do usługi zewnętrznej
Składnik zakłada, że tryb renderowania interactive WebAssembly (InteractiveWebAssembly
) jest dziedziczony z składnika nadrzędnego lub stosowany globalnie do aplikacji.
@page "/file-upload-1"
@inject ILogger<FileUpload1> Logger
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
IBrowserFile zwraca metadane uwidocznione przez przeglądarkę jako właściwości. Użyj tych metadanych do wstępnej weryfikacji.
Nigdy nie ufaj wartościom poprzednich właściwości, zwłaszcza Name właściwości do wyświetlania w interfejsie użytkownika. Traktuj wszystkie dane dostarczone przez użytkownika jako istotne zagrożenie bezpieczeństwa aplikacji, serwera i sieci. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
Przekazywanie plików na serwer za pomocą renderowania po stronie serwera
Ta sekcja dotyczy składników interactive server w Blazor Web Appsystemach lub Blazor Server aplikacjach.
W poniższym przykładzie pokazano przekazywanie plików z aplikacji po stronie serwera do kontrolera internetowego interfejsu API zaplecza w oddzielnej aplikacji, prawdopodobnie na osobnym serwerze.
W pliku aplikacji Program
po stronie serwera dodaj IHttpClientFactory i powiązane usługi, które umożliwiają aplikacji tworzenie HttpClient wystąpień:
builder.Services.AddHttpClient();
Aby uzyskać więcej informacji, zobacz Tworzenie żądań HTTP za pomocą interfejsu IHttpClientFactory na platformie ASP.NET Core.
Przykłady w tej sekcji:
- Internetowy interfejs API jest uruchamiany pod adresem URL:
https://localhost:5001
- Aplikacja po stronie serwera działa pod adresem URL:
https://localhost:5003
Na potrzeby testowania poprzednie adresy URL są konfigurowane w plikach projektów Properties/launchSettings.json
.
Poniższa UploadResult
klasa zachowuje wynik przekazanego pliku. Gdy przekazywanie pliku na serwerze nie powiedzie się, zostanie zwrócony ErrorCode
kod błędu w celu wyświetlenia użytkownikowi. Bezpieczna nazwa pliku jest generowana na serwerze dla każdego pliku i zwracana do klienta w StoredFileName
programie w celu wyświetlenia. Pliki są kluczami między klientem a serwerem przy użyciu niebezpiecznej/niezaufanej nazwy pliku w programie FileName
.
UploadResult.cs
:
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
Najlepszym rozwiązaniem w zakresie zabezpieczeń dla aplikacji produkcyjnych jest unikanie wysyłania komunikatów o błędach do klientów, którzy mogą ujawniać poufne informacje o aplikacji, serwerze lub sieci. Udostępnianie szczegółowych komunikatów o błędach może pomóc złośliwego użytkownika w opracowywaniu ataków na aplikację, serwer lub sieć. Przykładowy kod w tej sekcji wysyła tylko numer kodu błędu (int
) do wyświetlenia po stronie klienta składnika, jeśli wystąpi błąd po stronie serwera. Jeśli użytkownik wymaga pomocy przy przekazaniu pliku, podaj kod błędu pomocy technicznej dla personelu pomocy technicznej w celu rozwiązania biletu pomocy technicznej bez znajomości dokładnej przyczyny błędu.
Poniższa LazyBrowserFileStream
klasa definiuje niestandardowy typ strumienia, który leniwie wywołuje OpenReadStream tuż przed żądaniem pierwszych bajtów strumienia. Strumień nie jest przesyłany z przeglądarki do serwera do momentu rozpoczęcia odczytu strumienia na platformie .NET.
LazyBrowserFileStream.cs
:
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
: Stream
{
private readonly IBrowserFile file = file;
private readonly int maxAllowedSize = maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public override void Flush() => underlyingStream?.Flush();
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen() =>
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
FileUpload2
Następujący składnik:
- Zezwala użytkownikom na przekazywanie plików z klienta.
- Wyświetla niezaufaną/niebezpieczną nazwę pliku podaną przez klienta w interfejsie użytkownika. Niezaufana/niebezpieczna nazwa pliku jest automatycznie zakodowana w Razor formacie HTML w celu bezpiecznego wyświetlania w interfejsie użytkownika.
Ostrzeżenie
Nie ufaj nazwam plików dostarczonym przez klientów dla:
- Zapisywanie pliku w systemie plików lub usłudze.
- Wyświetlaj w interfejsach użytkownika, które nie kodują automatycznie nazw plików ani za pośrednictwem kodu dewelopera.
Aby uzyskać więcej informacji na temat zagadnień dotyczących zabezpieczeń podczas przekazywania plików na serwer, zobacz Przekazywanie plików w programie ASP.NET Core.
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Any())
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
Jeśli składnik ogranicza przekazywanie plików do pojedynczego pliku w danym momencie lub jeśli składnik przyjmuje tylko interakcyjne renderowanie po stronie klienta (CSR, InteractiveWebAssembly
), składnik może uniknąć użycia LazyBrowserFileStream
elementu i użyć elementu Stream. Poniżej przedstawiono zmiany składnika FileUpload2
:
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
Usuń klasę LazyBrowserFileStream
(LazyBrowserFileStream.cs
), ponieważ nie jest używana.
Jeśli składnik ogranicza przekazywanie plików do pojedynczego pliku w danym momencie, składnik może uniknąć użycia LazyBrowserFileStream
elementu i użyć elementu Stream. Poniżej przedstawiono zmiany składnika FileUpload2
:
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
Usuń klasę LazyBrowserFileStream
(LazyBrowserFileStream.cs
), ponieważ nie jest używana.
Następujący kontroler w projekcie internetowego interfejsu API zapisuje przekazane pliki z klienta.
Ważne
Kontroler w tej sekcji jest przeznaczony do użycia w osobnym projekcie internetowego interfejsu Blazor API z aplikacji. Internetowy interfejs API powinien ograniczyć ryzyko fałszowania żądań między witrynami (XSRF/CSRF) w przypadku uwierzytelnienia użytkowników przekazywania plików.
Uwaga
Powiązanie wartości formularza z atrybutem[FromForm]
nie jest natywnie obsługiwane dla minimalnych interfejsów API w programie ASP.NET Core na platformie .NET 6. W związku z tym nie można przekonwertować następującego Filesave
przykładu kontrolera w celu użycia interfejsów API minimalnych. Obsługa powiązań z wartości formularza z minimalnymi interfejsami API jest dostępna w programie ASP.NET Core na platformie .NET 7 lub nowszym.
Aby użyć następującego kodu, utwórz Development/unsafe_uploads
folder w katalogu głównym projektu internetowego interfejsu API dla aplikacji działającej Development
w środowisku.
Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads
folder dla Staging
środowiska. Production/unsafe_uploads
Utwórz folder dla Production
środowiska.
Ostrzeżenie
Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
Controllers/FilesaveController.cs
:
using System.Net;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
W poprzednim kodzie GetRandomFileName jest wywoływana w celu wygenerowania bezpiecznej nazwy pliku. Nigdy nie ufaj nazwie pliku podanej przez przeglądarkę, ponieważ cyberattacker może wybrać istniejącą nazwę pliku, która zastępuje istniejący plik lub wysyła ścieżkę, która próbuje zapisać poza aplikacją.
Aplikacja serwera musi rejestrować usługi kontrolera i punkty końcowe kontrolera mapy. Aby uzyskać więcej informacji, zobacz Routing do akcji kontrolera w ASP.NET Core.
Przekazywanie plików na serwer za pomocą renderowania po stronie klienta (CSR)
Ta sekcja dotyczy składników renderowanych po stronie klienta (CSR) w Blazor Web Appaplikacjach lub s.Blazor WebAssembly
W poniższym przykładzie pokazano przekazywanie plików do internetowego kontrolera interfejsu API zaplecza w oddzielnej aplikacji, na przykład na osobnym serwerze, od składnika Blazor Web App , który przyjmuje csr lub składnik w Blazor WebAssembly aplikacji.
Poniższa UploadResult
klasa zachowuje wynik przekazanego pliku. Gdy przekazywanie pliku na serwerze nie powiedzie się, zostanie zwrócony ErrorCode
kod błędu w celu wyświetlenia użytkownikowi. Bezpieczna nazwa pliku jest generowana na serwerze dla każdego pliku i zwracana do klienta w StoredFileName
programie w celu wyświetlenia. Pliki są kluczami między klientem a serwerem przy użyciu niebezpiecznej/niezaufanej nazwy pliku w programie FileName
.
UploadResult.cs
:
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
Uwaga
Poprzednia UploadResult
klasa może być współdzielona między projektami klienckimi i serwerowymi. Gdy projekty klienta i serwera współużytkuje klasę, dodaj import do pliku każdego projektu _Imports.razor
dla udostępnionego projektu. Na przykład:
@using BlazorSample.Shared
FileUpload2
Następujący składnik:
- Zezwala użytkownikom na przekazywanie plików z klienta.
- Wyświetla niezaufaną/niebezpieczną nazwę pliku podaną przez klienta w interfejsie użytkownika. Niezaufana/niebezpieczna nazwa pliku jest automatycznie zakodowana w Razor formacie HTML w celu bezpiecznego wyświetlania w interfejsie użytkownika.
Najlepszym rozwiązaniem w zakresie zabezpieczeń dla aplikacji produkcyjnych jest unikanie wysyłania komunikatów o błędach do klientów, którzy mogą ujawniać poufne informacje o aplikacji, serwerze lub sieci. Udostępnianie szczegółowych komunikatów o błędach może pomóc złośliwego użytkownika w opracowywaniu ataków na aplikację, serwer lub sieć. Przykładowy kod w tej sekcji wysyła tylko numer kodu błędu (int
) do wyświetlenia po stronie klienta składnika, jeśli wystąpi błąd po stronie serwera. Jeśli użytkownik wymaga pomocy przy przekazaniu pliku, podaj kod błędu pomocy technicznej dla personelu pomocy technicznej w celu rozwiązania biletu pomocy technicznej bez znajomości dokładnej przyczyny błędu.
Ostrzeżenie
Nie ufaj nazwam plików dostarczonym przez klientów dla:
- Zapisywanie pliku w systemie plików lub usłudze.
- Wyświetlaj w interfejsach użytkownika, które nie kodują automatycznie nazw plików ani za pośrednictwem kodu dewelopera.
Aby uzyskać więcej informacji na temat zagadnień dotyczących zabezpieczeń podczas przekazywania plików na serwer, zobacz Przekazywanie plików w programie ASP.NET Core.
W głównym projekcie Blazor Web App dodaj IHttpClientFactory i powiązane usługi w pliku projektu Program
:
builder.Services.AddHttpClient();
Usługi HttpClient
należy dodać do głównego projektu, ponieważ składnik po stronie klienta jest wstępnie zainstalowany na serwerze. Jeśli wyłączysz prerendering dla następującego składnika, nie musisz udostępniać HttpClient
usług w głównej aplikacji i nie musisz dodawać poprzedniego wiersza do głównego projektu.
Aby uzyskać więcej informacji na temat dodawania HttpClient
usług do aplikacji ASP.NET Core, zobacz Make HTTP requests using IHttpClientFactory in ASP.NET Core (Tworzenie żądań HTTP przy użyciu elementu IHttpClientFactory w programie ASP.NET Core).
Projekt klienta (.Client
) Blazor Web App obiektu musi również zarejestrować żądania HttpClient HTTP POST na kontrolerze internetowego interfejsu API zaplecza. Potwierdź lub dodaj następujący kod do pliku projektu Program
klienta:
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
W poprzednim przykładzie ustawiono adres podstawowy (IWebAssemblyHostEnvironment.BaseAddress), który pobiera adres builder.HostEnvironment.BaseAddress
podstawowy dla aplikacji i jest zwykle pobierany z <base>
wartości tagu href
na stronie hosta. Jeśli wywołujesz zewnętrzny internetowy interfejs API, ustaw identyfikator URI na adres podstawowy internetowego interfejsu API.
Określ atrybut Tryb renderowania interakcyjnego zestawu WebAssembly w górnej części następującego składnika w elemencie Blazor Web App:
@rendermode InteractiveWebAssembly
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName);
if (result is null)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result = new();
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
Następujący kontroler w projekcie po stronie serwera zapisuje przekazane pliki z klienta.
Uwaga
Powiązanie wartości formularza z atrybutem[FromForm]
nie jest natywnie obsługiwane dla minimalnych interfejsów API w programie ASP.NET Core na platformie .NET 6. W związku z tym nie można przekonwertować następującego Filesave
przykładu kontrolera w celu użycia interfejsów API minimalnych. Obsługa powiązań z wartości formularza z minimalnymi interfejsami API jest dostępna w programie ASP.NET Core na platformie .NET 7 lub nowszym.
Aby użyć następującego kodu, utwórz Development/unsafe_uploads
folder w katalogu głównym projektu po stronie serwera dla aplikacji działającej Development
w środowisku.
Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads
folder dla Staging
środowiska. Production/unsafe_uploads
Utwórz folder dla Production
środowiska.
Ostrzeżenie
Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
W poniższym przykładzie zaktualizuj przestrzeń nazw udostępnionego projektu tak, aby odpowiadała projektowi udostępnionemu, jeśli udostępniony projekt dostarcza klasę UploadResult
.
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
W poprzednim kodzie GetRandomFileName jest wywoływana w celu wygenerowania bezpiecznej nazwy pliku. Nigdy nie ufaj nazwie pliku podanej przez przeglądarkę, ponieważ cyberattacker może wybrać istniejącą nazwę pliku, która zastępuje istniejący plik lub wysyła ścieżkę, która próbuje zapisać poza aplikacją.
Aplikacja serwera musi rejestrować usługi kontrolera i punkty końcowe kontrolera mapy. Aby uzyskać więcej informacji, zobacz Routing do akcji kontrolera w ASP.NET Core.
Anulowanie przekazywania pliku
Składnik przekazywania plików może wykryć, kiedy użytkownik anulował przekazywanie przy użyciu CancellationToken elementu podczas wywoływania elementu IBrowserFile.OpenReadStream lub StreamReader.ReadAsync.
Utwórz element CancellationTokenSource dla InputFile
składnika. Na początku OnInputFileChange
metody sprawdź, czy poprzednie przekazywanie jest w toku.
Jeśli przekazywanie pliku jest w toku:
- Wywołaj Cancel poprzednią metodę przekazywania.
- Utwórz nowy CancellationTokenSource element dla następnego przekazywania i przekaż element CancellationTokenSource.Token do lub ReadAsyncOpenReadStream .
Przekazywanie plików po stronie serwera z postępem
W poniższym przykładzie pokazano, jak przekazywać pliki w aplikacji po stronie serwera z postępem przekazywania wyświetlanym użytkownikowi.
Aby użyć następującego przykładu w aplikacji testowej:
- Utwórz folder do zapisania przekazanych plików dla
Development
środowiska:Development/unsafe_uploads
. - Skonfiguruj maksymalny rozmiar pliku (
maxFileSize
15 KB w poniższym przykładzie) i maksymalną liczbę dozwolonych plików (maxAllowedFiles
3 w poniższym przykładzie). - Ustaw bufor na inną wartość (10 KB w poniższym przykładzie), jeśli jest to konieczne, aby zwiększyć stopień szczegółowości w toku raportowania. Nie zalecamy używania buforu większego niż 30 KB z powodu problemów z wydajnością i zabezpieczeniami.
Ostrzeżenie
Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
FileUpload3.razor
:
@page "/file-upload-3"
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 3</PageTitle>
<h1>File Upload Example 3</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
Aby uzyskać więcej informacji, zobacz następujące zasoby interfejsu API:
- FileStream: udostępnia Stream element dla pliku, który obsługuje zarówno operacje synchroniczne, jak i asynchroniczne operacji odczytu i zapisu.
- FileStream.ReadAsync: Poprzedni
FileUpload3
składnik odczytuje strumień asynchronicznie za pomocą polecenia ReadAsync. Odczytywanie strumienia synchronicznie z elementem Read nie jest obsługiwane w Razor składnikach.
Strumienie plików
W przypadku interakcyjności serwera dane plików są przesyłane strumieniowo za pośrednictwem SignalR połączenia z kodem platformy .NET na serwerze, ponieważ plik jest odczytywany.
RemoteBrowserFileStreamOptions umożliwia konfigurowanie właściwości przekazywania plików.
W przypadku składnika renderowanego za pomocą zestawu WebAssembly dane plików są przesyłane strumieniowo bezpośrednio do kodu platformy .NET w przeglądarce.
Przekazywanie podglądu obrazu
Aby uzyskać podgląd obrazu przekazywania obrazów, zacznij od dodania InputFile
składnika z odwołaniem do składnika i procedurą OnChange
obsługi:
<InputFile @ref="inputFile" OnChange="ShowPreview" />
Dodaj element obrazu z odwołaniem do elementu, który służy jako symbol zastępczy podglądu obrazu:
<img @ref="previewImageElem" />
Dodaj skojarzone odwołania:
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
}
W języku JavaScript dodaj funkcję o nazwie z kodem HTML input
i img
elementem, który wykonuje następujące czynności:
- Wyodrębnia wybrany plik.
- Tworzy adres URL obiektu za pomocą polecenia
createObjectURL
. - Ustawia odbiornik zdarzeń, aby odwołać adres URL obiektu za pomocą
revokeObjectURL
polecenia po załadowaniu obrazu, więc pamięć nie zostanie ujawniona. img
Ustawia źródło elementu, aby wyświetlić obraz.
window.previewImage = (inputElem, imgElem) => {
const url = URL.createObjectURL(inputElem.files[0]);
imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
imgElem.src = url;
}
Na koniec użyj wstrzykniętego IJSRuntime elementu , aby dodać procedurę OnChange
obsługi, która wywołuje funkcję JavaScript:
@inject IJSRuntime JS
...
@code {
...
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
Powyższy przykład dotyczy przekazywania pojedynczego obrazu. Podejście można rozszerzyć w celu obsługi multiple
obrazów.
FileUpload4
Poniższy składnik przedstawia kompletny przykład.
FileUpload4.razor
:
@page "/file-upload-4"
@inject IJSRuntime JS
<h1>File Upload Example</h1>
<InputFile @ref="inputFile" OnChange="ShowPreview" />
<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
@page "/file-upload-4"
@inject IJSRuntime JS
<h1>File Upload Example</h1>
<InputFile @ref="inputFile" OnChange="ShowPreview" />
<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
Przekazywanie plików do usługi zewnętrznej
Zamiast aplikacji obsługującej bajty przekazywania plików i serwera aplikacji odbierającego przekazane pliki, klienci mogą bezpośrednio przekazywać pliki do usługi zewnętrznej. Aplikacja może bezpiecznie przetwarzać pliki z usługi zewnętrznej na żądanie. Takie podejście wzmacnia zabezpieczenia aplikacji i jej serwera przed złośliwymi atakami i potencjalnymi problemami z wydajnością.
Rozważmy podejście korzystające z usługi Azure Files, Azure Blob Storage lub usługi innej firmy z następującymi potencjalnymi korzyściami:
- Przekazywanie plików z klienta bezpośrednio do usługi zewnętrznej przy użyciu biblioteki klienta języka JavaScript lub REST interfejsu API. Na przykład platforma Azure oferuje następujące biblioteki klienta i interfejsy API:
- Autoryzowanie przekazywania przez użytkownika za pomocą tokenu sygnatury dostępu współdzielonego (SAS) delegowanego przez użytkownika wygenerowanego przez aplikację (po stronie serwera) dla każdego przekazywania pliku klienta. Na przykład platforma Azure oferuje następujące funkcje sygnatury dostępu współdzielonego:
- Zapewnij automatyczną nadmiarowość i kopię zapasową udziału plików.
- Ogranicz przekazywanie przy użyciu limitów przydziału. Należy pamiętać, że limity przydziału usługi Azure Blob Storage są ustawiane na poziomie konta, a nie na poziomie kontenera. Jednak limity przydziału usługi Azure Files są na poziomie udziału plików i mogą zapewnić lepszą kontrolę nad limitami przekazywania. Aby uzyskać więcej informacji, zobacz dokumenty platformy Azure połączone wcześniej na tej liście.
- Zabezpieczanie plików przy użyciu szyfrowania po stronie serwera (SSE).
Aby uzyskać więcej informacji na temat usług Azure Blob Storage i Azure Files, zobacz dokumentację usługi Azure Storage.
Limit rozmiaru komunikatów po stronie SignalR serwera
Przekazywanie plików może zakończyć się niepowodzeniem nawet przed rozpoczęciem, gdy Blazor pobiera dane dotyczące plików, które przekraczają maksymalny SignalR rozmiar komunikatu.
SignalR Definiuje limit rozmiaru komunikatów, który ma zastosowanie do każdego odbieranego komunikatu Blazor , a InputFile składnik przesyła strumieniowo pliki do serwera w komunikatach, które przestrzegają skonfigurowanego limitu. Jednak pierwszy komunikat, który wskazuje zestaw plików do przekazania, jest wysyłany jako unikatowy pojedynczy komunikat. Rozmiar pierwszego komunikatu może przekroczyć limit rozmiaru komunikatu SignalR . Problem nie jest związany z rozmiarem plików, jest związany z liczbą plików.
Zarejestrowany błąd jest podobny do następującego:
Błąd: Połączenie zostało rozłączone z powodu błędu "Błąd: Serwer zwrócił błąd podczas zamykania: połączenie zostało zamknięte z powodu błędu". e.log @ blazor.server.js:1
Podczas przekazywania plików osiągnięcie limitu rozmiaru komunikatu dla pierwszego komunikatu jest rzadkie. Jeśli limit zostanie osiągnięty, aplikacja może skonfigurować HubOptions.MaximumReceiveMessageSize z większą wartością.
Aby uzyskać więcej informacji na SignalR temat konfiguracji i sposobu ustawiania MaximumReceiveMessageSizeprogramu , zobacz wskazówki dotyczące platformy ASP.NET CoreBlazorSignalR.
Maksymalna liczba wywołań równoległych na ustawienie centrum klienta
Blazor wartość jest ustawiona na MaximumParallelInvocationsPerClient wartość 1, która jest wartością domyślną.
Zwiększenie wartości prowadzi do wysokiego prawdopodobieństwa, że CopyTo
operacje zgłaszają wartość System.InvalidOperationException: 'Reading is not allowed after reader was completed.'
. Aby uzyskać więcej informacji, zobacz MaximumParallelInvocationsPerClient > 1 przerywa przekazywanie plików w Blazor Server trybie (dotnet/aspnetcore
#53951).
Rozwiązywanie problemów
Wiersz, który wywołuje IBrowserFile.OpenReadStream element , zgłasza element System.TimeoutException:
System.TimeoutException: Did not receive any data in the allotted time.
Możliwe przyczyny:
Używanie kontenera Autofac Inversion of Control (IoC) zamiast wbudowanego kontenera iniekcji zależności ASP.NET Core. Aby rozwiązać ten problem, ustaw wartość DisableImplicitFromServicesParameters na
true
w opcjach centrum obsługi obwodu po stronie serwera. Aby uzyskać więcej informacji, zobacz FileUpload: Nie odebrano żadnych danych w czasie przydzielonym (dotnet/aspnetcore
#38842).Nie odczytuje strumienia do ukończenia. Nie jest to problem ze strukturą. Załapuj wyjątek i zbadaj go dalej w środowisku lokalnym/sieci.
- Używanie renderowania po stronie serwera i wywoływania OpenReadStream wielu plików przed odczytaniem ich do ukończenia. Aby rozwiązać ten problem, użyj
LazyBrowserFileStream
klasy i podejścia opisanego w sekcji Przekazywanie plików do serwera z renderowaniem po stronie serwera w tym artykule.