You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor Web Server:实现用户每次新访问站点时强制登出已登录用户

针对你在Blazor Web Server中遇到的「关闭所有页面后重新加载站点,未过期的认证Cookie仍允许访问受保护页面」问题,以下是几个更优雅的解决方案,替代当前每次返回首页都强制登出的粗糙实现:

方案1:将认证Cookie改为会话Cookie(最直接)

如果业务允许关闭浏览器后自动失效认证状态,无需持久化登录,直接修改Cookie配置为会话Cookie(关闭浏览器即自动删除):

在Program.cs的认证配置中调整:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "YourAuthCookie";
        // 启用滑动过期,保持会话内的有效期
        options.SlidingExpiration = true;
        // 不设置持久化过期时间,Cookie随浏览器会话结束而失效
    });

同时,登录时确保不设置IsPersistent为true:

// 登录方法示例
await HttpContext.SignInAsync(
    claimsPrincipal,
    new AuthenticationProperties { IsPersistent = false } // 禁用持久化登录
);

此方案无需额外前端逻辑,用户关闭所有标签页后Cookie自动清除,再次打开站点需重新登录,完全匹配需求。

方案2:本地存储标记区分新会话(兼容持久Cookie场景)

若必须保留持久Cookie(如支持「记住我」功能),但仍希望关闭所有页面后首次打开站点时登出,可通过JS结合localStorage实现会话标记:

  1. 在Index.razor中添加精准判断逻辑:
@inject IJSRuntime JSRuntime
@inject SignOutManager<ClaimsPrincipal> SignOutManager
@inject AuthenticationStateProvider AuthStateProvider

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 检查是否存在会话标记
            var hasSessionMarker = await JSRuntime.InvokeAsync<bool>(
                "() => localStorage.getItem('blazor_session_marker') !== null"
            );

            if (!hasSessionMarker)
            {
                // 无标记说明是新会话,若已登录则执行登出
                var authState = await AuthStateProvider.GetAuthenticationStateAsync();
                if (authState.User.Identity.IsAuthenticated)
                {
                    await SignOutManager.SignOutAsync();
                }
                // 设置会话标记,站内跳转不再触发登出
                await JSRuntime.InvokeVoidAsync(
                    "localStorage.setItem", "blazor_session_marker", "active"
                );
            }
        }
        await base.OnAfterRenderAsync(firstRender);
    }

    // 监听页面卸载,可选发送请求通知服务器清理会话数据
    protected override void OnInitialized()
    {
        JSRuntime.InvokeVoidAsync(@"
            window.addEventListener('beforeunload', () => {
                navigator.sendBeacon('/api/auth/end-session');
            });
        ");
    }
}
  1. 后端可选添加接口处理会话结束:
[ApiController]
[Route("api/auth")]
public class AuthApiController : ControllerBase
{
    [HttpPost("end-session")]
    public IActionResult EndSession()
    {
        // 可在此清理服务器端用户会话相关数据
        return Ok();
    }
}

此方案核心是通过localStorage标记区分「新会话打开站点」和「站内跳转首页」,避免用户在站内操作时被误登出。

方案3:服务器端会话跟踪(严格安全场景)

若需服务器端严格控制会话状态,结合Blazor Web Server的会话特性实现:

  1. 在Program.cs中启用会话:
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

// 会话中间件需放在认证中间件之前
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
  1. 登录时将当前会话ID存入认证Claims:
var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, username),
    new Claim("SessionId", HttpContext.Session.Id)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
    new ClaimsPrincipal(claimsIdentity),
    new AuthenticationProperties { IsPersistent = true }
);
  1. 在Index.razor或全局布局中校验会话一致性:
@inject IHttpContextAccessor HttpContextAccessor
@inject SignOutManager<ClaimsPrincipal> SignOutManager

@code {
    protected override async Task OnInitializedAsync()
    {
        var user = HttpContextAccessor.HttpContext.User;
        if (user.Identity.IsAuthenticated)
        {
            var storedSessionId = user.FindFirst("SessionId")?.Value;
            var currentSessionId = HttpContextAccessor.HttpContext.Session.Id;

            if (storedSessionId != currentSessionId)
            {
                // 会话不匹配,说明是新会话,执行登出
                await SignOutManager.SignOutAsync();
            }
        }
    }
}

用户关闭所有页面后,服务器端会话会超时(或会话Cookie失效),再次打开站点会生成新会话ID,与认证Claims中的旧ID不匹配,从而触发登出。

优化现有IJSRuntime方案

若想保留现有前端调用方式,至少要区分「首次打开站点」和「站内跳转」,避免误登出:

@inject IJSRuntime JSRuntime
@inject NavigationManager NavManager

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 通过来源判断是否为外部打开/直接输入网址
            var referrer = await JSRuntime.InvokeAsync<string>("document.referrer");
            var isNewSession = string.IsNullOrEmpty(referrer) || !referrer.StartsWith(NavManager.BaseUri);
            
            if (isNewSession)
            {
                // 仅新会话时调用登出接口
                await JSRuntime.InvokeVoidAsync("fetch", "/api/auth/signout", new { method = "POST" });
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者plaguebreath

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 14:15:31