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

posted on 2021-08-15 22:26  SunnyTrudeau  阅读(1934)  评论(1编辑  收藏  举报