如何在服务器端blazor中存储会话数据

Cod*_*odo 11 c# server-side asp.net-core blazor blazor-server-side

在服务器端Blazor应用程序中,我想存储页面导航之间保留的某些状态。我该怎么做?

常规的ASP.NET Core会话状态似乎不可用,因为很可能适用于ASP.NET Core的“会话和应用程序”中的以下说明:

SignalR 应用程序不支持会话,因为SignalR集线器可以独立于HTTP上下文执行。例如,当长轮询请求由集线器在请求的HTTP上下文的生存期之外保持打开状态时,可能会发生这种情况。

GitHub问题向SignalR for Session添加支持中提到您可以使用Context.Items。但是我不知道如何使用它,即我不知道如何访问该HubConnectionContext实例。

我对会话状态有哪些选择?

Rod*_*voi 12

以下是 ASP.NET Core 5.0+ 的相关解决方案(ProtectedSessionStorageProtectedLocalStorage): https: //learn.microsoft.com/en-gb/aspnet/core/blazor/state-management? view=aspnetcore-5.0&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);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,此方法存储加密的数据。

  • ProtectedSessionStorage 和 ProtectedLocalStorage 很棒,它不会将数据保存为明文并使用加密/解密来保存在浏览器存储中。我不知道为什么人们甚至考虑使用其他东西。 (2认同)

Kon*_*cki 11

史蒂夫桑德森深入探讨了如何拯救国家。

对于服务器端 blazor,您需要使用 JavaScript 中的任何存储实现,可以是 cookie、查询参数,或者例如您可以使用local/session storage

目前有 NuGet 包通过IJSRuntimeBlazorStorageMicrosoft.AspNetCore.ProtectedBrowserStorage

现在棘手的部分是服务器端 blazor 正在预渲染页面,因此您的 Razor 视图代码将在服务器上运行和执行,然后才会显示到客户端的浏览器。这会导致一个问题,IJSRuntime因此localStorage此时不可用。您需要禁用预渲染或等待服务器生成的页面发送到客户端的浏览器并建立与服务器的连接

在预渲染期间,没有与用户浏览器的交互连接,浏览器还没有任何可以运行 JavaScript 的页面。所以当时不可能与 localStorage 或 sessionStorage 交互。如果您尝试,您将收到类似于此时无法发出 JavaScript 互操作调用的错误。这是因为组件正在被预渲染。

要禁用预渲染:

(...) 打开您的_Host.razor文件,然后删除对Html.RenderComponentAsync. 然后,打开你的Startup.cs文件,并替换调用endpoints.MapBlazorHub()endpoints.MapBlazorHub<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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

现在到要在页面之间保持状态的实际答案,您应该使用CascadingParameter. Chris Sainty 将其解释为

级联值和参数是一种将值从组件传递到其所有后代的方法,而无需使用传统的组件参数。

这将是一个参数,它是一个类,用于保存您的所有状态数据并公开可以通过您选择的存储提供程序加载/保存的方法。这在Chris Sainty 的博客Steve Sanderson 的笔记Microsoft 文档中进行了解释

更新:微软发布了解释 Blazor 状态管理的新文档

更新 2:请注意,当前 BlazorStorage 无法在具有最新 .NET SDK 预览版的服务器端 Blazor 上正常工作。您可以在我发布临时解决方法的地方关注此问题


Cod*_*odo 8

@JohnB暗示了穷人的状态方法:使用范围服务。在服务器端Blazor中,作用域服务绑定到SignalR连接。这是最接近会话的内容。它对于单个用户当然是私有的。但是它也很容易丢失。重新加载页面或修改浏览器地址列表中的URL会加载以启动新的SignalR连接,创建新的服务实例,从而丢失状态。

因此,首先创建状态服务:

public class SessionState
{
    public string SomeProperty { get; set; }
    public int AnotherProperty { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

然后在App项目(而非服务器项目)的Startup类中配置服务:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<SessionState>();
    }

    public void Configure(IBlazorApplicationBuilder app)
    {
        app.AddComponent<Main>("app");
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以将状态注入任何Blazor页面:

@inject SessionState state

 <p>@state.SomeProperty</p>
 <p>@state.AnotherProperty</p>
Run Code Online (Sandbox Code Playgroud)

更好的解决方案仍然是超级欢迎。


jsm*_*ars 5

这是一个完整的代码示例,说明如何使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)


Jas*_*n D 5

我找到了一种在服务器端会话中存储用户数据的方法。我通过使用 CircuitHandler Id 作为 \xe2\x80\x98token\xe2\x80\x99 供用户访问系统来完成此操作。仅用户名和 CircuitId 存储在客户端 LocalStorage 中(使用 Blazored.LocalStorage);其他用户数据存储在服务器中。我知道这是很多代码,但这是我能找到的在服务器端保证用户数据安全的最佳方法。

\n

UserModel.cs(用于客户端 LocalStorage)

\n
public class UserModel\n{\n    public string Username { get; set; }\n\n    public string CircuitId { get; set; }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

SessionModel.cs(我的服务器端会话的模型)

\n
public class SessionModel\n{\n    public string Username { get; set; }\n\n    public string CircuitId { get; set; }\n\n    public DateTime DateTimeAdded { get; set; }  //this could be used to timeout the session\n\n    //My user data to be stored server side...\n    public int UserRole { get; set; } \n    etc...\n}\n
Run Code Online (Sandbox Code Playgroud)\n

SessionData.cs(保留服务器上所有活动会话的列表)

\n
public class SessionData\n{\n    private List<SessionModel> sessions = new List<SessionModel>();\n    private readonly ILogger _logger;\n    public List<SessionModel> Sessions { get { return sessions; } }\n\n    public SessionData(ILogger<SessionData> logger)\n    {\n        _logger = logger;\n    }\n\n    public void Add(SessionModel model)\n    {\n        model.DateTimeAdded = DateTime.Now;\n\n        sessions.Add(model);\n        _logger.LogInformation("Session created. User:{0}, CircuitId:{1}", model.Username, model.CircuitId);\n    }\n\n    //Delete the session by username\n    public void Delete(string token)\n    {\n        //Determine if the token matches a current session in progress\n        var matchingSession = sessions.FirstOrDefault(s => s.Token == token);\n        if (matchingSession != null)\n        {\n            _logger.LogInformation("Session deleted. User:{0}, Token:{1}", matchingSession.Username, matchingSession.CircuitId);\n\n            //remove the session\n            sessions.RemoveAll(s => s.Token == token);\n        }\n    }\n\n    public SessionModel Get(string circuitId)\n    {\n        return sessions.FirstOrDefault(s => s.CircuitId == circuitId);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

CircuitHandlerService.cs

\n
public class CircuitHandlerService : CircuitHandler\n{\n    public string CircuitId { get; set; }\n    public SessionData sessionData { get; set; }\n\n    public CircuitHandlerService(SessionData sessionData)\n    {\n        this.sessionData = sessionData;\n    }\n\n    public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)\n    {\n        CircuitId = circuit.Id;\n        return base.OnCircuitOpenedAsync(circuit, cancellationToken);\n    }\n\n    public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)\n    {\n        //when the circuit is closing, attempt to delete the session\n        //  this will happen if the current circuit represents the main window\n        sessionData.Delete(circuit.Id); \n\n        return base.OnCircuitClosedAsync(circuit, cancellationToken);\n    }\n\n    public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)\n    {\n        return base.OnConnectionDownAsync(circuit, cancellationToken);\n    }\n\n    public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)\n    {\n        return base.OnConnectionUpAsync(circuit, cancellationToken);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

登录.razor

\n
@inject ILocalStorageService localStorage\n@inject SessionData sessionData\n....\npublic SessionModel session { get; set; } = new SessionModel();\n...\nif (isUserAuthenticated == true)\n{\n    //assign the sesssion token based on the current CircuitId\n    session.CircuitId = (circuitHandler as CircuitHandlerService).CircuitId;\n    sessionData.Add(session);\n\n    //Then, store the username in the browser storage\n    //  this username will be used to access the session as needed\n    UserModel user = new UserModel\n    {\n        Username = session.Username,\n        CircuitId = session.CircuitId\n    };\n\n    await localStorage.SetItemAsync("userSession", user);\n    NavigationManager.NavigateTo("Home");\n}\n
Run Code Online (Sandbox Code Playgroud)\n

启动.cs

\n
public void ConfigureServices(IServiceCollection services)\n{\n    ...\n    services.AddServerSideBlazor();\n    services.AddScoped<CircuitHandler>((sp) => new CircuitHandlerService(sp.GetRequiredService<SessionData>()));\n    services.AddSingleton<SessionData>();\n    services.AddBlazoredLocalStorage();\n    ...\n}\n
Run Code Online (Sandbox Code Playgroud)\n

  • 注意力!在 *Singleton* 中使用 *Lists* 时要小心(本例中为 SessionData)。作为 Blazor 服务器应用程序,这可能会导致竞争条件和不良行为。请使用 *ConcurrentDictionary* 或任何其他线程安全集合。 (2认同)