Blazor Server訪問Identity Server 4單點登錄


網上有大量Asp.Net Core訪問id4單點登錄的介紹,但是Blazor Server的不多,我參考網上文章練習了一下,做一個記錄。

參考文章,感謝作者:

Blazor與IdentityServer4的集成 - towerbit - 博客園 (cnblogs.com)

Blazor.Server以正確的方式集成Ids4_dotNET跨平台-CSDN博客

 

創建Identity Server 4項目

在控制台進入解決方案目錄,安裝id4項目模板。

D:\software\gitee\blzid4>dotnet new -i IdentityServer4.Templates

新建一個測試用的id4項目,帶有UI和測試用戶。

D:\software\gitee\blzid4>dotnet new is4inmem -n Id4Web

已成功創建模板“IdentityServer4 with In-Memory Stores and Test Users”。

 

新增2個客戶端定義

                new Client()
                {
                    ClientId="BlazorServer1",
                    ClientName = "BlazorServer1",
                    ClientSecrets=new []{new Secret("BlazorServer1.Secret".Sha256())},

                    AllowedGrantTypes = GrantTypes.Code,
                    
                    AllowedCorsOrigins = { "https://localhost:5101" },
                    RedirectUris = { "https://localhost:5101/signin-oidc" },
                    PostLogoutRedirectUris = { "https://localhost:5101/signout-callback-oidc" },

                    AllowedScopes = { "openid", "profile", "scope1" }
                },

                new Client()
                {
                    ClientId="BlazorServer2",
                    ClientName = "BlazorServer2",
                    ClientSecrets=new []{new Secret("BlazorServer2.Secret".Sha256())},

                    AllowedGrantTypes = GrantTypes.Code,

                    AllowedCorsOrigins = { "https://localhost:5201" },
                    RedirectUris = { "https://localhost:5201/signin-oidc" },
                    PostLogoutRedirectUris = { "https://localhost:5201/signout-callback-oidc" },

                    AllowedScopes = { "openid", "profile", "scope1" }
                },

  

 

創建Blazor Server項目

創建Blazor Server項目。NuGet安裝

    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.9" />

 

修改App.razor實現未登錄用戶自動跳轉登錄

@inject IJSRuntime _jsRuntime
@inject NavigationManager _navManager

<CascadingAuthenticationState>

    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (!context.User.Identity.IsAuthenticated)
                    {
                        //如果用戶未登錄,跳轉到Account控制器Login函數,發起登錄
                        _jsRuntime.InvokeVoidAsync("window.location.assign", $"account/login?returnUrl={Uri.EscapeDataString(_navManager.Uri)}");
                    }
                    else
                    {
                        <h4 class="text-danger">Sorry</h4>
                        <p>You're not authorized to reach this page.</p>
                        <p>You may need to log in as a different user.</p>
                        <a href="/account/login" class="btn btn-primary">Login</a>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

  

修改program默認端口

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                        .UseUrls("https://*:5101")
                        .UseStartup<Startup>();
                });

  

修改launchSettings.json默認端口

      "applicationUrl": "https://localhost:5101",

 

修改startup添加oidc認證服務

            //默認采用cookie認證方案,添加oidc認證方案
            services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "cookies";
                    options.DefaultChallengeScheme = "oidc";
                })
                //配置cookie認證
                .AddCookie("cookies")
                .AddOpenIdConnect("oidc", options =>
                {
                    //id4服務的地址
                    options.Authority = "https://localhost:5001";

                    //id4配置的ClientId以及ClientSecrets
                    options.ClientId = "BlazorServer1";
                    options.ClientSecret = "BlazorServer1.Secret";

                    //認證模式
                    options.ResponseType = "code";

                    //保存token到本地
                    options.SaveTokens = true;

                    //很重要,指定從Identity Server的UserInfo地址來取Claim
                    options.GetClaimsFromUserInfoEndpoint = true;

                });

  

開啟認證和授權服務

            app.UseRouting();

            //開啟認證和授權服務
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>

  

添加登錄用的MVC控制器AccountController,這個真是Blazor Server的痛點了,非要借助MVC做一次跳轉,Net 7是不是能安排解決一下?

public class AccountController : Controller
    {
        private readonly ILogger _logger;

        public AccountController(ILogger<AccountController> logger)
        {
            _logger = logger;
        }

        /// <summary>
        /// 跳轉到Identity Server 4統一登錄
        /// </summary>
        /// <param name="returnUrl">登錄成功后,返回之前的網頁路由</param>
        /// <returns></returns>
        [HttpGet]
        public IActionResult Login(string returnUrl = "")
        {
            if (string.IsNullOrEmpty(returnUrl))
                returnUrl = "/";

            var properties = new AuthenticationProperties
            {
                //記住登錄狀態
                IsPersistent = true,

                RedirectUri = returnUrl
            };

            _logger.LogInformation($"id4跳轉登錄, returnUrl={returnUrl}");

            //跳轉到Identity Server 4統一登錄
            return Challenge(properties, "oidc");
        }

        /// <summary>
        /// 退出登錄
        /// </summary>
        /// <param name="returnUrl"></param>
        /// <returns></returns>
        [HttpGet]
        public async Task<IActionResult> Logout()
        {
            var userName = HttpContext.User.Identity?.Name;

            _logger.LogInformation($"{userName}退出登錄。");

            //刪除登錄狀態cookies
            await HttpContext.SignOutAsync("cookies");

            var properties = new AuthenticationProperties
            {
                RedirectUri = "/"
            };

            //跳轉到Identity Server 4統一退出登錄
            return SignOut(properties, "oidc");
        }

  

還要修改startup讓系統支持MVC路由。

            app.UseEndpoints(endpoints =>
            {
                //支持MVC路由,跳轉登錄
                endpoints.MapDefaultControllerRoute();

                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });

  

在Index頁面顯示一下登錄用戶信息

<AuthorizeView>
    <Authorized>

        <p>您已經登錄</p>

        <div class="card">
            <div class="card-header">
                <h2>context.User.Claims</h2>
            </div>
            <div class="card-body">
                <dl>
                    <dt>context.User.Identity.Name</dt>
                    <dd>@context.User.Identity.Name</dd>
                    @foreach (var claim in context.User.Claims)
                    {
                        <dt>@claim.Type</dt>
                        <dd>@claim.Value</dd>
                    }
                </dl>
            </div>
        </div>

        <a class="nav-link" href="Account/Logout">退出登錄</a>
    </Authorized>

    <NotAuthorized>
        <p>您還沒有登錄,請先登錄</p>
        <a class="nav-link" href="Account/Login">登錄</a>
    </NotAuthorized>

</AuthorizeView>

  

給counter頁面增加認證要求,這樣如果沒有登錄的狀態下,點擊counter頁面就會觸發自動跳轉登錄

@attribute [Authorize]

 

把id4項目和blazor server項目一起運行,點擊BlzWeb1主頁的登錄,即可跳轉到id4登錄頁面

 

輸入id4提供的測試賬號aclie和密碼alice。

登錄成功,跳轉回到BlzWeb1主頁,看一下用戶身份信息。

可以通過HttpContext獲取更多信息。

修改startup添加服務。

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

 

修改BlzWeb1主頁

@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor httpContextAccessor

@if (AuthResult is not null)
        {
            <p>AuthResult.Principal.Identity.Name: <strong>@AuthResult.Principal.Identity.Name</strong></p>

            <div class="card">
                <div class="card-header">
                    <h2>AuthenticateResult.Principal</h2>
                </div>
                <div class="card-body">
                    <dl>
                        @foreach (var claim in AuthResult.Principal.Claims)
                        {
                            <dt>@claim.Type</dt>
                            <dd>@claim.Value</dd>
                        }
                    </dl>
                </div>
            </div>

            <div class="card">
                <div class="card-header">
                    <h2>AuthenticateResult.Properties.Items</h2>
                </div>
                <div class="card-body">
                    <dl>
                        @foreach (var prop in AuthResult.Properties.Items)
                        {
                            <dt>@prop.Key</dt>
                            <dd>@prop.Value</dd>
                        }
                    </dl>
                </div>
            </div>
        }

@code{
    private AuthenticateResult AuthResult;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            AuthResult = await httpContextAccessor.HttpContext.AuthenticateAsync();

            StateHasChanged();
        }
    }

}

  

可以看到token等信息。

但是獲取不到context.User.Identity.Name,這也是一個痛點,為什么id4就是不爽快地返回Username呢?

修改startup可以把id4用戶的name字段賦值給User.Identity.Name,然而我想要的是id4用戶的Username。

                    //這里是個ClaimType的轉換,Identity Server的ClaimType和Blazor中間件使用的名稱有區別,需要統一。

                    //User.Identity.Name=JwtClaimTypes.Name

                    options.TokenValidationParameters.NameClaimType = "name";

                    //options.TokenValidationParameters.RoleClaimType = "role";

 

有一個鴕鳥辦法,就是自己定義的用戶class中,讓name跟Username保持同一個值。

獲取role則更麻煩,還要轉換數據類型,補充添加到cliams,這些最常用的功能都沒銜接好,心很累。

 

接着創建第二個Blazor Server項目。

 

測試驗證

注意這里有坑!

測試方案一:

在VS2019同時調試運行id4項目和2個Blazor Server項目,自動打開了3個Edge瀏覽器窗口。在BlzWeb1網頁登錄,然后刷新BlzWeb2網頁,點擊主頁的登錄按鈕,會發現還要再次跳轉到id4網頁登錄,根本沒有實現單點登錄!為什么會這樣!我也不知道。

百度查資料,沒有結果。

 

測試方案二:

后來我改變了一下測試方法,在BlzWeb1瀏覽器新建一個頁卡,然后訪問BlzWeb2主頁,然后再點擊BlzWeb2主頁的登錄按鈕,這次自動登錄了。

然后在BlzWeb1主頁退出登錄,再次刷新BlzWeb2主頁地址欄,它又提示當前是未登錄狀態了,實現了單點登錄。

如果在測試過程中,反復在兩個Edge瀏覽器登錄,退出,很任意導致網頁死機,不知道是什么問題。

 

查看Edge的cookies,可以看到在同一個瀏覽器的2個頁卡運行的BlzWeb1和BlzWeb2的登錄狀態相同,共享了cookies,這是單點登錄的原理和基礎。

注意,如果部署BlzWeb1和BlzWeb2到雲服務器測試,需要共用一個數據保護秘鑰,因為Asp.Net Core采用數據保護秘鑰加密cookies,要確保2個項目能夠互認cookies,詳情參見:

DataProtection設置問題引起不同ASP.NET Core站點無法共享用戶驗證Cookie - dudu - 博客園 (cnblogs.com)

 

DEMO代碼地址:https://gitee.com/woodsun/blzid4


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM