Как хранить данные сеанса в серверной среде
В серверном приложении Blazor я хотел бы сохранить некоторое состояние, которое сохраняется между переходами по страницам. Как мне это сделать?
Обычное состояние сеанса ASP.NET Core, по-видимому, недоступно, поскольку, скорее всего, применимо следующее примечание в разделе Сеанс и приложение в ASP.NET Core:
Сеанс не поддерживается в приложениях SignalR, потому что SignalR Hub может выполняться независимо от контекста HTTP. Например, это может произойти, когда длинный запрос опроса остается открытым концентратором после истечения срока действия контекста HTTP запроса.
Проблема GitHub Добавление поддержки в SignalR for Session упоминает, что вы можете использовать Context.Items. Но я понятия не имею, как его использовать, т.е. я не знаю, как получить доступ к HubConnectionContext
пример.
Каковы мои варианты для состояния сеанса?
9 ответов
@JohnB намекает на подход бедного человека к государству: пользуйтесь услугами с ограниченным доступом. В серверной части Blazor область обслуживания привязана к соединению SignalR. Это самая близкая вещь к сессии, которую вы можете получить. Это, безусловно, личное для одного пользователя. Но это также легко теряется. Перезагрузка страницы или изменение URL в списке адресов браузера загружает, запускает новое соединение SignalR, создает новый экземпляр службы и тем самым теряет состояние.
Итак, сначала создайте государственную службу:
public class SessionState
{
public string SomeProperty { get; set; }
public int AnotherProperty { get; set; }
}
Затем настройте службу в классе запуска проекта App (не на сервере):
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<SessionState>();
}
public void Configure(IBlazorApplicationBuilder app)
{
app.AddComponent<Main>("app");
}
}
Теперь вы можете внедрить состояние в любую страницу Blazor:
@inject SessionState state
<p>@state.SomeProperty</p>
<p>@state.AnotherProperty</p>
Лучшие решения по-прежнему приветствуются.
Вот подходящее решение для ASP.NET Core 5.0+ (
ProtectedSessionStorage
,
ProtectedLocalStorage
): https://docs.microsoft.com/en-gb/aspnet/core/blazor/state-management?view=aspnetcore-5.0&amp;pivots=server
Пример:
@page "/"
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
User name: @UserName
<p/><input value="@UserName" @onchange="args => UserName = args.Value?.ToString()" />
<button class="btn btn-primary" @onclick="SaveUserName">Save</button>
@code {
private string UserName;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
UserName = (await ProtectedSessionStore.GetAsync<string>("UserName")).Value ?? "";
StateHasChanged();
}
}
private async Task SaveUserName() {
await ProtectedSessionStore.SetAsync("UserName", UserName);
}
}
Обратите внимание, что этот метод хранит данные в зашифрованном виде.
Стив Сандерсон подробно рассказывает о том, как спасти штат.
Для серверного Blazor вам нужно будет использовать любую реализацию хранилища на JavaScript, которая может быть файлами cookie, параметрами запроса или, например, вы можете использовать локальное / сеансовое хранилище.
В настоящее время существуют пакеты NuGet, реализующие это через IJSRuntime
как BlazorStorage илиMicrosoft.AspNetCore.ProtectedBrowserStorage
Сложность заключается в том, что серверный блейзер выполняет предварительную визуализацию страниц, поэтому ваш код представления Razor будет запускаться и выполняться на сервере еще до того, как он отобразится в браузере клиента. Это вызывает проблему, когдаIJSRuntime
и поэтому localStorage
в настоящее время недоступен. Вам нужно будет либо отключить предварительную отрисовку, либо подождать, пока сгенерированная сервером страница будет отправлена в браузер клиента, и установить соединение с сервером.
Во время предварительной отрисовки нет интерактивного подключения к браузеру пользователя, и у браузера еще нет страницы, на которой он мог бы запускать JavaScript. Таким образом, в это время невозможно взаимодействовать с localStorage или sessionStorage. Если вы попытаетесь, вы получите ошибку, похожую на то, что вызовы взаимодействия JavaScript не могут быть выполнены в настоящее время. Это потому, что компонент предварительно отрисовывается.
Чтобы отключить предварительную отрисовку:
(...) Откройте свой
_Host.razor
файл и удалите вызовHtml.RenderComponentAsync
. Затем откройте свойStartup.cs
файл и замените вызов наendpoints.MapBlazorHub()
сendpoints.MapBlazorHub<App>("app")
, гдеApp
- это тип вашего корневого компонента, а "app" - это селектор CSS, определяющий, где в документе должен быть размещен корневой компонент.
Если вы хотите продолжить предварительную отрисовку:
@inject YourJSStorageProvider storageProvider
bool isWaitingForConnection;
protected override async Task OnInitAsync()
{
if (ComponentContext.IsConnected)
{
// Looks like we're not prerendering, so we can immediately load
// the data from browser storage
string mySessionValue = storageProvider.GetKey("x-my-session-key");
}
else
{
// We are prerendering, so have to defer the load operation until later
isWaitingForConnection = true;
}
}
protected override async Task OnAfterRenderAsync()
{
// By this stage we know the client has connected back to the server, and
// browser services are available. So if we didn't load the data earlier,
// we should do so now, then trigger a new render.
if (isWaitingForConnection)
{
isWaitingForConnection = false;
//load session data now
string mySessionValue = storageProvider.GetKey("x-my-session-key");
StateHasChanged();
}
}
Теперь к фактическому ответу, в котором вы хотите сохранить состояние между страницами, вы должны использовать CascadingParameter
. Крис Сейнти объясняет это так:
Каскадные значения и параметры - это способ передать значение от компонента всем его потомкам без использования традиционных параметров компонента.
Это будет параметр, который будет классом, который содержит все ваши данные о состоянии и предоставляет методы, которые можно загружать / сохранять через выбранного вами поставщика хранилища. Это объясняется в блоге Криса Сэйнти, в заметке Стива Сандерсона или в документации Microsoft.
Обновление: Microsoft опубликовала новые документы, объясняющие управление состоянием Blazor
Обновление 2: обратите внимание, что в настоящее время BlazorStorage некорректно работает для серверного Blazor с последней предварительной версией.NET SDK. Вы можете следить за этой проблемой, где я опубликовал временное решение
Я нашел способ хранения пользовательских данных в сеансе на стороне сервера. Я сделал это, используя CircuitHandler Id в качестве «токена» для доступа пользователя к системе. Только имя пользователя и CircuitId сохраняются в клиентском LocalStorage (с использованием Blazored.LocalStorage); другие пользовательские данные хранятся на сервере. Я знаю, что это большой объем кода, но это был лучший способ, который я мог найти для защиты пользовательских данных на стороне сервера.
UserModel.cs (для LocalStorage на стороне клиента)
public class UserModel
{
public string Username { get; set; }
public string CircuitId { get; set; }
}
SessionModel.cs (модель для моего сеанса на стороне сервера)
public class SessionModel
{
public string Username { get; set; }
public string CircuitId { get; set; }
public DateTime DateTimeAdded { get; set; } //this could be used to timeout the session
//My user data to be stored server side...
public int UserRole { get; set; }
etc...
}
SessionData.cs (хранит список всех активных сессий на сервере)
public class SessionData
{
private List<SessionModel> sessions = new List<SessionModel>();
private readonly ILogger _logger;
public List<SessionModel> Sessions { get { return sessions; } }
public SessionData(ILogger<SessionData> logger)
{
_logger = logger;
}
public void Add(SessionModel model)
{
model.DateTimeAdded = DateTime.Now;
sessions.Add(model);
_logger.LogInformation("Session created. User:{0}, CircuitId:{1}", model.Username, model.CircuitId);
}
//Delete the session by username
public void Delete(string token)
{
//Determine if the token matches a current session in progress
var matchingSession = sessions.FirstOrDefault(s => s.Token == token);
if (matchingSession != null)
{
_logger.LogInformation("Session deleted. User:{0}, Token:{1}", matchingSession.Username, matchingSession.CircuitId);
//remove the session
sessions.RemoveAll(s => s.Token == token);
}
}
public SessionModel Get(string circuitId)
{
return sessions.FirstOrDefault(s => s.CircuitId == circuitId);
}
}
CircuitHandlerService.cs
public class CircuitHandlerService : CircuitHandler
{
public string CircuitId { get; set; }
public SessionData sessionData { get; set; }
public CircuitHandlerService(SessionData sessionData)
{
this.sessionData = sessionData;
}
public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
{
CircuitId = circuit.Id;
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
{
//when the circuit is closing, attempt to delete the session
// this will happen if the current circuit represents the main window
sessionData.Delete(circuit.Id);
return base.OnCircuitClosedAsync(circuit, cancellationToken);
}
public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
return base.OnConnectionDownAsync(circuit, cancellationToken);
}
public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
return base.OnConnectionUpAsync(circuit, cancellationToken);
}
}
Login.razor
@inject ILocalStorageService localStorage
@inject SessionData sessionData
....
public SessionModel session { get; set; } = new SessionModel();
...
if (isUserAuthenticated == true)
{
//assign the sesssion token based on the current CircuitId
session.CircuitId = (circuitHandler as CircuitHandlerService).CircuitId;
sessionData.Add(session);
//Then, store the username in the browser storage
// this username will be used to access the session as needed
UserModel user = new UserModel
{
Username = session.Username,
CircuitId = session.CircuitId
};
await localStorage.SetItemAsync("userSession", user);
NavigationManager.NavigateTo("Home");
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddServerSideBlazor();
services.AddScoped<CircuitHandler>((sp) => new CircuitHandlerService(sp.GetRequiredService<SessionData>()));
services.AddSingleton<SessionData>();
services.AddBlazoredLocalStorage();
...
}
Вот полный пример кода того, как вы можете использовать https://github.com/Blazored/LocalStorage для сохранения данных сеанса. Используется, например, для хранения вошедшего в систему пользователя и т. Д. Подтверждена работа с версией3.0.100-preview9-014004
@page "/login"
@inject Blazored.LocalStorage.ILocalStorageService localStorage
<hr class="mb-5" />
<div class="row mb-5">
<div class="col-md-4">
@if (UserName == null)
{
<div class="input-group">
<input class="form-control" type="text" placeholder="Username" @bind="LoginName" />
<div class="input-group-append">
<button class="btn btn-primary" @onclick="LoginUser">Login</button>
</div>
</div>
}
else
{
<div>
<p>Logged in as: <strong>@UserName</strong></p>
<button class="btn btn-primary" @onclick="Logout">Logout</button>
</div>
}
</div>
</div>
@code {
string UserName { get; set; }
string UserSession { get; set; }
string LoginName { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await GetLocalSession();
localStorage.Changed += (sender, e) =>
{
Console.WriteLine($"Value for key {e.Key} changed from {e.OldValue} to {e.NewValue}");
};
StateHasChanged();
}
}
async Task LoginUser()
{
await localStorage.SetItemAsync("UserName", LoginName);
await localStorage.SetItemAsync("UserSession", "PIOQJWDPOIQJWD");
await GetLocalSession();
}
async Task GetLocalSession()
{
UserName = await localStorage.GetItemAsync<string>("UserName");
UserSession = await localStorage.GetItemAsync<string>("UserSession");
}
async Task Logout()
{
await localStorage.RemoveItemAsync("UserName");
await localStorage.RemoveItemAsync("UserSession");
await GetLocalSession();
}
}
Вы можете хранить данные в сессиях, используя пакет Blazored.SessionStorage.
Установить https://github.com/Blazored/SessionStorage
`@inject Blazored.SessionStorage.ISessionStorageService sessionStorage`
@code {
protected override async Task OnInitializedAsync()
{
await sessionStorage.SetItemAsync("name", "John Smith");
var name = await sessionStorage.GetItemAsync<string>("name");
}
}
С .net 5.0 теперь у вас есть ProtectedSessionStorage, который предоставляет вам зашифрованные данные сеанса браузера.
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
@inject ProtectedSessionStorage storage
// Set
await storage.SetAsync("myFlag", "Green");
// Get
var myFlag= await storage.GetAsync<string>("myFlag");
Использует прерывания JavaScript, поэтому не используйте в
OnInitialize
, но в
OnAfterRender
вместо.
Не использовать состояние сеанса вообще (не пробовал, но подозреваю AddSession
даже не работает под Blazor, поскольку идентификатор сеанса основан на файлах cookie, а HTTP в основном отсутствует на картинке). Даже для веб-приложений, не относящихся к Blazor, нет надежного механизма для определения конца сеанса, поэтому очистка сеанса в лучшем случае беспорядочная.
Вместо этого внедрите реализацию IDistributedCache
который поддерживает настойчивость. Один из самых популярных примеров - Redis cache. В одном из моих проектов на работе я экспериментирую с использованием Microsoft Orleans для распределенного кэширования. Я не вправе делиться нашей внутренней реализацией, но вы можете увидеть ранний пример этого в моем репо здесь.
Под капотом состояние сеанса - это просто словарь (связанный с идентификатором сеанса), содержащий еще один словарь ваших пар ключ-значение. Воспроизвести этот подход с использованием долгосрочного надежного ключа, такого как идентификатор аутентифицированного пользователя, тривиально. Однако я даже не захожу так далеко, поскольку постоянная сериализация и десериализация всего словаря, когда мне обычно нужен только один или два ключа, - это много ненужных накладных расходов. Вместо этого я добавляю к отдельным ключам значений свои уникальные идентификаторы пользователей и сохраняю каждое значение напрямую.
Для реализации сеанса на стороне сервера обратитесь к следующему репозиторию:https://github.com/alihasan94/BlazorSessionApp
На странице Login.razor напишите следующий код:
@page "/"
@using Microsoft.AspNetCore.Http
@using Helpers;
@using Microsoft.JSInterop;
@inject SessionState session
@inject IJSRuntime JSRuntime
@code{
public string Username { get; set; }
public string Password { get; set; }
}
@functions {
private async Task SignIn()
{
if (!session.Items.ContainsKey("Username") && !session.Items.ContainsKey("Password"))
{
//Add to the Singleton scoped Item
session.Items.Add("Username", Username);
session.Items.Add("Password", Password);
//Redirect to homepage
await JSRuntime.InvokeAsync<string>(
"clientJsMethods.RedirectTo", "/home");
}
}
}
<div class="col-md-12">
<h1 class="h3 mb-3 font-weight-normal">Please Sign In</h1>
</div>
<div class="col-md-12 form-group">
<input type="text" @bind="Username" class="form-control" id="username"
placeholder="Enter UserName" title="Enter UserName" />
</div>
<div class="col-md-12 form-group">
<input type="password" @bind="Password" class="form-control" id="password"
placeholder="Enter Password" title="Enter Password" />
</div>
<button @onclick="SignIn">Login</button>
SessionState.cs
using System.Collections.Generic;
namespace BlazorSessionApp.Helpers
{
public class SessionState
{
public SessionState()
{
Items = new Dictionary<string, object>();
}
public Dictionary<string, object> Items { get; set; }
}
}
SessionBootstrapper.cs(содержит логику для настройки сеанса)
using Microsoft.AspNetCore.Http;
namespace BlazorSessionApp.Helpers
{
public class SessionBootstrapper
{
private readonly IHttpContextAccessor accessor;
private readonly SessionState session;
public SessionBootstrapper(IHttpContextAccessor _accessor, SessionState _session)
{
accessor = _accessor;
session = _session;
}
public void Bootstrap()
{
//Singleton Item: services.AddSingleton<SessionState>(); in Startup.cs
//Code to save data in server side session
//If session already has data
string Username = accessor.HttpContext.Session.GetString("Username");
string Password = accessor.HttpContext.Session.GetString("Password");
//If server session is null
if (session.Items.ContainsKey("Username") && Username == null)
{
//get from singleton item
Username = session.Items["Username"]?.ToString();
// save to server side session
accessor.HttpContext.Session.SetString("Username", Username);
//remove from singleton Item
session.Items.Remove("Username");
}
if (session.Items.ContainsKey("Password") && Password == null)
{
Password = session.Items["Password"].ToString();
accessor.HttpContext.Session.SetString("Password", Password);
session.Items.Remove("Password");
}
//If Session is not expired yet then navigate to home
if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password) && accessor.HttpContext.Request.Path == "/")
{
accessor.HttpContext.Response.Redirect("/home");
}
//If Session is expired then navigate to login
else if (string.IsNullOrEmpty(Username) && string.IsNullOrEmpty(Password) && accessor.HttpContext.Request.Path != "/")
{
accessor.HttpContext.Response.Redirect("/");
}
}
}
}
_Host.cshtml(здесь инициализируйте класс SessionBootstrapper)
@page "/"
@namespace BlazorSessionApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
@using BlazorSessionApp.Helpers
@inject SessionBootstrapper bootstrapper
<!DOCTYPE html>
<html lang="en">
<body>
@{
bootstrapper.Bootstrap();
}
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>
<script src="_framework/blazor.server.js"></script>
<script>
// use this to redirect from "Login Page" only in order to save the state on server side session
// because blazor's NavigateTo() won't refresh the page. The function below refresh
// the page and runs bootstrapper.Bootstrap(); to save data in server side session.
window.clientJsMethods = {
RedirectTo: function (path) {
window.location = path;
}
};
</script>
</body>
</html>