隐藏

Blazor Server访问Identity Server 4单点登录

发布:2022/4/27 14:32:29作者:管理员 来源:本站 浏览次数:1493



网上有大量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