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