38

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

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

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

GitHub 问题Add support to SignalR for Session提到您可以使用Context.Items。但我不知道如何使用它,即我不知道热访问HubConnectionContext实例。

我有哪些会话状态选项?

4

9 回答 9

16

注意:此答案来自 2018 年 12 月,当时服务器端 Blazor 的早期版本可用。最有可能的是,它不再相关。

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

所以首先创建状态服务:

public class SessionState
{
    public string SomeProperty { get; set; }
    public int AnotherProperty { get; set; }
}

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

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>

更好的解决方案仍然非常受欢迎。

于 2018-12-26T22:19:44.063 回答
11

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

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

目前有 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是您的根组件的类型,“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. Chris Sainty 将其解释为

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

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

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

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

于 2019-08-01T16:18:43.093 回答
6

这是 ASP.NET Core 5.0+ ( ProtectedSessionStorage, ProtectedLocalStorage) 的相关解决方案:https ://docs.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);
    }
}

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

于 2021-01-13T00:05:26.827 回答
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();
    }
}
于 2019-09-17T11:39:51.313 回答
4

您可以使用 Blazored.SessionStorage 包将数据存储在会话中。

安装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");
    }

}
于 2020-04-20T11:23:32.403 回答
3

我找到了一种在服务器端会话中存储用户数据的方法。我通过使用 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);
    }
}

登录.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");
}

启动.cs

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddServerSideBlazor();
    services.AddScoped<CircuitHandler>((sp) => new CircuitHandlerService(sp.GetRequiredService<SessionData>()));
    services.AddSingleton<SessionData>();
    services.AddBlazoredLocalStorage();
    ...
}
于 2021-03-11T11:17:16.703 回答
1

根本不要使用会话状态(我没有尝试过,但我怀疑AddSession甚至在 Blazor 下都不起作用,因为会话 ID 是基于 cookie 的,而且 HTTP 大多不在图片中)。即使对于非 Blazor Web 应用程序,也没有可靠的机制来检测会话的结束,因此会话清理充其量是混乱的。

相反,注入一个IDistributedCache支持持久性的实现。最流行的例子之一是Redis 缓存。在我的一个工作项目中,我正在尝试使用 Microsoft Orleans 进行分布式缓存。我不能随意分享我们的内部实现,但你可以在我的 repo 中看到一个早期的例子

在底层,会话状态只是一个字典(以会话 ID 为键),其中包含另一个键值对字典。使用长期可靠的密钥(例如经过身份验证的用户 ID)重现该方法是微不足道的。不过,我什至没有走那么远,因为当我通常只需要一个或两个键时,不断地序列化和反序列化整个字典是很多不必要的开销。相反,我使用我唯一的用户 ID 作为单个值键的前缀,并直接存储每个值。

于 2019-12-08T13:05:46.183 回答
1

服务器端会话实现请参考以下存储库: 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>

会话状态.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>
于 2019-12-15T13:09:56.013 回答
1

使用 .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 互操作,因此不要使用 in OnInitialize,而是使用 in OnAfterRender

于 2021-04-01T05:47:34.273 回答