From b0c994b003acb98523557971ee026e82aaf1f85b Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 9 Oct 2026 16:12:19 +0800 Subject: [PATCH 1/3] fix(Layout): update authentication state on navigation --- .../Components/Layout/Layout.razor.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/BootstrapBlazor/Components/Layout/Layout.razor.cs b/src/BootstrapBlazor/Components/Layout/Layout.razor.cs index 0a8dadaf79f..77c3fcd10db 100644 --- a/src/BootstrapBlazor/Components/Layout/Layout.razor.cs +++ b/src/BootstrapBlazor/Components/Layout/Layout.razor.cs @@ -729,18 +729,34 @@ public async Task UpdateAsync(string key) private void Navigation_LocationChanged(object? sender, LocationChangedEventArgs e) { + if (OnAuthorizing == null) + { + return; + } - if (OnAuthorizing != null) + InvokeAsync(async () => { - InvokeAsync(async () => + var auth = await OnAuthorizing(e.Location); + if (auth) { - var auth = await OnAuthorizing(e.Location); - if (!auth && IsAutoNavigateWhenNotAuthorize) + // 当前地址已授权时恢复 UI 状态 + if (!_authenticated) { - Navigation.NavigateTo(NotAuthorizeUrl, true); + _authenticated = true; + StateHasChanged(); } - }); - } + } + else if (IsAutoNavigateWhenNotAuthorize) + { + Navigation.NavigateTo(NotAuthorizeUrl, true); + } + else if (_authenticated) + { + // 未开启自动跳转时更新 UI 状态显示 NotAuthorized 模板 + _authenticated = false; + StateHasChanged(); + } + }); } /// From 298b86692a4b25791830d3ee15b38be4af205d54 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 9 Oct 2026 16:12:35 +0800 Subject: [PATCH 2/3] chore: bump version 11.0.1-beta02 --- src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index f7bfcec7507..00d482fedc4 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 11.0.1-beta01 + 11.0.1-beta02 From ef1d43f4798abc0fa294a0649afe226d2517327c Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 9 Oct 2026 16:14:51 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/LayoutTest.cs | 123 +++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/test/UnitTest/Components/LayoutTest.cs b/test/UnitTest/Components/LayoutTest.cs index 38954cebb14..7b6bbe76087 100644 --- a/test/UnitTest/Components/LayoutTest.cs +++ b/test/UnitTest/Components/LayoutTest.cs @@ -556,6 +556,129 @@ public void IsAutoNavigateWhenNotAuthorize_Ok() cut.WaitForAssertion(() => Assert.Equal("http://localhost/", navMan.Uri)); } + [Fact] + public void IsAutoNavigateWhenNotAuthorize_RenderNotAuthorized() + { + var navMan = Context.Services.GetRequiredService(); + var cut = Context.Render(pb => + { + pb.Add(a => a.Resource, null); + pb.Add(a => a.NotAuthorizeUrl, "/Test"); + pb.Add(a => a.IsAutoNavigateWhenNotAuthorize, false); + pb.Add(a => a.Main, builder => builder.AddContent(0, "Main")); + pb.Add(a => a.NotAuthorized, builder => builder.AddContent(0, "NotAuth")); + pb.Add(a => a.OnAuthorizing, url => Task.FromResult(false)); + }); + + // 初始状态已授权 渲染 Main 内容 + Assert.Contains("Main", cut.Markup); + Assert.DoesNotContain("NotAuth", cut.Markup); + + // 导航到未授权地址 不跳转 显示 NotAuthorized 模板 + navMan.NavigateTo("/unauthorized"); + cut.WaitForAssertion(() => Assert.Contains("NotAuth", cut.Markup)); + Assert.Equal("http://localhost/unauthorized", navMan.Uri); + Assert.DoesNotContain("Main", cut.Markup); + } + + [Fact] + public void IsAutoNavigateWhenNotAuthorize_RestoreAuthorized() + { + var navMan = Context.Services.GetRequiredService(); + var authenticated = false; + var cut = Context.Render(pb => + { + pb.Add(a => a.Resource, null); + pb.Add(a => a.NotAuthorizeUrl, "/Test"); + pb.Add(a => a.IsAutoNavigateWhenNotAuthorize, false); + pb.Add(a => a.Main, builder => builder.AddContent(0, "Main")); + pb.Add(a => a.NotAuthorized, builder => builder.AddContent(0, "NotAuth")); + pb.Add(a => a.OnAuthorizing, url => Task.FromResult(authenticated)); + }); + + // 导航到未授权地址 显示 NotAuthorized 模板 + navMan.NavigateTo("/unauthorized"); + cut.WaitForAssertion(() => Assert.Contains("NotAuth", cut.Markup)); + + // 导航到已授权地址 恢复 Layout 内容 + authenticated = true; + navMan.NavigateTo("/authorized"); + cut.WaitForAssertion(() => Assert.Contains("Main", cut.Markup)); + Assert.DoesNotContain("NotAuth", cut.Markup); + Assert.Equal("http://localhost/authorized", navMan.Uri); + } + + [Fact] + public void IsAutoNavigateWhenNotAuthorize_DefaultNavigateToLogin() + { + var navMan = Context.Services.GetRequiredService(); + var cut = Context.Render(pb => + { + pb.Add(a => a.Resource, null); + pb.Add(a => a.NotAuthorizeUrl, "/Test"); + pb.Add(a => a.Main, builder => builder.AddContent(0, "Main")); + pb.Add(a => a.NotAuthorized, builder => builder.AddContent(0, "NotAuth")); + pb.Add(a => a.OnAuthorizing, url => Task.FromResult(url == "http://localhost/Test")); + }); + + // 默认 IsAutoNavigateWhenNotAuthorize = true 跳转到 NotAuthorizeUrl 不显示 NotAuthorized 模板 + navMan.NavigateTo("/unauthorized"); + cut.WaitForAssertion(() => Assert.Equal("http://localhost/Test", navMan.Uri)); + Assert.DoesNotContain("NotAuth", cut.Markup); + Assert.Contains("Main", cut.Markup); + } + + [Fact] + public void IsAutoNavigateWhenNotAuthorize_OnAuthorizingNull() + { + var navMan = Context.Services.GetRequiredService(); + var cut = Context.Render(pb => + { + pb.Add(a => a.Resource, null); + pb.Add(a => a.NotAuthorizeUrl, "/Test"); + pb.Add(a => a.IsAutoNavigateWhenNotAuthorize, false); + pb.Add(a => a.Main, builder => builder.AddContent(0, "Main")); + pb.Add(a => a.NotAuthorized, builder => builder.AddContent(0, "NotAuth")); + }); + + // 未设置 OnAuthorizing 时不订阅 LocationChanged 事件 导航不执行任何授权逻辑 + navMan.NavigateTo("/unauthorized"); + Assert.Equal("http://localhost/unauthorized", navMan.Uri); + Assert.Contains("Main", cut.Markup); + Assert.DoesNotContain("NotAuth", cut.Markup); + } + + [Fact] + public void IsAutoNavigateWhenNotAuthorize_OnAuthorizingSetToNull() + { + var navMan = Context.Services.GetRequiredService(); + var invoked = 0; + var cut = Context.Render(pb => + { + pb.Add(a => a.Resource, null); + pb.Add(a => a.NotAuthorizeUrl, "/Test"); + pb.Add(a => a.IsAutoNavigateWhenNotAuthorize, false); + pb.Add(a => a.Main, builder => builder.AddContent(0, "Main")); + pb.Add(a => a.NotAuthorized, builder => builder.AddContent(0, "NotAuth")); + pb.Add(a => a.OnAuthorizing, url => + { + invoked++; + return Task.FromResult(false); + }); + }); + + navMan.NavigateTo("/unauthorized"); + cut.WaitForAssertion(() => Assert.Contains("NotAuth", cut.Markup)); + Assert.Equal(1, invoked); + + // 将 OnAuthorizing 置为 null 后续导航直接返回不再执行授权逻辑 + cut.Render(pb => pb.Add(a => a.OnAuthorizing, (Func>?)null)); + navMan.NavigateTo("/home"); + Assert.Equal("http://localhost/home", navMan.Uri); + Assert.Equal(1, invoked); + Assert.Contains("NotAuth", cut.Markup); + } + [Fact] public void Main_Ok() {